Skip to content

Commit

Permalink
Oak/policy hackathon mbrock changes (#50169)
Browse files Browse the repository at this point in the history
* Adding access analyzer to create user tasks

* Pulling new policy recommendation and comparing against the old one

* Upserting the policy updates as a user task

* Decoding existing doc and remove user task upserting

* Now sending policy recommendations to access graph
  • Loading branch information
mvbrock authored Dec 12, 2024
1 parent dbcc533 commit cd0f9ab
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 0 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ require (
software.sslmate.com/src/go-pkcs12 v0.5.0
)

require github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.36.2

require (
cel.dev/expr v0.16.1 // indirect
cloud.google.com/go v0.116.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvK
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 h1:r67ps7oHCYnflpgDy2LZU0MAQtQbYIOqNNnqGO6xQkE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25/go.mod h1:GrGY+Q4fIokYLtjCVB/aFfCVL6hhGUFl8inD18fDalE=
github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.36.2 h1:9WvCTutkgDExBamb9UZQ94oiCjJwXUhhtoBH3NIs0Iw=
github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.36.2/go.mod h1:9QmJU2Zam+wUZe8etjM4VY9NlC0WeMFLIvtUIOIko4U=
github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.34.1 h1:8EwNbY+A/Q5myYggLJ7v9v9f00UuWoh9S04y5kre8UQ=
github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.34.1/go.mod h1:2mMP2R86zLPAUz0TpJdsKW8XawHgs9Nk97fYJomO3o8=
github.com/aws/aws-sdk-go-v2/service/athena v1.49.0 h1:D+iatX9gV6gCuNd6BnUkfwfZJw/cXlEk+LwwDdSMdtw=
Expand Down
21 changes: 21 additions & 0 deletions lib/cloud/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import (
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/request"
awssession "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/accessanalyzer"
"github.com/aws/aws-sdk-go/service/accessanalyzer/accessanalyzeriface"
"github.com/aws/aws-sdk-go/service/eks"
"github.com/aws/aws-sdk-go/service/eks/eksiface"
"github.com/aws/aws-sdk-go/service/elasticache"
Expand Down Expand Up @@ -137,6 +139,8 @@ type AWSClients interface {
GetAWSKMSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (kmsiface.KMSAPI, error)
// GetAWSS3Client returns AWS S3 client.
GetAWSS3Client(ctx context.Context, region string, opts ...AWSOptionsFn) (s3iface.S3API, error)
// GetAWSIAMAccessAnalyzerClient returns AWS IAM Access Analyzer client
GetAWSIAMAccessAnalyzerClient(ctx context.Context, region string, opts ...AWSOptionsFn) (accessanalyzeriface.AccessAnalyzerAPI, error)
}

// AzureClients is an interface for Azure-specific API clients
Expand Down Expand Up @@ -597,6 +601,14 @@ func (c *cloudClients) GetAWSEKSClient(ctx context.Context, region string, opts
return eks.New(session), nil
}

func (c *cloudClients) GetAWSIAMAccessAnalyzerClient(ctx context.Context, region string, opts ...AWSOptionsFn) (accessanalyzeriface.AccessAnalyzerAPI, error) {
session, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return accessanalyzer.New(session), nil
}

// GetAWSKMSClient returns AWS KMS client for the specified region.
func (c *cloudClients) GetAWSKMSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (kmsiface.KMSAPI, error) {
session, err := c.GetAWSSession(ctx, region, opts...)
Expand Down Expand Up @@ -1012,6 +1024,7 @@ type TestCloudClients struct {
EKS eksiface.EKSAPI
KMS kmsiface.KMSAPI
S3 s3iface.S3API
AccessAnalyzer accessanalyzeriface.AccessAnalyzerAPI
AzureMySQL azure.DBServersClient
AzureMySQLPerSub map[string]azure.DBServersClient
AzurePostgres azure.DBServersClient
Expand Down Expand Up @@ -1177,6 +1190,14 @@ func (c *TestCloudClients) GetAWSKMSClient(ctx context.Context, region string, o
return c.KMS, nil
}

func (c *TestCloudClients) GetAWSIAMAccessAnalyzerClient(ctx context.Context, region string, opts ...AWSOptionsFn) (accessanalyzeriface.AccessAnalyzerAPI, error) {
_, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return c.AccessAnalyzer, nil
}

// GetGCPIAMClient returns GCP IAM client.
func (c *TestCloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) {
return gcpcredentials.NewIamCredentialsClient(ctx,
Expand Down
1 change: 1 addition & 0 deletions lib/srv/discovery/access_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ func (s *Server) accessGraphFetchersFromMatchers(ctx context.Context, matchers M
Regions: awsFetcher.Regions,
Integration: awsFetcher.Integration,
DiscoveryConfigName: discoveryConfigName,
AccessPoint: s.AccessPoint,
},
)
if err != nil {
Expand Down
124 changes: 124 additions & 0 deletions lib/srv/discovery/fetchers/aws-sync/access_analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package aws_sync

import (
"context"
"fmt"
accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
"net/url"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/accessanalyzer"
)

const pollInterval = 100 * time.Millisecond
const maxPollTime = 1 * time.Minute

func (a *awsFetcher) fetchPolicyChanges(ctx context.Context, result *Resources) ([]*accessgraphv1alpha.AWSPolicyChange, error) {
// Initialize the client
client, err := a.CloudClients.GetAWSIAMAccessAnalyzerClient(ctx, "us-east-2", a.getAWSOptions()...)
if err != nil {
return nil, err
}
analyzerArn := "arn:aws:access-analyzer:us-east-2:278576220453:analyzer/mbrock-test"
input := &accessanalyzer.ListFindingsV2Input{
AnalyzerArn: &analyzerArn,
}
findings, err := client.ListFindingsV2(input)
if err != nil {
return nil, err
}

// Generate the finding recommendations
var findingsWithRecs []*accessanalyzer.FindingSummaryV2
for _, finding := range findings.Findings {
if *finding.FindingType != "UnusedPermission" || *finding.Status == "RESOLVED" {
continue
}
_, err = client.GenerateFindingRecommendation(&accessanalyzer.GenerateFindingRecommendationInput{
AnalyzerArn: aws.String(analyzerArn),
Id: finding.Id,
})
if err != nil {
continue
}
findingsWithRecs = append(findingsWithRecs, finding)
}

// Poll the recommendations until they've been successfully generated or max time is reached
findingRecs := make(map[string]*accessanalyzer.GetFindingRecommendationOutput)
timeStart := time.Now()
for {
for _, finding := range findingsWithRecs {
if _, ok := findingRecs[*finding.Id]; ok {
continue
}
rec, err := client.GetFindingRecommendation(&accessanalyzer.GetFindingRecommendationInput{
AnalyzerArn: aws.String(analyzerArn),
Id: finding.Id,
})
if err != nil {
time.Sleep(pollInterval)
continue
}
if *rec.Status == "SUCCEEDED" {
findingRecs[*finding.Id] = rec
} else {
time.Sleep(pollInterval)
}
}
if time.Since(timeStart) > maxPollTime {
break
}
if len(findingRecs) == len(findingsWithRecs) {
break
}
}

// Get the fetched policies and associate them with the recommendations
policies := make(map[string]*accessgraphv1alpha.AWSPolicyV1)
for _, policy := range result.Policies {
policies[policy.Arn] = policy
}
var policyChanges []*accessgraphv1alpha.AWSPolicyChange
for _, rec := range findingRecs {
policyChange := &accessgraphv1alpha.AWSPolicyChange{
ResourceArn: *rec.ResourceArn,
}
for _, step := range rec.RecommendedSteps {
unused := step.UnusedPermissionsRecommendedStep
existingPolicy, ok := policies[*unused.ExistingPolicyId]
if !ok {
fmt.Printf("No existing policy found for %s\n", *unused.ExistingPolicyId)
continue
}
existingDoc, err := url.QueryUnescape(string(existingPolicy.PolicyDocument))
if err != nil {
fmt.Printf("Could not decode URL-encoded policy document: %v\n", err)
continue
}
newDoc := ""
if unused.RecommendedPolicy != nil {
newDoc = *unused.RecommendedPolicy
}
fmt.Printf("Recommending policy change from '%s' to '%s'\n", existingDoc, newDoc)
change := accessgraphv1alpha.PolicyChange{
PolicyName: *unused.ExistingPolicyId,
ExistingPolicy: existingDoc,
NewDocument: newDoc,
Detach: false,
}
if *unused.RecommendedAction == "DETACH_POLICY" {
change.Detach = true
}
policyChange.Changes = append(policyChange.Changes, &change)
}
if len(policyChange.Changes) > 0 {
policyChanges = append(policyChanges, policyChange)
} else {
fmt.Printf("Not appending an empty set of policy changes for %s\n", policyChange.ResourceArn)
}
}
fmt.Printf("Returning %d policy changes\n", len(policyChanges))
return policyChanges, nil
}
12 changes: 12 additions & 0 deletions lib/srv/discovery/fetchers/aws-sync/aws-sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package aws_sync

import (
"context"
"github.com/gravitational/teleport/lib/auth/authclient"
"reflect"
"sync"
"time"
Expand Down Expand Up @@ -59,6 +60,8 @@ type Config struct {
Integration string
// DiscoveryConfigName if set, will be used to report the Discovery Config Status to the Auth Server.
DiscoveryConfigName string
// The AccessPoint for sending auth commands
AccessPoint authclient.DiscoveryAccessPoint
}

// AssumeRole is the configuration for assuming an AWS role.
Expand Down Expand Up @@ -138,6 +141,8 @@ type Resources struct {
SAMLProviders []*accessgraphv1alpha.AWSSAMLProviderV1
// OIDCProviders is a list of OIDC providers.
OIDCProviders []*accessgraphv1alpha.AWSOIDCProviderV1
// PolicyChanges is a list of policy changes from the IAM Access Analyzer
PolicyChanges []*accessgraphv1alpha.AWSPolicyChange
}

func (r *Resources) count() int {
Expand Down Expand Up @@ -203,6 +208,12 @@ func (a *awsFetcher) Poll(ctx context.Context, features Features) (*Resources, e
result, err := a.poll(ctx, features)
deduplicateResources(result)
a.storeReport(result, err)
// Fetch policy changes outside the poll loop for max hackathonification
changes, taskErr := a.fetchPolicyChanges(ctx, result)
if taskErr != nil {
err = trace.NewAggregate(err, taskErr)
}
result.PolicyChanges = changes
return result, trace.Wrap(err)
}

Expand Down Expand Up @@ -299,6 +310,7 @@ func (a *awsFetcher) poll(ctx context.Context, features Features) (*Resources, e
if err := eGroup.Wait(); err != nil {
return nil, trace.Wrap(err)
}

return result, trace.NewAggregate(errs...)
}

Expand Down
2 changes: 2 additions & 0 deletions lib/srv/discovery/fetchers/aws-sync/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func MergeResources(results ...*Resources) *Resources {
result.RDSDatabases = append(result.RDSDatabases, r.RDSDatabases...)
result.SAMLProviders = append(result.SAMLProviders, r.SAMLProviders...)
result.OIDCProviders = append(result.OIDCProviders, r.OIDCProviders...)
result.PolicyChanges = append(result.PolicyChanges, r.PolicyChanges...)
}

deduplicateResources(result)
Expand Down Expand Up @@ -78,4 +79,5 @@ func deduplicateResources(result *Resources) {
result.RDSDatabases = deduplicateSlice(result.RDSDatabases, rdsDbKey)
result.SAMLProviders = deduplicateSlice(result.SAMLProviders, samlProvKey)
result.OIDCProviders = deduplicateSlice(result.OIDCProviders, oidcProvKey)
result.PolicyChanges = deduplicateSlice(result.PolicyChanges, policyChangeKey)
}
9 changes: 9 additions & 0 deletions lib/srv/discovery/fetchers/aws-sync/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func ReconcileResults(old *Resources, new *Resources) (upsert, delete *accessgra
reconcile(old.RDSDatabases, new.RDSDatabases, rdsDbKey, rdsDbWrap),
reconcile(old.SAMLProviders, new.SAMLProviders, samlProvKey, samlProvWrap),
reconcile(old.OIDCProviders, new.OIDCProviders, oidcProvKey, oidcProvWrap),
reconcile(old.PolicyChanges, new.PolicyChanges, policyChangeKey, policyChangeWrap),
}
for _, res := range reconciledResources {
upsert.Resources = append(upsert.Resources, res.upsert.Resources...)
Expand Down Expand Up @@ -296,3 +297,11 @@ func oidcProvKey(provider *accessgraphv1alpha.AWSOIDCProviderV1) string {
func oidcProvWrap(provider *accessgraphv1alpha.AWSOIDCProviderV1) *accessgraphv1alpha.AWSResource {
return &accessgraphv1alpha.AWSResource{Resource: &accessgraphv1alpha.AWSResource_OidcProvider{OidcProvider: provider}}
}

func policyChangeKey(change *accessgraphv1alpha.AWSPolicyChange) string {
return fmt.Sprintf("%s", change.ResourceArn)
}

func policyChangeWrap(change *accessgraphv1alpha.AWSPolicyChange) *accessgraphv1alpha.AWSResource {
return &accessgraphv1alpha.AWSResource{Resource: &accessgraphv1alpha.AWSResource_AwsPolicyChange{AwsPolicyChange: change}}
}

0 comments on commit cd0f9ab

Please sign in to comment.