11package aws
22
33import (
4- "encoding/json"
5- "errors"
64 "fmt"
7- "net/url"
85 "path/filepath"
9- "regexp"
106 "sort"
117 "strconv"
128 "strings"
139
1410 "github.com/BishopFox/cloudfox/aws/sdk"
1511 "github.com/BishopFox/cloudfox/internal"
12+ "github.com/BishopFox/cloudfox/internal/aws/policy"
1613 "github.com/aws/aws-sdk-go-v2/aws"
1714 "github.com/aws/aws-sdk-go-v2/service/iam"
18- "github.com/aws/aws-sdk-go-v2/service/iam/types"
1915 "github.com/aws/aws-sdk-go-v2/service/sts"
2016 "github.com/bishopfox/knownawsaccountslookup"
2117 "github.com/sirupsen/logrus"
@@ -65,46 +61,12 @@ type RoleTrustRow struct {
6561
6662type AnalyzedRole struct {
6763 roleARN * string
68- trustsDoc trustPolicyDocument
64+ trustsDoc policy. TrustPolicyDocument
6965 // trustType string // UNUSED FIELD, PLEASE REVIEW
7066 Admin string
7167 CanPrivEsc string
7268}
7369
74- type trustPolicyDocument struct {
75- Version string `json:"Version"`
76- Statement []RoleTrustStatementEntry `json:"Statement"`
77- }
78-
79- type RoleTrustStatementEntry struct {
80- Sid string `json:"Sid"`
81- Effect string `json:"Effect"`
82- Principal struct {
83- AWS ListOfPrincipals `json:"AWS"`
84- Service ListOfPrincipals `json:"Service"`
85- Federated ListOfPrincipals `json:"Federated"`
86- } `json:"Principal"`
87- Action string `json:"Action"`
88- Condition struct {
89- StringEquals struct {
90- StsExternalID string `json:"sts:ExternalId"`
91- SAMLAud string `json:"SAML:aud"`
92- OidcEksSub string `json:"OidcEksSub"`
93- OidcEksAud string `json:"OidcEksAud"`
94- CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
95- } `json:"StringEquals"`
96- StringLike struct {
97- TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
98- TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
99- OidcEksSub string `json:"OidcEksSub"`
100- OidcEksAud string `json:"OidcEksAud"`
101- } `json:"StringLike"`
102- ForAnyValueStringLike struct {
103- CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
104- } `json:"ForAnyValue:StringLike"`
105- } `json:"Condition"`
106- }
107-
10870func (m * RoleTrustsModule ) PrintRoleTrusts (outputDirectory string , verbosity int ) {
10971 m .output .Verbosity = verbosity
11072 m .output .Directory = outputDirectory
@@ -140,6 +102,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
140102
141103 }
142104 }
105+
143106 o := internal.OutputClient {
144107 Verbosity : verbosity ,
145108 CallingModule : m .output .CallingModule ,
@@ -189,12 +152,12 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
189152 fmt .Printf ("[%s][%s] No role trusts found, skipping the creation of an output file.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ))
190153 }
191154 if len (servicesBody ) > 0 {
192- fmt .Printf ("[%s][%s] %s principal role trusts found.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ), strconv .Itoa (len (servicesBody )))
155+ fmt .Printf ("[%s][%s] %s service role trusts found.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ), strconv .Itoa (len (servicesBody )))
193156 } else {
194157 fmt .Printf ("[%s][%s] No role trusts found, skipping the creation of an output file.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ))
195158 }
196159 if len (federatedBody ) > 0 {
197- fmt .Printf ("[%s][%s] %s principal role trusts found.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ), strconv .Itoa (len (federatedBody )))
160+ fmt .Printf ("[%s][%s] %s federated role trusts found.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ), strconv .Itoa (len (federatedBody )))
198161 } else {
199162 fmt .Printf ("[%s][%s] No role trusts found, skipping the creation of an output file.\n " , cyan (m .output .CallingModule ), cyan (m .AWSProfile ))
200163 }
@@ -373,7 +336,7 @@ func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string,
373336 tableCols = strings .Split (m .AWSTableCols , "," )
374337 // If the user specified wide as the output format, use these columns.
375338 } else if m .AWSOutputType == "wide" {
376- tableCols = []string {"Role Arn" , "Trusted Service" , "IsAdmin?" , "CanPrivEscToAdmin?" }
339+ tableCols = []string {"Account" , " Role Arn" , "Trusted Service" , "IsAdmin?" , "CanPrivEscToAdmin?" }
377340 // Otherwise, use the default columns for this module (brief)
378341 } else {
379342 tableCols = []string {"Role Name" , "Trusted Service" , "IsAdmin?" , "CanPrivEscToAdmin?" }
@@ -441,7 +404,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
441404 tableCols = strings .Split (m .AWSTableCols , "," )
442405 // If the user specified wide as the output format, use these columns.
443406 } else if m .AWSOutputType == "wide" {
444- tableCols = []string {"Role Arn" , "Trusted Provider" , "Trusted Subject" , "IsAdmin?" , "CanPrivEscToAdmin?" }
407+ tableCols = []string {"Account" , " Role Arn" , "Trusted Provider" , "Trusted Subject" , "IsAdmin?" , "CanPrivEscToAdmin?" }
445408 // Otherwise, use the default columns for this module (brief)
446409 } else {
447410 tableCols = []string {"Role Name" , "Trusted Provider" , "Trusted Subject" , "IsAdmin?" , "CanPrivEscToAdmin?" }
@@ -454,23 +417,25 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
454417 for _ , role := range m .AnalyzedRoles {
455418 for _ , statement := range role .trustsDoc .Statement {
456419 if len (statement .Principal .Federated ) > 0 {
457- provider , subject := parseFederatedTrustPolicy (statement )
458- RoleTrustRow := RoleTrustRow {
459- RoleARN : aws .ToString (role .roleARN ),
460- RoleName : GetResourceNameFromArn (aws .ToString (role .roleARN )),
461- TrustedFederatedProvider : provider ,
462- TrustedFederatedSubject : subject ,
463- IsAdmin : role .Admin ,
464- CanPrivEsc : role .CanPrivEsc ,
420+ provider , subjects := parseFederatedTrustPolicy (statement )
421+ for _ , subject := range subjects {
422+ RoleTrustRow := RoleTrustRow {
423+ RoleARN : aws .ToString (role .roleARN ),
424+ RoleName : GetResourceNameFromArn (aws .ToString (role .roleARN )),
425+ TrustedFederatedProvider : provider ,
426+ TrustedFederatedSubject : subject ,
427+ IsAdmin : role .Admin ,
428+ CanPrivEsc : role .CanPrivEsc ,
429+ }
430+ body = append (body , []string {
431+ aws .ToString (m .Caller .Account ),
432+ RoleTrustRow .RoleARN ,
433+ RoleTrustRow .RoleName ,
434+ RoleTrustRow .TrustedFederatedProvider ,
435+ RoleTrustRow .TrustedFederatedSubject ,
436+ RoleTrustRow .IsAdmin ,
437+ RoleTrustRow .CanPrivEsc })
465438 }
466- body = append (body , []string {
467- aws .ToString (m .Caller .Account ),
468- RoleTrustRow .RoleARN ,
469- RoleTrustRow .RoleName ,
470- RoleTrustRow .TrustedFederatedProvider ,
471- RoleTrustRow .TrustedFederatedSubject ,
472- RoleTrustRow .IsAdmin ,
473- RoleTrustRow .CanPrivEsc })
474439 }
475440
476441 }
@@ -481,47 +446,120 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
481446
482447}
483448
484- func parseFederatedTrustPolicy (statement RoleTrustStatementEntry ) (string , string ) {
485- var column2 , column3 string
486- if statement .Condition .StringLike .TokenActionsGithubusercontentComAud != "" || len (statement .Condition .StringLike .TokenActionsGithubusercontentComSub ) > 0 {
487- column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
488- trustedRepos := strings .Join (statement .Condition .StringLike .TokenActionsGithubusercontentComSub , "\n " )
489- if trustedRepos == "" {
490- column3 = "ALL REPOS!!!"
449+ func parseFederatedTrustPolicy (statement policy.RoleTrustStatementEntry ) (string , []string ) {
450+ var provider string
451+ var subjects []string
452+ if len (statement .Principal .Federated ) > 1 {
453+ sharedLogger .Warnf ("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics." )
454+ provider = "Multiple Federated Providers"
455+ subjects = append (subjects , "Review policy for specifics\n and submit issue to cloudfox repo." )
456+ }
457+
458+ switch {
459+ // lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
460+ case strings .Contains (statement .Principal .Federated [0 ], "token.actions.githubusercontent.com" ):
461+ provider = "GitHub"
462+ if len (statement .Condition .StringLike .TokenActionsGithubusercontentComSub ) > 0 {
463+ subjects = append (subjects , statement .Condition .StringLike .TokenActionsGithubusercontentComSub ... )
464+ } else if len (statement .Condition .StringEquals .TokenActionsGithubusercontentComSub ) > 0 {
465+ subjects = append (subjects , statement .Condition .StringEquals .TokenActionsGithubusercontentComSub ... )
491466 } else {
492- column3 = trustedRepos
467+ subjects = append ( subjects , "ALL REPOS!!!" )
493468 }
494- } else if statement .Condition .StringEquals .SAMLAud == "https://signin.aws.amazon.com/saml" {
495- if strings .Contains (statement .Principal .Federated [0 ], "AWSSSO" ) {
496- column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
497- } else if strings .Contains (statement .Principal .Federated [0 ], "Okta" ) {
498- column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
469+ case strings .Contains (statement .Principal .Federated [0 ], "oidc.eks" ):
470+ provider = "EKS"
471+ if len (statement .Condition .StringLike .OidcEksSub ) > 0 {
472+ subjects = append (subjects , statement .Condition .StringLike .OidcEksSub ... )
473+ } else if len (statement .Condition .StringEquals .OidcEksSub ) > 0 {
474+ subjects = append (subjects , statement .Condition .StringEquals .OidcEksSub ... )
475+ } else {
476+ subjects = append (subjects , "ALL SERVICE ACCOUNTS!!!" )
477+ }
478+ // terraform case
479+ case strings .Contains (statement .Principal .Federated [0 ], "app.terraform.io" ):
480+ provider = "Terraform Cloud"
481+ if len (statement .Condition .StringLike .TerraformSub ) > 0 {
482+ subjects = append (subjects , statement .Condition .StringLike .TerraformSub ... )
483+ } else if len (statement .Condition .StringEquals .TerraformSub ) > 0 {
484+ subjects = append (subjects , statement .Condition .StringEquals .TerraformSub ... )
485+ } else {
486+ subjects = append (subjects , "ALL WORKSPACES" )
499487 }
500- column3 = "Not applicable"
501- } else if statement . Condition . StringEquals . OidcEksAud != "" || statement .Condition . StringEquals . OidcEksSub != "" || statement . Condition . StringLike . OidcEksAud != "" || statement . Condition . StringLike . OidcEksSub != "" {
502- column2 = "EKS" // (" + statement.Principal.Federated[0] + ") "
503- if statement .Condition .StringEquals . OidcEksSub != "" {
504- column3 = statement .Condition .StringEquals . OidcEksSub
505- } else if statement .Condition .StringLike . OidcEksSub != "" {
506- column3 = statement .Condition .StringLike . OidcEksSub
488+ // Azure AD case
489+ case strings . Contains ( statement .Principal . Federated [ 0 ], "http://sts.windows.net" ):
490+ provider = "Azure AD "
491+ if len ( statement .Condition .StringLike . AzureADIss ) > 0 {
492+ subjects = append ( subjects , statement .Condition .StringLike . AzureADIss ... )
493+ } else if len ( statement .Condition .StringEquals . AzureADIss ) > 0 {
494+ subjects = append ( subjects , statement .Condition .StringEquals . AzureADIss ... )
507495 } else {
508- column3 = "ALL SERVICE ACCOUNTS!"
496+ subjects = append ( subjects , "ALL ISSUERS" )
509497 }
510- } else if statement .Principal .Federated [0 ] == "cognito-identity.amazonaws.com" {
511- column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
498+
499+ // AWS SSO case
500+ case strings .Contains (statement .Principal .Federated [0 ], "AWSSSO" ):
501+ provider = "AWS SSO"
502+ subjects = append (subjects , "Not applicable" )
503+
504+ // okta case
505+ case strings .Contains (statement .Principal .Federated [0 ], "Okta" ):
506+ provider = "Okta"
507+ subjects = append (subjects , "Not applicable" )
508+
509+ // cognito case
510+ case statement .Principal .Federated [0 ] == "cognito-identity.amazonaws.com" :
511+ provider = "Cognito"
512512 if statement .Condition .ForAnyValueStringLike .CognitoAMR != "" {
513- column3 = statement .Condition .ForAnyValueStringLike .CognitoAMR
513+ subjects = append (subjects , statement .Condition .ForAnyValueStringLike .CognitoAMR )
514+ } else {
515+ subjects = append (subjects , "ALL IDENTITIES" )
514516 }
515- } else {
516- if column2 == "" && strings .Contains (statement .Principal .Federated [0 ], "oidc.eks" ) {
517- column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
518- column3 = "ALL SERVICE ACCOUNTS!"
519- } else if column2 == "" && strings .Contains (statement .Principal .Federated [0 ], "AWSSSO" ) {
520- column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
517+ // google workspace case
518+ case strings .Contains (statement .Principal .Federated [0 ], "workspace.google.com" ):
519+ provider = "Google Workspace"
520+ if len (statement .Condition .StringLike .GoogleWorkspaceSub ) > 0 {
521+ subjects = append (subjects , statement .Condition .StringLike .GoogleWorkspaceSub ... )
522+ } else if len (statement .Condition .StringEquals .GoogleWorkspaceSub ) > 0 {
523+ subjects = append (subjects , statement .Condition .StringEquals .GoogleWorkspaceSub ... )
524+ } else {
525+ subjects = append (subjects , "ALL USERS" )
526+ }
527+ // GCP case
528+ case strings .Contains (statement .Principal .Federated [0 ], "accounts.google.com" ):
529+ provider = "GCP"
530+ if len (statement .Condition .StringLike .GCPSub ) > 0 {
531+ subjects = append (subjects , statement .Condition .StringLike .GCPSub ... )
532+ } else if len (statement .Condition .StringEquals .GCPSub ) > 0 {
533+ subjects = append (subjects , statement .Condition .StringEquals .GCPSub ... )
534+ } else {
535+ subjects = append (subjects , "ALL USERS" )
521536 }
537+ // auth0 case
538+ //not ready yet
539+ // case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
540+ // provider = "Auth0"
541+ // if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
542+ // subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
543+ // } else {
544+ // subjects = append(subjects, "ALL GROUPS")
545+ // }
546+ // circleci case
547+ case strings .Contains (statement .Principal .Federated [0 ], "oidc.circleci.com" ):
548+ provider = "CircleCI"
549+ if len (statement .Condition .StringLike .CircleCIAud ) > 0 {
550+ subjects = append (subjects , statement .Condition .StringLike .CircleCIAud ... )
551+ } else if len (statement .Condition .StringEquals .CircleCIAud ) > 0 {
552+ subjects = append (subjects , statement .Condition .StringEquals .CircleCIAud ... )
553+ } else {
554+ subjects = append (subjects , "ALL PROJECTS" )
555+ }
556+
557+ default :
558+ provider = "Unknown Federated Provider"
559+ subjects = append (subjects , "Review policy for specifics\n and submit issue to cloudfox repo." )
522560
523561 }
524- return column2 , column3
562+ return provider , subjects
525563}
526564
527565func (m * RoleTrustsModule ) sortTrustsTablePerTrustedPrincipal () {
@@ -541,7 +579,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
541579 }
542580
543581 for _ , role := range ListRoles {
544- trustsdoc , err := parseRoleTrustPolicyDocument (role )
582+ trustsdoc , err := policy . ParseRoleTrustPolicyDocument (role )
545583 if err != nil {
546584 m .modLog .Error (err .Error ())
547585 m .CommandCounter .Error ++
@@ -561,40 +599,3 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
561599 }
562600
563601}
564-
565- func parseRoleTrustPolicyDocument (role types.Role ) (trustPolicyDocument , error ) {
566- document , _ := url .QueryUnescape (aws .ToString (role .AssumeRolePolicyDocument ))
567-
568- // These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
569- // and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
570- // the random pattern with a predictable one so that we can add the predictable one in the struct
571- // used to unmarshall.
572- pattern := `(\w+)\:`
573- pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
574- var reEKSSub = regexp .MustCompile (pattern2 + pattern + "sub" )
575- var reEKSAud = regexp .MustCompile (pattern2 + pattern + "aud" )
576- document = reEKSSub .ReplaceAllString (document , "\" OidcEksSub" )
577- document = reEKSAud .ReplaceAllString (document , "\" OidcEksAud" )
578-
579- var parsedDocumentToJSON trustPolicyDocument
580- _ = json .Unmarshal ([]byte (document ), & parsedDocumentToJSON )
581- return parsedDocumentToJSON , nil
582- }
583-
584- // A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
585- // https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
586- type ListOfPrincipals []string
587-
588- func (r * ListOfPrincipals ) UnmarshalJSON (b []byte ) error {
589- var s string
590- if err := json .Unmarshal (b , & s ); err == nil {
591- * r = append (* r , s )
592- return nil
593- }
594- var ss []string
595- if err := json .Unmarshal (b , & ss ); err == nil {
596- * r = ss
597- return nil
598- }
599- return errors .New ("cannot unmarshal neither to a string nor a slice of strings" )
600- }
0 commit comments