Skip to content

Commit d5b772e

Browse files
authored
feat: AWS v2: Split users and credential report users into separate resources (#1835)
1 parent 20096aa commit d5b772e

File tree

11 files changed

+338
-398
lines changed

11 files changed

+338
-398
lines changed

plugins/source/aws/client/mocks/mock_iam.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/source/aws/client/services.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ type IamClient interface {
510510
ListRoles(ctx context.Context, params *iam.ListRolesInput, optFns ...func(*iam.Options)) (*iam.ListRolesOutput, error)
511511
ListSAMLProviders(ctx context.Context, params *iam.ListSAMLProvidersInput, optFns ...func(*iam.Options)) (*iam.ListSAMLProvidersOutput, error)
512512
ListUserPolicies(ctx context.Context, params *iam.ListUserPoliciesInput, optFns ...func(*iam.Options)) (*iam.ListUserPoliciesOutput, error)
513+
ListUsers(context.Context, *iam.ListUsersInput, ...func(*iam.Options)) (*iam.ListUsersOutput, error)
513514
ListVirtualMFADevices(ctx context.Context, params *iam.ListVirtualMFADevicesInput, optFns ...func(*iam.Options)) (*iam.ListVirtualMFADevicesOutput, error)
514-
515515
iam.ListServerCertificatesAPIClient
516516
iam.ListAccountAliasesAPIClient
517517
GetAccountSummary(ctx context.Context, params *iam.GetAccountSummaryInput, optFns ...func(*iam.Options)) (*iam.GetAccountSummaryOutput, error)

plugins/source/aws/codegen/recipes/iam.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ func IAMResources() []*Resource {
2626
},
2727
},
2828
},
29+
{
30+
SubService: "credential_reports",
31+
Struct: &iamService.CredentialReportEntry{},
32+
SkipFields: []string{"Arn", "UserCreationTime"},
33+
ExtraColumns: []codegen.ColumnDefinition{
34+
{
35+
Name: "arn",
36+
Type: schema.TypeString,
37+
Resolver: `schema.PathResolver("Arn")`,
38+
Options: schema.ColumnCreationOptions{PrimaryKey: true},
39+
},
40+
{
41+
Name: "user_creation_time",
42+
Type: schema.TypeTimestamp,
43+
Resolver: `schema.PathResolver("UserCreationTime")`,
44+
Options: schema.ColumnCreationOptions{PrimaryKey: true},
45+
},
46+
},
47+
Relations: []string{},
48+
},
2949
{
3050
SubService: "groups",
3151
Struct: &types.Group{},
@@ -226,10 +246,9 @@ func IAMResources() []*Resource {
226246
},
227247
},
228248
{
229-
SubService: "users",
230-
Struct: &iamService.UserWrapper{},
231-
SkipFields: []string{"Arn", "AccountId", "Id", "Tags"},
232-
PostResourceResolver: `postIamUserResolver`,
249+
SubService: "users",
250+
Struct: &types.User{},
251+
SkipFields: []string{"Arn", "AccountId", "Id", "Tags"},
233252
ExtraColumns: []codegen.ColumnDefinition{
234253
{
235254
Name: "arn",

plugins/source/aws/resources/plugin/tables.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ func tables() []*schema.Table {
195195
glue.Workflows(),
196196
guardduty.Detectors(),
197197
iam.Accounts(),
198+
iam.CredentialReports(),
198199
iam.Groups(),
199200
iam.OpenidConnectIdentityProviders(),
200201
iam.PasswordPolicies(),

plugins/source/aws/resources/services/iam/credential_reports.go

Lines changed: 131 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package iam
2+
3+
import (
4+
"context"
5+
"errors"
6+
"time"
7+
8+
"github.com/aws/aws-sdk-go-v2/service/iam"
9+
"github.com/aws/smithy-go"
10+
"github.com/cloudquery/cloudquery/plugins/source/aws/client"
11+
"github.com/cloudquery/plugin-sdk/schema"
12+
"github.com/gocarina/gocsv"
13+
)
14+
15+
type CredentialReportEntry struct {
16+
User string `csv:"user"`
17+
Arn string `csv:"arn"`
18+
UserCreationTime time.Time `csv:"user_creation_time"`
19+
PasswordStatus string `csv:"password_enabled"`
20+
PasswordLastChanged string `csv:"password_last_changed"`
21+
PasswordNextRotation string `csv:"password_next_rotation"`
22+
MfaActive bool `csv:"mfa_active"`
23+
AccessKey1Active bool `csv:"access_key_1_active"`
24+
AccessKey2Active bool `csv:"access_key_2_active"`
25+
AccessKey1LastRotated string `csv:"access_key_1_last_rotated"`
26+
AccessKey2LastRotated string `csv:"access_key_2_last_rotated"`
27+
Cert1Active bool `csv:"cert_1_active"`
28+
Cert2Active bool `csv:"cert_2_active"`
29+
Cert1LastRotated string `csv:"cert_1_last_rotated"`
30+
Cert2LastRotated string `csv:"cert_2_last_rotated"`
31+
AccessKey1LastUsedDate time.Time `csv:"access_key_1_last_used_date"`
32+
AccessKey1LastUsedRegion string `csv:"access_key_1_last_used_region"`
33+
AccessKey1LastUsedService string `csv:"access_key_1_last_used_service"`
34+
AccessKey2LastUsedDate time.Time `csv:"access_key_2_last_used_date"`
35+
AccessKey2LastUsedRegion string `csv:"access_key_2_last_used_region"`
36+
AccessKey2LastUsedService string `csv:"access_key_2_last_used_service"`
37+
PasswordLastUsed string `csv:"password_last_used"`
38+
}
39+
40+
func fetchIamCredentialReports(ctx context.Context, meta schema.ClientMeta, _ *schema.Resource, res chan<- interface{}) error {
41+
var err error
42+
var apiErr smithy.APIError
43+
var reportOutput *iam.GetCredentialReportOutput
44+
svc := meta.(*client.Client).Services().IAM
45+
for {
46+
reportOutput, err = svc.GetCredentialReport(ctx, &iam.GetCredentialReportInput{})
47+
if err == nil && reportOutput != nil {
48+
var users []*CredentialReportEntry
49+
err = gocsv.UnmarshalBytes(reportOutput.Content, &users)
50+
if err != nil {
51+
return err
52+
}
53+
res <- users
54+
}
55+
if !errors.As(err, &apiErr) {
56+
return err
57+
}
58+
switch apiErr.ErrorCode() {
59+
case "ReportNotPresent", "ReportExpired":
60+
_, err := svc.GenerateCredentialReport(ctx, &iam.GenerateCredentialReportInput{})
61+
if err != nil {
62+
var serviceError smithy.APIError
63+
if !errors.As(err, &serviceError) {
64+
return err
65+
}
66+
// LimitExceeded is the only specific error that should not stop processing
67+
// If Limit Exceeded is returned we should try and see if there is a credential report
68+
// already generated so we want to sleep for 5 seconds then continue
69+
if serviceError.ErrorCode() != "LimitExceeded" {
70+
return err
71+
}
72+
if err := client.Sleep(ctx, 5*time.Second); err != nil {
73+
return err
74+
}
75+
}
76+
case "ReportInProgress":
77+
meta.Logger().Debug().Msg("Waiting for credential report to be generated")
78+
if err := client.Sleep(ctx, 5*time.Second); err != nil {
79+
return err
80+
}
81+
default:
82+
return err
83+
}
84+
}
85+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package iam
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/aws/aws-sdk-go-v2/service/iam"
8+
"github.com/cloudquery/cloudquery/plugins/source/aws/client"
9+
"github.com/cloudquery/cloudquery/plugins/source/aws/client/mocks"
10+
"github.com/cloudquery/faker/v3"
11+
"github.com/gocarina/gocsv"
12+
"github.com/golang/mock/gomock"
13+
)
14+
15+
func buildCredentialReportUsers(t *testing.T, ctrl *gomock.Controller) client.Services {
16+
m := mocks.NewMockIamClient(ctrl)
17+
18+
ru := CredentialReportEntry{}
19+
err := faker.FakeData(&ru)
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
ru.Arn = "arn123"
24+
ru.PasswordStatus = "true"
25+
ru.PasswordNextRotation = time.Now().Format(time.RFC3339)
26+
ru.PasswordLastChanged = time.Now().Format(time.RFC3339)
27+
ru.AccessKey1LastRotated = time.Now().Format(time.RFC3339)
28+
ru.AccessKey2LastRotated = time.Now().Format(time.RFC3339)
29+
ru.Cert1LastRotated = time.Now().Format(time.RFC3339)
30+
ru.Cert2LastRotated = time.Now().Format(time.RFC3339)
31+
content, err := gocsv.MarshalBytes([]CredentialReportEntry{ru})
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
36+
m.EXPECT().GetCredentialReport(gomock.Any(), gomock.Any()).Return(
37+
&iam.GetCredentialReportOutput{
38+
Content: content,
39+
}, nil)
40+
41+
return client.Services{
42+
IAM: m,
43+
}
44+
}
45+
46+
func TestCredentialReports(t *testing.T) {
47+
client.AwsMockTestHelper(t, CredentialReports(), buildCredentialReportUsers, client.TestOptions{})
48+
}

plugins/source/aws/resources/services/iam/user_policies_fetch.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import (
77

88
"github.com/aws/aws-sdk-go-v2/aws"
99
"github.com/aws/aws-sdk-go-v2/service/iam"
10+
"github.com/aws/aws-sdk-go-v2/service/iam/types"
1011
"github.com/cloudquery/cloudquery/plugins/source/aws/client"
1112
"github.com/cloudquery/plugin-sdk/schema"
1213
)
1314

1415
func fetchIamUserPolicies(ctx context.Context, meta schema.ClientMeta, parent *schema.Resource, res chan<- interface{}) error {
1516
c := meta.(*client.Client)
1617
svc := c.Services().IAM
17-
user := parent.Item.(UserWrapper)
18-
if aws.ToString(user.UserName) == rootName {
19-
return nil
20-
}
18+
user := parent.Item.(*types.User)
2119
config := iam.ListUserPoliciesInput{UserName: user.UserName}
2220
for {
2321
output, err := svc.ListUserPolicies(ctx, &config)

0 commit comments

Comments
 (0)