package aws import ( "fmt" "os" "path/filepath" "sort" "strconv" "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/awsservicemap" "github.com/sirupsen/logrus" ) type ECRModule struct { // General configuration data ECRClient sdk.AWSECRClientInterface Caller sts.GetCallerIdentityOutput AWSRegions []string AWSOutputType string AWSTableCols string Goroutines int AWSProfile string WrapTable bool // Main module data Repositories []Repository CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry } type Repository struct { AWSService string Region string Name string URI string PushedAt string ImageTags string ImageSize int64 Policy policy.Policy PolicyJSON string } func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) { // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "ecr" 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 container repositories 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 Repository) // 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 // This is the complete list of potential table columns m.output.Headers = []string{ "Account", "Service", "Region", "Name", "URI", "PushedAt", "ImageTags", "ImageSize", } // 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, ",") // If the user specified wide as the output format, use these columns. } else if m.AWSOutputType == "wide" { tableCols = []string{ "Account", "Service", "Region", "Name", "URI", "PushedAt", "ImageTags", "ImageSize", } // Otherwise, use the default columns. } else { tableCols = []string{ "Service", "Region", "Name", "URI", "PushedAt", "ImageTags", "ImageSize", } } // sort the table by Name sort.Slice(m.Repositories, func(i, j int) bool { return m.Repositories[i].Name < m.Repositories[j].Name }) // Table rows for i := range m.Repositories { m.output.Body = append( m.output.Body, []string{ aws.ToString(m.Caller.Account), m.Repositories[i].AWSService, m.Repositories[i].Region, m.Repositories[i].Name, m.Repositories[i].URI, m.Repositories[i].PushedAt, m.Repositories[i].ImageTags, strconv.Itoa(int(m.Repositories[i].ImageSize)), }, ) } 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] %s repositories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) } else { fmt.Printf("[%s][%s] No repositories 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 *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) { defer wg.Done() servicemap := &awsservicemap.AwsServiceMap{ JsonFileSource: "DOWNLOAD_FROM_AWS", } res, err := servicemap.IsServiceInRegion("ecr", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver) } } func (m *ECRModule) Receiver(receiver chan Repository, receiverDone chan bool) { defer close(receiverDone) for { select { case data := <-receiver: m.Repositories = append(m.Repositories, data) case <-receiverDone: receiverDone <- true return } } } func (m *ECRModule) 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, "ecr-pull-commands.txt") var out string out = out + fmt.Sprintln("#############################################") out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox") out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the repositories.") out = out + fmt.Sprintln("# E.g., export profile=dev-prod.") out = out + fmt.Sprintln("#############################################") out = out + fmt.Sprintln("") for _, repo := range m.Repositories { loginURI := strings.Split(repo.URI, "/")[0] out = out + fmt.Sprintf("aws --profile $profile --region %s ecr get-login-password | docker login --username AWS --password-stdin %s\n", repo.Region, loginURI) out = out + fmt.Sprintf("docker pull %s\n", repo.URI) out = out + fmt.Sprintf("docker inspect %s\n", repo.URI) out = out + fmt.Sprintf("docker history --no-trunc %s\n", repo.URI) out = out + fmt.Sprintf("docker run -it --entrypoint /bin/sh %s\n", repo.URI) out = out + fmt.Sprintf("docker save %s -o %s.tar\n\n", repo.URI, repo.Name) } 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("Use the commands below to authenticate to ECR and download the images that look interesting")) fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("You will need the ecr:GetAuthorizationToken on the registry to authenticate and this is not part of the SecurityAudit permissions policy")) fmt.Print(out) 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 *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) { defer func() { m.CommandCounter.Executing-- m.CommandCounter.Complete++ wg.Done() }() semaphore <- struct{}{} defer func() { <-semaphore }() Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } for _, repo := range Repositories { repoName := aws.ToString(repo.RepositoryName) repoURI := aws.ToString(repo.RepositoryUri) //created := *repo.CreatedAt //fmt.Printf("%s, %s, %s", repoName, repoURI, created) images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ continue } sort.Slice(images, func(i, j int) bool { return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05") }) var image types.ImageDetail var imageTags string if len(images) > 1 { image = images[len(images)-1] } else if len(images) == 1 { image = images[0] } else { continue } if len(image.ImageTags) > 0 { imageTags = image.ImageTags[0] } else { imageTags = "No tags" } //imageTags := image.ImageTags[0] pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05") imageSize := aws.ToInt64(image.ImageSizeInBytes) pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags) dataReceiver <- Repository{ AWSService: "ECR", Name: repoName, Region: r, URI: pullURI, PushedAt: pushedAt, ImageTags: imageTags, ImageSize: imageSize, } } } // func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) { // var repositories []types.Repository // Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) // if err != nil { // m.CommandCounter.Error++ // return nil, err // } // repositories = append(repositories, Repositories...) // return repositories, nil // } // func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) { // var images []types.ImageDetail // ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName) // if err != nil { // m.CommandCounter.Error++ // return nil, err // } // images = append(images, ImageDetails...) // return images, nil // } func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) { var repoPolicy policy.Policy Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository) if err != nil { m.CommandCounter.Error++ return repoPolicy, err } repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy)) if err != nil { return repoPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", repository, err) } return repoPolicy, nil }