mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 09:56:01 -10:00
Commands were hanging for several minutes when run with low-privilege credentials against accounts where opt-in regions (e.g. me-south-1) are not enabled. Each unreachable region exhausted the full AWS SDK retry budget (3 attempts × ~30s timeout) per service call, compounding across every check in all-checks mode. Three-part fix: 1. TCP reachability probe: Before making any API calls, probe sts.REGION.amazonaws.com:443 with a 3-second timeout. Unreachable regions are filtered out concurrently and cached for the run so the probe only happens once per region. This eliminates multi-minute stalls caused by regions that are not enabled for the account. 2. --max-retries flag: New persistent flag on the aws subcommand (default 3, matching prior behaviour). Setting --max-retries 0 skips all retries, useful when running black-box against an account where most API calls will return 403/401 anyway. 3. Remove me-south-1 from the static fallback region list: This Bahrain opt-in region was the primary offender reported in issue #132. It is excluded from the hardcoded list so accounts that never opted in skip it entirely even before the TCP probe runs. Also fixes a pre-existing bug in GetIamSimResult where a redundant second isRoleAdminBool check after the cache-or-compute block would overwrite a correct "YES" result from the cache with "No" (because isRoleAdminBool stays false on a cache hit). Fixes #132
588 lines
19 KiB
Go
588 lines
19 KiB
Go
package internal
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"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{}
|
|
|
|
// MaxRetries controls the maximum number of retry attempts for AWS API calls.
|
|
// The default AWS SDK behavior is 3 attempts. Set to 0 for no retries, which
|
|
// is useful when running with limited-permission credentials to avoid slow
|
|
// timeouts on access-denied errors.
|
|
MaxRetries = 3
|
|
|
|
// unreachableRegions tracks regions that failed a TCP connectivity check so
|
|
// they can be skipped for the remainder of the run. Protected by a mutex
|
|
// since multiple goroutines may probe regions concurrently.
|
|
unreachableRegions = make(map[string]bool)
|
|
unreachableRegionsMu sync.RWMutex
|
|
|
|
// RegionReachabilityTimeout controls how long the TCP probe waits before
|
|
// declaring a region unreachable. Default 3 seconds is enough for any
|
|
// healthy AWS endpoint.
|
|
RegionReachabilityTimeout = 3 * time.Second
|
|
)
|
|
|
|
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(), MaxRetries)
|
|
}), 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(), MaxRetries)
|
|
}), 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("[%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))
|
|
}
|
|
//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
|
|
}
|
|
|
|
// isRegionReachable does a fast TCP dial to the STS endpoint in the given region.
|
|
// STS is available in every region and is a lightweight endpoint to probe. If the
|
|
// connection succeeds, the region is reachable. Results are cached so each region
|
|
// is only probed once per run.
|
|
func isRegionReachable(region string) bool {
|
|
unreachableRegionsMu.RLock()
|
|
alreadyUnreachable := unreachableRegions[region]
|
|
unreachableRegionsMu.RUnlock()
|
|
if alreadyUnreachable {
|
|
return false
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("sts.%s.amazonaws.com:443", region)
|
|
conn, err := net.DialTimeout("tcp", endpoint, RegionReachabilityTimeout)
|
|
if err != nil {
|
|
unreachableRegionsMu.Lock()
|
|
unreachableRegions[region] = true
|
|
unreachableRegionsMu.Unlock()
|
|
TxtLog.Warnf("Region %s is unreachable (probe to %s failed: %v) - skipping for this run", region, endpoint, err)
|
|
return false
|
|
}
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
// FilterReachableRegions takes a list of regions and returns only the ones that
|
|
// pass a TCP connectivity probe. Unreachable regions are probed in parallel and
|
|
// cached so subsequent calls return immediately.
|
|
func FilterReachableRegions(regions []string) []string {
|
|
// Probe all regions concurrently for speed
|
|
type probeResult struct {
|
|
region string
|
|
reachable bool
|
|
}
|
|
resultsChan := make(chan probeResult, len(regions))
|
|
|
|
for _, region := range regions {
|
|
go func(r string) {
|
|
resultsChan <- probeResult{region: r, reachable: isRegionReachable(r)}
|
|
}(region)
|
|
}
|
|
|
|
var reachableRegions []string
|
|
for range regions {
|
|
result := <-resultsChan
|
|
if result.reachable {
|
|
reachableRegions = append(reachableRegions, result.region)
|
|
}
|
|
}
|
|
return reachableRegions
|
|
}
|
|
|
|
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)
|
|
}
|
|
reachableRegions := FilterReachableRegions(AWSRegions)
|
|
Cache.Set(cacheKey, reachableRegions, cache.DefaultExpiration)
|
|
return reachableRegions
|
|
}
|
|
|
|
for _, region := range regions.Regions {
|
|
enabledRegions = append(enabledRegions, *region.RegionName)
|
|
}
|
|
enabledRegions = FilterReachableRegions(enabledRegions)
|
|
Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration)
|
|
return enabledRegions
|
|
|
|
}
|
|
|
|
// ModuleFirstFormatter formats logs with module field before the message
|
|
type ModuleFirstFormatter struct {
|
|
logrus.TextFormatter
|
|
}
|
|
|
|
func (f *ModuleFirstFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
// Use the standard text formatter to get the base format
|
|
formatted, err := f.TextFormatter.Format(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If there's a module field, reformat to put it before msg
|
|
if module, ok := entry.Data["module"]; ok {
|
|
// Parse the formatted output and rebuild with module first
|
|
str := string(formatted)
|
|
|
|
// Find and extract the module field
|
|
moduleStr := fmt.Sprintf("module=%v", module)
|
|
|
|
// If the formatted string contains the module field, move it before msg=
|
|
if idx := strings.Index(str, moduleStr); idx > 0 {
|
|
// Remove module from its current position
|
|
before := str[:idx]
|
|
after := str[idx+len(moduleStr):]
|
|
// Trim any extra space that might be left
|
|
after = strings.TrimPrefix(after, " ")
|
|
|
|
// Find where msg= starts
|
|
if msgIdx := strings.Index(before, "msg="); msgIdx > 0 {
|
|
// Rebuild: everything before msg, then module, then msg and after
|
|
newStr := before[:msgIdx] + moduleStr + " " + before[msgIdx:] + after
|
|
return []byte(newStr), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return formatted, nil
|
|
}
|
|
|
|
// SplitLevelHook is a custom logrus hook that splits logs by level
|
|
// Error and Fatal messages go to errorWriter, everything else goes to infoWriter
|
|
type SplitLevelHook struct {
|
|
errorWriter io.Writer
|
|
infoWriter io.Writer
|
|
formatter logrus.Formatter
|
|
}
|
|
|
|
func (h *SplitLevelHook) Levels() []logrus.Level {
|
|
return logrus.AllLevels
|
|
}
|
|
|
|
func (h *SplitLevelHook) Fire(entry *logrus.Entry) error {
|
|
line, err := h.formatter.Format(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch entry.Level {
|
|
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
|
|
_, err = h.errorWriter.Write(line)
|
|
default:
|
|
_, err = h.infoWriter.Write(line)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// txtLogger - Returns the txt logger
|
|
func TxtLogger() *logrus.Logger {
|
|
txtLogger := logrus.New()
|
|
|
|
// Open error log file (only errors and fatal messages)
|
|
errorFile, 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 {
|
|
errorFile, 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 error log file %v", err))
|
|
}
|
|
|
|
// Open info log file (all non-error messages)
|
|
infoFile, err := os.OpenFile(fmt.Sprintf("%s/cloudfox-info.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
infoFile, err = os.OpenFile(fmt.Sprintf("./cloudfox-info.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
}
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Failed to open info log file %v", err))
|
|
}
|
|
|
|
// Discard default output since we're using hooks
|
|
txtLogger.SetOutput(io.Discard)
|
|
|
|
// Add custom hook to split logs by level
|
|
txtLogger.AddHook(&SplitLevelHook{
|
|
errorWriter: errorFile,
|
|
infoWriter: infoFile,
|
|
formatter: &ModuleFirstFormatter{},
|
|
})
|
|
|
|
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"
|
|
|
|
// CommandCounter tracks per-module task progress. All fields are accessed
|
|
// atomically through the Incr/Decr/Load methods so worker goroutines and the
|
|
// spinner goroutine can update and read counts safely.
|
|
type CommandCounter struct {
|
|
Total int64
|
|
Pending int64
|
|
Complete int64
|
|
Error int64
|
|
Executing int64
|
|
}
|
|
|
|
func (c *CommandCounter) IncrTotal() { atomic.AddInt64(&c.Total, 1) }
|
|
func (c *CommandCounter) IncrPending() { atomic.AddInt64(&c.Pending, 1) }
|
|
func (c *CommandCounter) IncrComplete() { atomic.AddInt64(&c.Complete, 1) }
|
|
func (c *CommandCounter) IncrError() { atomic.AddInt64(&c.Error, 1) }
|
|
func (c *CommandCounter) IncrExecuting() { atomic.AddInt64(&c.Executing, 1) }
|
|
|
|
func (c *CommandCounter) DecrPending() { atomic.AddInt64(&c.Pending, -1) }
|
|
func (c *CommandCounter) DecrExecuting() { atomic.AddInt64(&c.Executing, -1) }
|
|
|
|
func (c *CommandCounter) LoadTotal() int64 { return atomic.LoadInt64(&c.Total) }
|
|
func (c *CommandCounter) LoadComplete() int64 { return atomic.LoadInt64(&c.Complete) }
|
|
func (c *CommandCounter) LoadError() int64 { return atomic.LoadInt64(&c.Error) }
|
|
|
|
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.LoadComplete(), counter.LoadTotal(), spinType, counter.LoadError(), fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())))
|
|
case <-done:
|
|
complete := counter.LoadComplete()
|
|
fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)\n", cyan(callingModuleName), complete, complete, spinType, counter.LoadError(), 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
|
|
}
|