Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions cmd/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,31 @@ func (a *MethodAws) InitIamCommand() {
return
}

excludeDefaultRoles, err := cmd.Flags().GetBool("exclude-default-roles")
if err != nil {
a.OutputSignal.AddError(err)
return
}

// Get Config
config := getIamEnumerateConfig(a.RootFlags.Regions, accountID)
config := getIamEnumerateConfig(accountID, excludeDefaultRoles)

// Get Report
report := iamInternal.EnumerateIam(cmd.Context(), *a.AwsConfig, config)
a.OutputSignal.Content = report
},
}

enumerateCmd.Flags().Bool("exclude-default-roles", false, "Exclude default roles")

iamCmd.AddCommand(enumerateCmd)
a.RootCmd.AddCommand(iamCmd)
}

// getIamEnumerateConfig returns an IamEnumerateConfig with the given regions and account ID
func getIamEnumerateConfig(regions []string, accountID string) iam.IamEnumerateConfig {
func getIamEnumerateConfig(accountID string, excludeDefaultRoles bool) iam.IamEnumerateConfig {
return iam.IamEnumerateConfig{
AccountId: accountID,
AccountId: accountID,
ExcludeDefaultRoles: excludeDefaultRoles,
}
}
1 change: 1 addition & 0 deletions fern/definition/iam/enumerate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ types:
IamEnumerateConfig:
properties:
accountId: string
excludeDefaultRoles: boolean
# Supporting Structs
RoleLastUsed:
properties:
Expand Down
2 changes: 1 addition & 1 deletion fern/fern.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"organization": "method-security",
"version": "3.20.0"
"version": "3.47.1"
}
2 changes: 1 addition & 1 deletion internal/iam/enumerate/enumerate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func EnumerateIam(ctx context.Context, awsConfig aws.Config, config iam.IamEnume

// Enumerate roles (with nested policy information)
log.Info("Enumerating IAM roles with attached policies")
roles, rolesErrors := enumerateIamRoles(ctx, awsConfig)
roles, rolesErrors := enumerateIamRoles(ctx, awsConfig, config.ExcludeDefaultRoles)
if len(rolesErrors) > 0 {
log.Warn("Errors encountered during role enumeration",
svc1log.SafeParam("errorCount", len(rolesErrors)))
Expand Down
66 changes: 65 additions & 1 deletion internal/iam/enumerate/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/url"
"regexp"
"strings"

common "github.com/Method-Security/methodaws/generated/go/common"
iam "github.com/Method-Security/methodaws/generated/go/iam"
Expand All @@ -16,7 +17,7 @@ import (
)

// enumerateIamRoles retrieves all IAM roles with their attached policies
func enumerateIamRoles(ctx context.Context, cfg aws.Config) ([]*iam.IamRoles, []string) {
func enumerateIamRoles(ctx context.Context, cfg aws.Config, excludeDefaultRoles bool) ([]*iam.IamRoles, []string) {
log := svc1log.FromContext(ctx)
client := iamaws.NewFromConfig(cfg)
var iamRoles []*iam.IamRoles
Expand All @@ -31,6 +32,19 @@ func enumerateIamRoles(ctx context.Context, cfg aws.Config) ([]*iam.IamRoles, []

log.Info("Processing IAM roles", svc1log.SafeParam("roleCount", len(roles)))

// Filter out default roles if requested
if excludeDefaultRoles {
filteredRoles := make([]types.Role, 0, len(roles))
for _, role := range roles {
if !isDefaultAwsRole(role) {
filteredRoles = append(filteredRoles, role)
}
}
roles = filteredRoles
log.Info("Filtered default roles",
svc1log.SafeParam("remainingRoleCount", len(roles)))
}

for _, role := range roles {
if role.Arn == nil {
log.Warn("role ARN is nil for role", svc1log.SafeParam("role", role))
Expand Down Expand Up @@ -294,3 +308,53 @@ func discoverEc2ReferencesFromPolicies(policies []*iam.AttachedPolicy) []*common
}
return ec2Instances
}

var serviceLinkedRoleArnRe = regexp.MustCompile(
`^arn:aws[a-z-]*:iam::\d{12}:role/aws-service-role/`,
)

// isServiceLinkedRole returns true if the role is an AWS service-linked role.
func isServiceLinkedRole(role types.Role) bool {
if role.Path != nil && strings.HasPrefix(*role.Path, "/aws-service-role/") {
return true
}

// Fallback if Path is missing
if role.Arn != nil && serviceLinkedRoleArnRe.MatchString(*role.Arn) {
return true
}

return false
}

// isIdentityCenterRole returns true if the role was created by IAM Identity Center (SSO).
func isIdentityCenterRole(role types.Role) bool {
// Identity Center roles always start with AWSReservedSSO_
if role.RoleName != nil && strings.HasPrefix(*role.RoleName, "AWSReservedSSO_") {
return true
}

// Extra safety: path used by SSO-managed roles
if role.Path != nil && strings.Contains(*role.Path, "/aws-reserved/sso.amazonaws.com/") {
return true
}

return false
}

// isControlTowerRole returns true if the role is created/required by AWS Control Tower.
func isControlTowerRole(role types.Role) bool {
return role.RoleName != nil && *role.RoleName == "AWSControlTowerExecution"
}

// isDefaultAwsRole returns true if the role is AWS-shipped (created and managed by AWS).
//
// This includes:
// - Service-linked roles
// - IAM Identity Center (SSO) roles
// - Control Tower roles
func isDefaultAwsRole(role types.Role) bool {
return isServiceLinkedRole(role) ||
isIdentityCenterRole(role) ||
isControlTowerRole(role)
}
Loading