diff --git a/aws/cape-tui.go b/aws/cape-tui.go index 7b87aa8..31b74e0 100644 --- a/aws/cape-tui.go +++ b/aws/cape-tui.go @@ -301,7 +301,7 @@ func preloadData(filePaths []string) (*AllAccountData, error) { for _, filePath := range filePaths { fileRecords, err := loadFileRecords(filePath) - if err != nil { + if err != nil { return nil, err } appData.Files[filePath] = fileRecords diff --git a/aws/cape.go b/aws/cape.go index 5e9e502..f4e07d8 100644 --- a/aws/cape.go +++ b/aws/cape.go @@ -1,7 +1,9 @@ package aws import ( + "bufio" "fmt" + "os" "path/filepath" "strings" @@ -200,7 +202,12 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source) //for the source vertex, we only want to deal with the ones that are NOT in this account if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) { + // skip if the source Name contains AWSSSO- + if strings.Contains(sourceVertexWithProperties.Attributes["Name"], "AWSSSO-") { + continue + } // now let's see if there is a path from this source to our destination + path, _ := graph.ShortestPath(m.GlobalGraph, s, d) // if we have a path, then lets document this source as having a path to our destination if path != nil { @@ -235,7 +242,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s privescPathsBody = append(privescPathsBody, []string{ aws.ToString(m.Caller.Account), s, - magenta(d), + d, magenta(destinationVertexWithProperties.Attributes["IsAdminString"]), paths}) } else { @@ -289,7 +296,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{ TrustedPrincipal: principal, - ExternalID: statement.Condition.StringEquals.StsExternalID, + ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"), VendorName: vendorName, //IsAdmin: false, //CanPrivEscToAdmin: false, @@ -650,6 +657,48 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } + // if the role trusts a principal in another account explicitly, then the principal can assume the role + if thisAccount != trustedPrincipalAccount { + // make a CAN_ASSUME relationship between the trusted principal and this role + + err := GlobalGraph.AddEdge( + TrustedPrincipal.TrustedPrincipal, + a.Arn, + //graph.EdgeAttribute("AssumeRole", "Cross account explicit trust"), + graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit cross account trust) "), + ) + if err != nil { + //fmt.Println(err) + //fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account explicit trust") + if err == graph.ErrEdgeAlreadyExists { + // update the edge by copying the existing graph.Edge with attributes and add the new attributes + //fmt.Println("Edge already exists") + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit cross account trust) " + err = GlobalGraph.UpdateEdge( + TrustedPrincipal.TrustedPrincipal, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } + + } + } + // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role // if the role we are looking at trusts root in it's own account @@ -667,6 +716,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { if PermissionsRowAccount == thisAccount { // lets only look for rows that have sts:AssumeRole permissions if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role @@ -820,10 +870,10 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) } if PermissionsRowAccount == trustedPrincipalAccount { - // lets only look for rows that have sts:AssumeRole permissions - if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + // lets only look for rows that have sts:AssumeRole permis sions + if policy.MatchesAfterExpansion("sts:AssumeRole", PermissionsRow.Action) { // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || - // strings.EqualFold(PermissionsRow.Action, "*") || + // strings.EqualFold(PermissionsRow.Action, "*") { // strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || // strings.EqualFold(PermissionsRow.Action, "sts:*") { // lets only focus on rows that have an effect of Allow @@ -979,3 +1029,21 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } + +// function to read file specified in CapeArnIgnoreList which is separated by newlines, and convert it to a slice of strings with each line as an entry in the slice. +// the function accepts a string with the filename + +func ReadArnIgnoreListFile(filename string) ([]string, error) { + var arnIgnoreList []string + file, err := os.Open(filename) + if err != nil { + return arnIgnoreList, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + arnIgnoreList = append(arnIgnoreList, scanner.Text()) + } + return arnIgnoreList, scanner.Err() +} diff --git a/aws/graph.go b/aws/graph.go index bf5a2b6..0f69552 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -331,7 +331,7 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{ TrustedPrincipal: principal, - ExternalID: statement.Condition.StringEquals.StsExternalID, + ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"), VendorName: vendorName, //IsAdmin: false, //CanPrivEscToAdmin: false, diff --git a/aws/iam-simulator.go b/aws/iam-simulator.go index ce1fc60..decaac8 100644 --- a/aws/iam-simulator.go +++ b/aws/iam-simulator.go @@ -33,8 +33,9 @@ type IamSimulatorModule struct { WrapTable bool // Main module data - SimulatorResults []SimulatorResult - CommandCounter internal.CommandCounter + SimulatorResults []SimulatorResult + CommandCounter internal.CommandCounter + IamSimulatorAdminCheckOnly bool // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry @@ -104,6 +105,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, go m.Receiver(dataReceiver, receiverDone) + if m.IamSimulatorAdminCheckOnly { + // set defaultActionNames to an empty slice + defaultActionNames = []string{} + } + // This double if/else section is here to handle the cases where --principal or --action (or both) are specified. if principal != "" { if action != "" { @@ -354,7 +360,9 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r Decision: "", } } else { - m.getPolicySimulatorResult(principal, actions, resource, dataReceiver) + if !m.IamSimulatorAdminCheckOnly { + m.getPolicySimulatorResult(principal, actions, resource, dataReceiver) + } } } @@ -394,7 +402,9 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r Decision: "", } } else { - m.getPolicySimulatorResult(principal, actions, resource, dataReceiver) + if !m.IamSimulatorAdminCheckOnly { + m.getPolicySimulatorResult(principal, actions, resource, dataReceiver) + } } } @@ -443,7 +453,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool { "iam:PutRolePolicy", "iam:AttachRolePolicy", "secretsmanager:GetSecretValue", - "ssm:GetDocument", + "ssm:GetParameters", } for { SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy( diff --git a/aws/principals.go b/aws/principals.go index a403cf9..e00340a 100644 --- a/aws/principals.go +++ b/aws/principals.go @@ -26,6 +26,12 @@ type IamPrincipalsModule struct { AWSProfile string WrapTable bool + SkipAdminCheck bool + iamSimClient IamSimulatorModule + pmapperMod PmapperModule + pmapperError error + PmapperDataBasePath string + // Main module data Users []User Roles []Role @@ -43,6 +49,8 @@ type User struct { Name string AttachedPolicies []string InlinePolicies []string + Admin string + CanPrivEsc string } type Group struct { @@ -62,6 +70,8 @@ type Role struct { Name string AttachedPolicies []string InlinePolicies []string + Admin string + CanPrivEsc string } func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) { @@ -69,6 +79,7 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "principals" + localAdminMap := make(map[string]bool) m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) @@ -78,6 +89,9 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + // wg := new(sync.WaitGroup) // done := make(chan bool) @@ -101,6 +115,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi "Arn", "AttachedPolicies", "InlinePolicies", + "IsAdminRole?", + "CanPrivEscToAdmin?", } // If the user specified table columns, use those. @@ -122,6 +138,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi "Arn", //"AttachedPolicies", //"InlinePolicies", + "IsAdminRole?", + "CanPrivEscToAdmin?", } // Otherwise, use the default columns. @@ -132,11 +150,25 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi "Arn", // "AttachedPolicies", // "InlinePolicies", + "IsAdminRole?", + "CanPrivEscToAdmin?", } } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } + //Table rows for i := range m.Users { + if m.pmapperError == nil { + m.Users[i].Admin, m.Users[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Users[i].Arn) + } else { + m.Users[i].Admin, m.Users[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Users[i].Arn, m.iamSimClient, localAdminMap) + } + m.output.Body = append( m.output.Body, []string{ @@ -146,12 +178,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi m.Users[i].Arn, strings.Join(m.Users[i].AttachedPolicies, " , "), strings.Join(m.Users[i].InlinePolicies, " , "), + m.Users[i].Admin, + m.Users[i].CanPrivEsc, }, ) } for i := range m.Roles { + if m.pmapperError == nil { + m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Roles[i].Arn) + } else { + m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Roles[i].Arn, m.iamSimClient, localAdminMap) + } m.output.Body = append( m.output.Body, []string{ @@ -161,6 +200,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi m.Roles[i].Arn, strings.Join(m.Roles[i].AttachedPolicies, " , "), strings.Join(m.Roles[i].InlinePolicies, " , "), + m.Roles[i].Admin, + m.Roles[i].CanPrivEsc, }, ) diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 85b351d..48b773d 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -219,9 +219,10 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin RoleARN: aws.ToString(role.roleARN), RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), TrustedPrincipal: principal, - ExternalID: statement.Condition.StringEquals.StsExternalID, - IsAdmin: role.Admin, - CanPrivEsc: role.CanPrivEsc, + // if there is more than one externalID concat them using newlines + ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"), + IsAdmin: role.Admin, + CanPrivEsc: role.CanPrivEsc, } body = append(body, []string{ aws.ToString(m.Caller.Account), @@ -281,7 +282,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) for _, role := range m.AnalyzedRoles { for _, statement := range role.trustsDoc.Statement { for _, principal := range statement.Principal.AWS { - if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" { + if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == nil { accountID := strings.Split(principal, ":")[4] vendorName := m.vendors.GetVendorNameFromAccountID(accountID) if vendorName != "" { @@ -292,7 +293,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) RoleARN: aws.ToString(role.roleARN), RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), TrustedPrincipal: principal, - ExternalID: statement.Condition.StringEquals.StsExternalID, + ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"), IsAdmin: role.Admin, CanPrivEsc: role.CanPrivEsc, } diff --git a/cli/aws.go b/cli/aws.go index f75990b..d405252 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -138,9 +138,10 @@ var ( PostRun: awsPostRun, } - CapeAdminOnly bool - CapeJobName string - CapeCommand = &cobra.Command{ + CapeAdminOnly bool + CapeArnIgnoreList string + CapeJobName string + CapeCommand = &cobra.Command{ Use: "cape", Aliases: []string{"CAPE"}, Short: "Cross-Account Privilege Escalation Route finder. Needs to be run with multiple profiles using -l or -a flag. Needs pmapper data to be present", @@ -263,10 +264,11 @@ var ( PostRun: awsPostRun, } - SimulatorResource string - SimulatorAction string - SimulatorPrincipal string - IamSimulatorCommand = &cobra.Command{ + SimulatorResource string + SimulatorAction string + SimulatorPrincipal string + IamSimulatorAdminCheckOnly bool + IamSimulatorCommand = &cobra.Command{ Use: "iam-simulator", Aliases: []string{"iamsimulator", "simulator"}, Short: "Wrapper around the AWS IAM Simulate Principal Policy command", @@ -1172,6 +1174,20 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } } + // if the CapeArnIgnoreList arg is not empty, read the file and add the arns to the CapeArnIgnoreList + if CapeArnIgnoreList != "" { + // call ReadArnIgnoreListFile and add the arns to the CapeArnIgnoreList + arnsToIgnore, err := aws.ReadArnIgnoreListFile(CapeArnIgnoreList) + if err != nil { + fmt.Println("Error reading the arn ignore list file: " + err.Error()) + } + + // remove nodes that are in the CapeArnIgnoreList from the graph + for _, arn := range arnsToIgnore { + GlobalGraph.RemoveVertex(arn) + } + } + // make pmapper edges //you can update edges, so we can just merge attributes as needed // first we add the edges that already exist in pmapper, then later we will make more edges based on the cloudfox role trusts logic @@ -1318,10 +1334,20 @@ func runCapeTUICommand(cmd *cobra.Command, args []string) { if _, err := os.Stat(filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)); os.IsNotExist(err) { fmt.Printf("[%s] Could not retrieve CAPE data for profile %s.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), profile) //remove the profile from the list of profiles to analyze - AWSProfiles = append(AWSProfiles[:i], AWSProfiles[i+1:]...) + if len(AWSProfiles) > 1 { + AWSProfiles = append(AWSProfiles[:i], AWSProfiles[i+1:]...) + } else { + if CapeAdminOnly { + fmt.Printf("[%s] Could not retrieve cape data. Did you run cape without the --admin-only flag? You'll need to run cape with --admin-only to use the tui with --admin-only\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version))) + } else { + fmt.Printf("[%s] Did you run cape with the --admin-only flag? You'll need to run cape without --admin-only to use the tui without --admin-only\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version))) + } + os.Exit(1) + } } } + if len(capeOutputFileLocations) == 0 { fmt.Printf("[%s] Could not retrieve CAPE data.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version))) os.Exit(1) @@ -1338,13 +1364,14 @@ func runIamSimulatorCommand(cmd *cobra.Command, args []string) { continue } m := aws.IamSimulatorModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfileProvided: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfileProvided: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + IamSimulatorAdminCheckOnly: IamSimulatorAdminCheckOnly, } m.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity) } @@ -1547,13 +1574,15 @@ func runPrincipalsCommand(cmd *cobra.Command, args []string) { continue } m := aws.IamPrincipalsModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintIamPrincipals(AWSOutputDirectory, Verbosity) } @@ -2375,9 +2404,6 @@ func init() { // Map Access Keys Module Flags AccessKeysCommand.Flags().StringVarP(&AccessKeysFilter, "filter", "f", "none", "Access key ID to search for") - // IAM Simulator Module Flags - //IamSimulatorCommand.Flags().StringVarP(&IamSimulatorFilter, "filter", "f", "none", "Access key ID to search for") - // Instances Map Module Flags InstancesCommand.Flags().StringVarP(&InstancesFilter, "filter", "f", "all", "[InstanceID | InstanceIDsFile]") InstancesCommand.Flags().BoolVarP(&InstanceMapUserDataAttributesOnly, "userdata", "u", false, "Use this flag to retrieve only the userData attribute from EC2 instances.") @@ -2395,6 +2421,7 @@ func init() { IamSimulatorCommand.Flags().StringVar(&SimulatorPrincipal, "principal", "", "Principal Arn") IamSimulatorCommand.Flags().StringVar(&SimulatorAction, "action", "", "Action") IamSimulatorCommand.Flags().StringVar(&SimulatorResource, "resource", "*", "Resource") + IamSimulatorCommand.Flags().BoolVar(&IamSimulatorAdminCheckOnly, "admin-check-only", false, "Only check check if principals are admin") // iam-simulator module flags PermissionsCommand.Flags().StringVar(&PermissionsPrincipal, "principal", "", "Principal Arn") @@ -2408,6 +2435,8 @@ func init() { // cape command flags CapeCommand.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") //CapeCommand.Flags().StringVar(&CapeJobName, "job-name", "", "Name of the cape job") + // flag that accepts a list of arns to ignore + CapeCommand.Flags().StringVar(&CapeArnIgnoreList, "arn-ignore-list", "", "File containing a list of ARNs to ignore separated by newlines") // cape tui command flags CapeTuiCmd.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") diff --git a/globals/utils.go b/globals/utils.go index 61b2997..e9e7d01 100644 --- a/globals/utils.go +++ b/globals/utils.go @@ -4,5 +4,4 @@ const CLOUDFOX_USER_AGENT = "cloudfox" const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox" const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output" const LOOT_DIRECTORY_NAME = "loot" -const CLOUDFOX_VERSION = "1.14.2" - +const CLOUDFOX_VERSION = "1.15.0" diff --git a/internal/aws/policy/policy.go b/internal/aws/policy/policy.go index 9a629c1..24f20da 100644 --- a/internal/aws/policy/policy.go +++ b/internal/aws/policy/policy.go @@ -109,6 +109,9 @@ func MatchesAfterExpansion(stringFromPolicyToCheck, stringToCheckAgainst string) return pattern.MatchString(stringFromPolicyToCheck) } + + + func (p *Policy) DoesPolicyHaveMatchingStatement(effect string, actionToCheck string, resourceToCheck string) bool { for _, statement := range p.Statement { diff --git a/internal/aws/policy/policy_test.go b/internal/aws/policy/policy_test.go index 3870a46..7ac1218 100644 --- a/internal/aws/policy/policy_test.go +++ b/internal/aws/policy/policy_test.go @@ -222,3 +222,51 @@ func TestDoesPolicyHaveMatchingStatement(t *testing.T) { } } } + +func TestDoesPermissionExpansionMatch(t *testing.T) { + + tests := []struct { + actionToExpand string + actionToCheckAgainst string + want bool + }{ + { + actionToExpand: "sts:AssumeRole", + actionToCheckAgainst: "sts:AssumeRole", + want: true, + }, + { + actionToExpand: "*", + actionToCheckAgainst: "sts:AssumeRole", + want: true, + }, + { + actionToExpand: "sts:Assume*", + actionToCheckAgainst: "sts:AssumeRole", + want: true, + }, + { + actionToExpand: "sts:*", + actionToCheckAgainst: "sts:AssumeRole", + want: true, + }, + { + actionToExpand: "s3:GetObject", + actionToCheckAgainst: "sts:AssumeRole", + want: false, + }, + { + actionToExpand: "s3:*", + actionToCheckAgainst: "sts:AssumeRole", + want: false, + }, + } + + for _, tt := range tests { + actual := MatchesAfterExpansion(tt.actionToCheckAgainst, tt.actionToExpand) + if tt.want != actual { + //fmt.Printf("DoesPermissionHaveMatchingStatement(%s, %s) is %v but should be %v", tt.actionToExpand, tt.actionToCheckAgainst, actual, tt.want) + t.Errorf("DoesPermissionHaveMatchingStatement(%s, %s) is %v but should be %v", tt.actionToExpand, tt.actionToCheckAgainst, actual, tt.want) + } + } +} diff --git a/internal/aws/policy/role-trust-policies.go b/internal/aws/policy/role-trust-policies.go index 40ddcdf..5773b5c 100644 --- a/internal/aws/policy/role-trust-policies.go +++ b/internal/aws/policy/role-trust-policies.go @@ -26,7 +26,7 @@ type RoleTrustStatementEntry struct { Action string `json:"Action"` Condition struct { StringEquals struct { - StsExternalID string `json:"sts:ExternalId"` + StsExternalID ListOfPrincipals `json:"sts:ExternalId"` SAMLAud string `json:"SAML:aud"` TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"` TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`