From 43365133936630f0417713a4f3f01fff853288ee Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Tue, 5 May 2020 16:25:36 +1000 Subject: [PATCH 1/2] Implement slow but accurate cloudformation filtering --- README.md | 6 ++ go.mod | 8 +-- go.sum | 28 +-------- iamy.go | 6 +- iamy/aws.go | 33 +++++++--- iamy/aws_test.go | 6 +- iamy/cfn.go | 160 +++++++++++++++++++++++++++++++++++++++++++++++ iamy/cfn_test.go | 34 ++++++++++ pull.go | 7 ++- push.go | 4 +- 10 files changed, 241 insertions(+), 51 deletions(-) create mode 100644 iamy/cfn.go create mode 100644 iamy/cfn_test.go diff --git a/README.md b/README.md index 9e2316d..f19984e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ Exec all aws commands? (y/N) y > aws iam attach-user-policy --user-name billy.blogs --policy-arn arn:aws:iam::aws:policy/ReadOnly ``` +## Accurate cloudformation matching + +By default, iamy will use a simple heuristic (does it end with an ID, eg -ABCDEF1234) to determine if a given resource is managed by cloudformation. + +This behaviour is good enough for some cases, but if you want slower but more accurate matching pass `--accurate-cfn` +to enumerate all cloudformation stacks and resources to determine exactly which resources are managed. ## Inspiration and similar tools - https://github.com/percolate/iamer diff --git a/go.mod b/go.mod index e0c1042..3a6943e 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module github.com/99designs/iamy +go 1.14 + require ( - github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect - github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect - github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/aws/aws-sdk-go v1.16.23 @@ -11,12 +10,9 @@ require ( github.com/fatih/color v1.7.0 github.com/ghodss/yaml v1.0.0 github.com/kr/pretty v0.1.0 // indirect - github.com/kr/pty v1.1.3 // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/pkg/errors v0.8.1 - github.com/sergi/go-diff v1.0.0 // indirect - github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.3.0 // indirect golang.org/x/net v0.0.0-20190119204137-ed066c81e75e // indirect golang.org/x/sys v0.0.0-20190122071731-054c452bb702 // indirect diff --git a/go.sum b/go.sum index c457fe0..7ddc201 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,13 @@ -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/aws/aws-sdk-go v0.0.0-20190104231327-923b7b1b0525 h1:qc2jx43HwaJSlY5UO/2cphRqZJElpLJlvJfJ4DkAqK0= -github.com/aws/aws-sdk-go v0.0.0-20190104231327-923b7b1b0525/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.16.23 h1:MwBOBeez0XEFVh6DCc888X+nHVBCjUDLnnWXSGGWUgM= github.com/aws/aws-sdk-go v1.16.23/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v0.0.0-20170523202404-62e9147c64a1 h1:UisrPJO/eDu/ceQR8WPTVgTAyL8XnaKKWIEy7GTZhS0= -github.com/fatih/color v0.0.0-20170523202404-62e9147c64a1/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -26,44 +17,29 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.0.8 h1:KatiXbcoFpoKmM5pL0yhug+tx/POfZO+0aVsuGhUhgo= -github.com/mattn/go-colorable v0.0.8/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.2 h1:F+DnWktyadxnOrohKLNUC9/GjFii5RJgY4GFG6ilggw= -github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 h1:chPfVn+gpAM5CTpTyVU9j8J+xgRGwmoDlNDLjKnJiYo= -github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q= golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sys v0.0.0-20170615053224-fb4cac33e319 h1:VOzr22cZ/j39L4pIzz1fEBJewkELEkgJuuLZHCdzFRg= -golang.org/x/sys v0.0.0-20170615053224-fb4cac33e319/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190122071731-054c452bb702 h1:Lk4tbZFnlyPgV+sLgTw5yGfzrlOn9kx4vSombi2FFlY= golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/alecthomas/kingpin.v2 v2.2.4 h1:CC8tJ/xljioKrK6ii3IeWVXU4Tw7VB+LbjZBJaBxN50= -gopkg.in/alecthomas/kingpin.v2 v2.2.4/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.0.0-20170407172122-cd8b52f8269e h1:o/mfNjxpTLivuKEfxzzwrJ8PmulH2wEp7t713uMwKAA= -gopkg.in/yaml.v2 v2.0.0-20170407172122-cd8b52f8269e/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/iamy.go b/iamy.go index 6a3b970..0814e13 100644 --- a/iamy.go +++ b/iamy.go @@ -34,6 +34,7 @@ func main() { pull = kingpin.Command("pull", "Syncs IAM users, groups and policies from the active AWS account to files") pullDir = pull.Flag("dir", "The directory to dump yaml files to").Default(defaultDir).Short('d').String() canDelete = pull.Flag("delete", "Delete extraneous files from destination dir").Bool() + lookupCfn = pull.Flag("accurate-cfn", "Fetch all known resource names from cloudformation to get exact filtering").Bool() push = kingpin.Command("push", "Syncs IAM users, groups and policies from files to the active AWS account") pushDir = push.Flag("dir", "The directory to load yaml files from").Default(defaultDir).Short('d').ExistingDir() ) @@ -68,8 +69,9 @@ func main() { case pull.FullCommand(): PullCommand(ui, PullCommandInput{ - Dir: *pullDir, - CanDelete: *canDelete, + Dir: *pullDir, + CanDelete: *canDelete, + HeuristicCfnMatching: !*lookupCfn, }) } } diff --git a/iamy/aws.go b/iamy/aws.go index 9b8ea04..3e0c6a9 100644 --- a/iamy/aws.go +++ b/iamy/aws.go @@ -3,7 +3,6 @@ package iamy import ( "fmt" "log" - "regexp" "strings" "sync" @@ -12,18 +11,18 @@ import ( "github.com/pkg/errors" ) -var cfnResourceRegexp = regexp.MustCompile(`-[A-Z0-9]{10,20}$`) - // AwsFetcher fetches account data from AWS type AwsFetcher struct { // As Policy and Role descriptions are immutable, we can skip fetching them // when pushing to AWS SkipFetchingPolicyAndRoleDescriptions bool + HeuristicCfnMatching bool Debug *log.Logger iam *iamClient s3 *s3Client + cfn *cfnClient account *Account data AccountData @@ -37,6 +36,8 @@ func (a *AwsFetcher) init() error { s := awsSession() a.iam = newIamClient(s) a.s3 = newS3Client(s) + a.cfn = newCfnClient(s) + if a.account, err = a.getAccount(); err != nil { return err } @@ -53,6 +54,13 @@ func (a *AwsFetcher) Fetch() (*AccountData, error) { return nil, errors.Wrap(err, "Error in init") } + if !a.HeuristicCfnMatching { + log.Println("Fetching CFN data") + if err := a.cfn.PopulateMangedResourceData(); err != nil { + return nil, errors.Wrap(err, "Error fetching CFN data") + } + } + var wg sync.WaitGroup var iamErr, s3Err error @@ -91,6 +99,10 @@ func (a *AwsFetcher) fetchS3Data() error { if b.policyJson == "" { continue } + if ok, err := a.isSkippableManagedResource(CfnS3Bucket, b.name); ok { + log.Printf(err) + continue + } policyDoc, err := NewPolicyDocumentFromJson(b.policyJson) if err != nil { @@ -107,6 +119,7 @@ func (a *AwsFetcher) fetchS3Data() error { return nil } + func (a *AwsFetcher) fetchIamData() error { var populateIamDataErr error var populateInstanceProfileErr error @@ -196,7 +209,7 @@ func (a *AwsFetcher) marshalRoleDescriptionAsync(roleName string, target *string func (a *AwsFetcher) populateInstanceProfileData(resp *iam.ListInstanceProfilesOutput) error { for _, profileResp := range resp.InstanceProfiles { - if ok, err := isSkippableManagedResource(*profileResp.InstanceProfileName); ok { + if ok, err := a.isSkippableManagedResource(CfnInstanceProfile, *profileResp.InstanceProfileName); ok { log.Printf(err) continue } @@ -216,7 +229,7 @@ func (a *AwsFetcher) populateInstanceProfileData(resp *iam.ListInstanceProfilesO func (a *AwsFetcher) populateIamData(resp *iam.GetAccountAuthorizationDetailsOutput) error { for _, userResp := range resp.UserDetailList { - if ok, err := isSkippableManagedResource(*userResp.UserName); ok { + if ok, err := a.isSkippableManagedResource(CfnIamUser, *userResp.UserName); ok { log.Printf(err) continue } @@ -246,7 +259,7 @@ func (a *AwsFetcher) populateIamData(resp *iam.GetAccountAuthorizationDetailsOut } for _, groupResp := range resp.GroupDetailList { - if ok, err := isSkippableManagedResource(*groupResp.GroupName); ok { + if ok, err := a.isSkippableManagedResource(CfnIamGroup, *groupResp.GroupName); ok { log.Printf(err) continue } @@ -267,7 +280,7 @@ func (a *AwsFetcher) populateIamData(resp *iam.GetAccountAuthorizationDetailsOut } for _, roleResp := range resp.RoleDetailList { - if ok, err := isSkippableManagedResource(*roleResp.RoleName); ok { + if ok, err := a.isSkippableManagedResource(CfnIamRole, *roleResp.RoleName); ok { log.Printf(err) continue } @@ -297,7 +310,7 @@ func (a *AwsFetcher) populateIamData(resp *iam.GetAccountAuthorizationDetailsOut } for _, policyResp := range resp.Policies { - if ok, err := isSkippableManagedResource(*policyResp.PolicyName); ok { + if ok, err := a.isSkippableManagedResource(CfnIamPolicy, *policyResp.PolicyName); ok { log.Printf(err) continue } @@ -390,8 +403,8 @@ func (a *AwsFetcher) getAccount() (*Account, error) { // // Returns a boolean of whether it can be skipped and a string of the // reasoning why it was skipped. -func isSkippableManagedResource(resourceIdentifier string) (bool, string) { - if cfnResourceRegexp.MatchString(resourceIdentifier) { +func (a *AwsFetcher) isSkippableManagedResource(cfnType CfnResourceType, resourceIdentifier string) (bool, string) { + if a.cfn.IsManagedResource(cfnType, resourceIdentifier) { return true, fmt.Sprintf("CloudFormation generated resource %s", resourceIdentifier) } diff --git a/iamy/aws_test.go b/iamy/aws_test.go index 62e27c2..d103162 100644 --- a/iamy/aws_test.go +++ b/iamy/aws_test.go @@ -17,10 +17,12 @@ func TestIsSkippableManagedResource(t *testing.T) { "myalias-123/iam/instance-profile/example.yaml", } + f := AwsFetcher{cfn: &cfnClient{}} + for _, name := range skippables { t.Run(name, func(t *testing.T) { - skipped, err := isSkippableManagedResource(name) + skipped, err := f.isSkippableManagedResource(CfnIamRole, name) if skipped == false { t.Errorf("expected %s to be skipped but got false", name) } @@ -34,7 +36,7 @@ func TestIsSkippableManagedResource(t *testing.T) { for _, name := range nonSkippables { t.Run(name, func(t *testing.T) { - skipped, err := isSkippableManagedResource(name) + skipped, err := f.isSkippableManagedResource(CfnIamRole, name) if skipped == true { t.Errorf("expected %s to not be skipped but got true", name) } diff --git a/iamy/cfn.go b/iamy/cfn.go new file mode 100644 index 0000000..bb2ebe1 --- /dev/null +++ b/iamy/cfn.go @@ -0,0 +1,160 @@ +package iamy + +import ( + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/awserr" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" +) + +var cfnResourceRegexp = regexp.MustCompile(`-[A-Z0-9]{10,20}$`) + +type CfnResourceType string + +const ( + CfnIamPolicy = "AWS::IAM::Policy" + CfnIamRole = "AWS::IAM::Role" + CfnIamUser = "AWS::IAM::User" + CfnIamGroup = "AWS::IAM::Group" + CfnInstanceProfile = "AWS::IAM::InstanceProfile" + CfnS3Bucket = "AWS::S3::Bucket" +) + +type cfnClient struct { + cloudformationiface.CloudFormationAPI + managedResources map[string]CfnResourceTypes +} + +func newCfnClient(sess *session.Session) *cfnClient { + return &cfnClient{ + CloudFormationAPI: cloudformation.New(sess), + } +} + +// PopulateMangedResourceData enumerates all cloudformation stacks and resources to build an internal list of all +// resources that are managed by cloudformation. This list can then be checked by IsManagedResource +func (c *cfnClient) PopulateMangedResourceData() error { + c.managedResources = map[string]CfnResourceTypes{} + var nextStack *string + + for { + stacks, err := c.ListStacks(&cloudformation.ListStacksInput{ + NextToken: nextStack, + StackStatusFilter: []*string{ + aws.String("CREATE_IN_PROGRESS"), + aws.String("CREATE_COMPLETE"), + aws.String("ROLLBACK_COMPLETE"), + aws.String("IMPORT_COMPLETE"), + aws.String("REVIEW_IN_PROGRESS"), + aws.String("CREATE_IN_PROGRESS"), + aws.String("UPDATE_ROLLBACK_COMPLETE"), + aws.String("UPDATE_IN_PROGRESS"), + aws.String("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"), + aws.String("UPDATE_COMPLETE"), + aws.String("UPDATE_ROLLBACK_IN_PROGRESS"), + aws.String("UPDATE_ROLLBACK_FAILED"), + aws.String("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"), + aws.String("UPDATE_ROLLBACK_COMPLETE"), + aws.String("REVIEW_IN_PROGRESS"), + }, + }) + if awserr, ok := err.(awserr.Error); ok && awserr != nil && awserr.Code() == "Throttling" { + time.Sleep(1 * time.Second) + continue + } + if err != nil { + return err + } + + for _, stack := range stacks.StackSummaries { + var nextResource *string + for { + resources, err := c.ListStackResources(&cloudformation.ListStackResourcesInput{ + NextToken: nextResource, + StackName: stack.StackName, + }) + if awserr, ok := err.(awserr.Error); ok && awserr != nil && awserr.Code() == "Throttling" { + time.Sleep(1 * time.Second) + continue + } + if err != nil { + return err + } + + for _, resource := range resources.StackResourceSummaries { + resType := CfnResourceType(*resource.ResourceType) + if resType == "AWS::IAM::ManagedPolicy" { + resType = CfnIamPolicy // we dont care about the distinction as they are both in the "policy" namespace + } + name := *resource.PhysicalResourceId + // Dont know why, but some physical ids are arns, instead of names... + if strings.HasPrefix(*resource.PhysicalResourceId, "arn:aws:iam") { + parts := strings.Split(*resource.PhysicalResourceId, "/") + name = parts[len(parts)-1] + } + + if !resType.isInterestingResource() { + continue + } + + c.managedResources[name] = append(c.managedResources[name], resType) + } + + nextResource = resources.NextToken + if nextResource == nil { + break + } + } + } + + nextStack = stacks.NextToken + if nextStack == nil { + break + } + } + + return nil +} + +// IsManagedResource checks if the given resource is managed by cloudformation +// +// If PopulateMangedResourceData has been called it will be accurate, however for some accounts this may be slow. +// If PopulateMangedResourceData has not been called it will use a heuristic match, looking for the random ID that +// CFN appends to the name +func (c *cfnClient) IsManagedResource(cfnType CfnResourceType, resourceIdentifier string) bool { + if c.managedResources != nil { + return c.managedResources[resourceIdentifier].contains(cfnType) + } + + if cfnResourceRegexp.MatchString(resourceIdentifier) { + return true + } + + return false +} + +func (r CfnResourceType) isInterestingResource() bool { + switch r { + case CfnIamPolicy, CfnIamRole, CfnIamUser, CfnIamGroup, CfnInstanceProfile, CfnS3Bucket: + return true + } + + return false +} + +type CfnResourceTypes []CfnResourceType + +func (r CfnResourceTypes) contains(t CfnResourceType) bool { + for _, v := range r { + if v == t { + return true + } + } + return false +} diff --git a/iamy/cfn_test.go b/iamy/cfn_test.go new file mode 100644 index 0000000..7fe0ad7 --- /dev/null +++ b/iamy/cfn_test.go @@ -0,0 +1,34 @@ +package iamy + +import "testing" + +func TestCfnMangedResources(t *testing.T) { + t.Run("With fetched CFN resource lists", func(t *testing.T) { + + cfn := cfnClient{ + managedResources: map[string]CfnResourceTypes{ + "foobar": []CfnResourceType{CfnIamPolicy, CfnIamRole}, + }, + } + + if cfn.IsManagedResource(CfnIamUser, "foobar") { + t.Fatal("different object types with same name is not managed") + } + + if !cfn.IsManagedResource(CfnIamPolicy, "foobar") { + t.Fatal("matching object and type should be managed") + } + }) + + t.Run("With heuristic matching", func(t *testing.T) { + cfn := cfnClient{} + + if cfn.IsManagedResource(CfnIamUser, "foobar") { + t.Fatal("names without id suffix are not managed") + } + + if !cfn.IsManagedResource(CfnIamPolicy, "foobar-ABCDEFGH1234567") { + t.Fatal("names with id suffix are managed") + } + }) +} diff --git a/pull.go b/pull.go index a0854eb..bb57eee 100644 --- a/pull.go +++ b/pull.go @@ -7,12 +7,13 @@ import ( ) type PullCommandInput struct { - Dir string - CanDelete bool + Dir string + CanDelete bool + HeuristicCfnMatching bool } func PullCommand(ui Ui, input PullCommandInput) { - aws := iamy.AwsFetcher{Debug: ui.Debug} + aws := iamy.AwsFetcher{Debug: ui.Debug, HeuristicCfnMatching: input.HeuristicCfnMatching} data, err := aws.Fetch() if err != nil { ui.Error.Fatal(fmt.Printf("%s", err)) diff --git a/push.go b/push.go index 21d84da..cd68ab5 100644 --- a/push.go +++ b/push.go @@ -21,7 +21,7 @@ func PushCommand(ui Ui, input PushCommandInput) { } aws := iamy.AwsFetcher{ SkipFetchingPolicyAndRoleDescriptions: true, - Debug: ui.Debug, + Debug: ui.Debug, } allDataFromYaml, err := yaml.Load() @@ -66,7 +66,7 @@ func sync(yamlData iamy.AccountData, awsData *iamy.AccountData, ui Ui) { return } - ui.Println("Commands to push changes to AWS:\n") + ui.Println("Commands to push changes to AWS:") printCommands(" ", awsCmds, ui) From 46e9334d0a2fab90120669ce7038da8c79f86bfa Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Mon, 15 Jun 2020 15:39:32 +1000 Subject: [PATCH 2/2] skip cfn resources without a physical resource attached --- iamy/cfn.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iamy/cfn.go b/iamy/cfn.go index bb2ebe1..8cd107b 100644 --- a/iamy/cfn.go +++ b/iamy/cfn.go @@ -88,6 +88,9 @@ func (c *cfnClient) PopulateMangedResourceData() error { } for _, resource := range resources.StackResourceSummaries { + if resource.PhysicalResourceId == nil { + continue + } resType := CfnResourceType(*resource.ResourceType) if resType == "AWS::IAM::ManagedPolicy" { resType = CfnIamPolicy // we dont care about the distinction as they are both in the "policy" namespace