diff --git a/aws/plugin.go b/aws/plugin.go index ee91bbe5a..3c54b2660 100644 --- a/aws/plugin.go +++ b/aws/plugin.go @@ -70,6 +70,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { }, TableMap: map[string]*plugin.Table{ "aws_accessanalyzer_analyzer": tableAwsAccessAnalyzer(ctx), + "aws_accessanalyzer_finding": tableAwsAccessAnalyzerFinding(ctx), "aws_account": tableAwsAccount(ctx), "aws_account_alternate_contact": tableAwsAccountAlternateContact(ctx), "aws_account_contact": tableAwsAccountContact(ctx), diff --git a/aws/table_aws_accessanalyzer_analyzer.go b/aws/table_aws_accessanalyzer_analyzer.go index d696302a8..cbc0d48dc 100644 --- a/aws/table_aws_accessanalyzer_analyzer.go +++ b/aws/table_aws_accessanalyzer_analyzer.go @@ -88,7 +88,7 @@ func tableAwsAccessAnalyzer(_ context.Context) *plugin.Table { }, { Name: "findings", - Description: "A list of findings retrieved from the analyzer that match the filter criteria specified, if any.", + Description: "[DEPRECATED] This column has been deprecated and will be removed in a future release, use table aws_accessanalyzer_finding instead. A list of findings retrieved from the analyzer that match the filter criteria specified, if any.", Type: proto.ColumnType_JSON, Hydrate: listAccessAnalyzerFindings, Transform: transform.FromValue(), diff --git a/aws/table_aws_accessanalyzer_finding.go b/aws/table_aws_accessanalyzer_finding.go new file mode 100644 index 000000000..c17167288 --- /dev/null +++ b/aws/table_aws_accessanalyzer_finding.go @@ -0,0 +1,310 @@ +package aws + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go-v2/service/accessanalyzer" + "github.com/aws/aws-sdk-go-v2/service/accessanalyzer/types" + "github.com/aws/smithy-go" + + accessanalyzerv1 "github.com/aws/aws-sdk-go/service/accessanalyzer" + + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +//// TABLE DEFINITION + +type accessanalyzerFindingInfo = struct { + Finding types.Finding + AccessAnalyzerArn string +} + +func tableAwsAccessAnalyzerFinding(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "aws_accessanalyzer_finding", + Description: "AWS Access Analyzer Finding", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"id", "access_analyzer_arn"}), + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ResourceNotFoundException", "ValidationException"}), + }, + Hydrate: getAccessAnalyzerFinding, + Tags: map[string]string{"service": "access-analyzer", "action": "GetFinding"}, + }, + List: &plugin.ListConfig{ + ParentHydrate: listAccessAnalyzers, + Hydrate: listAccessAnalyzersFindings, + Tags: map[string]string{"service": "access-analyzer", "action": "ListFindings"}, + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ResourceNotFoundException", "ValidationException"}), + }, + KeyColumns: []*plugin.KeyColumn{ + { + Name: "access_analyzer_arn", + Require: plugin.Optional, + }, + { + Name: "id", + Require: plugin.Optional, + }, + { + Name: "resource", + Require: plugin.Optional, + }, + { + Name: "status", + Require: plugin.Optional, + }, + }, + }, + GetMatrixItemFunc: SupportedRegionMatrix(accessanalyzerv1.EndpointsID), + Columns: awsRegionalColumns([]*plugin.Column{ + { + Name: "access_analyzer_arn", + Description: "The Amazon Resource Name (ARN) of the analyzer that generated the finding.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The ID of the finding.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.Id"), + }, + { + Name: "analyzed_at", + Description: "The time at which the resource-based policy that generated the finding was analyzed.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromField("Finding.AnalyzedAt"), + }, + { + Name: "created_at", + Description: "The time at which the finding was created.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromField("Finding.CreatedAt"), + }, + { + Name: "error", + Description: "The error that resulted in an Error finding.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.Error"), + }, + { + Name: "is_public", + Description: "Indicates whether the finding reports a resource that has a policy that allows public access.", + Type: proto.ColumnType_BOOL, + Transform: transform.FromField("Finding.IsPublic"), + }, + { + Name: "resource", + Description: "The resource that the external principal has access to.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.Resource"), + }, + { + Name: "resource_owner_account", + Description: "The Amazon Web Services account ID that owns the resource.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.ResourceOwnerAccount"), + }, + { + Name: "resource_type", + Description: "The type of the resource that the external principal has access to.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.ResourceType"), + }, + { + Name: "status", + Description: "The status of the finding.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.Status"), + }, + { + Name: "updated_at", + Description: "The time at which the finding was most recently updated.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromField("Finding.UpdatedAt"), + }, + { + Name: "action", + Description: "The action in the analyzed policy statement that an external principal has permission to use.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Finding.Action"), + }, + { + Name: "sources", + Description: "The sources of the finding, indicating how the access that generated the finding is granted. It is populated for Amazon S3 bucket findings.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Finding.Sources"), + }, + { + Name: "principal", + Description: "The external principal that has access to a resource within the zone of trust.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Finding.Principal"), + }, + { + Name: "condition", + Description: "The condition in the analyzed policy statement that resulted in a finding.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Finding.Condition"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: resourceInterfaceDescription("title"), + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Finding.Id"), + }, + }), + } +} + +//// LIST FUNCTION + +func listAccessAnalyzersFindings(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + + var arn string + if h.Item != nil { + arn = *h.Item.(types.AnalyzerSummary).Arn + } + + // Minimize API call with given Access analyzer ARN + if arn != "" && d.EqualsQualString("access_analyzer_arn") != "" { + if d.EqualsQualString("access_analyzer_arn") != arn { + return nil, nil + } + } + + // Create session + svc, err := AccessAnalyzerClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_accessanalyzer_finding.listAccessAnalyzersFindings", "client_error", err) + return nil, err + } + + // The maximum number for MaxResults parameter is not defined by the API + // We have set the MaxResults to 1000 based on our test + maxItems := int32(1000) + + // Reduce the basic request limit down if the user has only requested a small number of rows + if d.QueryContext.Limit != nil { + limit := int32(*d.QueryContext.Limit) + if limit < maxItems { + maxItems = int32(limit) + } + } + + input := &accessanalyzer.ListFindingsInput{ + AnalyzerArn: &arn, + MaxResults: &maxItems, + Filter: map[string]types.Criterion{}, + } + + // set optional params + setFilterCriteria(d, input) + + paginator := accessanalyzer.NewListFindingsPaginator(svc, input, func(o *accessanalyzer.ListFindingsPaginatorOptions) { + o.Limit = maxItems + o.StopOnDuplicateToken = true + }) + + for paginator.HasMorePages() { + // apply rate limiting + d.WaitForListRateLimit(ctx) + + output, err := paginator.NextPage(ctx) + + if err != nil { + var ae smithy.APIError + if errors.As(err, &ae) { + if ae.ErrorCode() == "ResourceNotFoundException" || ae.ErrorCode() == "ValidationException" { + return nil, nil + } + } + plugin.Logger(ctx).Error("aws_accessanalyzer_finding.listAccessAnalyzersFindings", "api_error", err) + return nil, err + } + + for _, finding := range output.Findings { + f := types.Finding{ + AnalyzedAt: finding.AnalyzedAt, + Condition: finding.Condition, + CreatedAt: finding.CreatedAt, + Error: finding.Error, + Id: finding.Id, + IsPublic: finding.IsPublic, + Principal: finding.Principal, + Resource: finding.Resource, + ResourceOwnerAccount: finding.ResourceOwnerAccount, + ResourceType: finding.ResourceType, + Sources: finding.Sources, + Status: finding.Status, + UpdatedAt: finding.UpdatedAt, + } + d.StreamListItem(ctx, accessanalyzerFindingInfo{f, arn}) + + // Context may get canceled due to manual cancellation or if the limit has been reached + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + return nil, nil +} + +//// HYDRATE FUNCTIONS + +func getAccessAnalyzerFinding(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + id := d.EqualsQualString("id") + arn := d.EqualsQualString("access_analyzer_arn") + + // check if id or arn is empty + if id == "" || arn == "" { + return nil, nil + } + + // Create Session + svc, err := AccessAnalyzerClient(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("aws_accessanalyzer_finding.getAccessAnalyzerFinding", "client_error", err) + return nil, err + } + + // Build the params + params := &accessanalyzer.GetFindingInput{ + AnalyzerArn: &arn, + Id: &id, + } + + // Get call + data, err := svc.GetFinding(ctx, params) + + if err != nil { + var ae smithy.APIError + if errors.As(err, &ae) { + if ae.ErrorCode() == "ResourceNotFoundException" || ae.ErrorCode() == "ValidationException" { + return nil, nil + } + } + plugin.Logger(ctx).Error("aws_accessanalyzer_finding.getAccessAnalyzerFinding", "api_error", err) + return nil, err + } + + return accessanalyzerFindingInfo{*data.Finding, arn}, nil +} + +func setFilterCriteria(d *plugin.QueryData, input *accessanalyzer.ListFindingsInput) { + params := []string{"id", "status", "resource"} + for _, param := range params { + if d.Quals[param] != nil { + input.Filter[param] = types.Criterion{ + Eq: []string{d.EqualsQualString(param)}, + } + } + } +} diff --git a/docs/tables/aws_accessanalyzer_finding.md b/docs/tables/aws_accessanalyzer_finding.md new file mode 100644 index 000000000..0fc862abf --- /dev/null +++ b/docs/tables/aws_accessanalyzer_finding.md @@ -0,0 +1,123 @@ +--- +title: "Steampipe Table: aws_accessanalyzer_finding - Query AWS Access Analyzer Findings using SQL" +description: "Allows users to query Access Analyzer findings in AWS IAM to retrieve detailed information about potential security risks." +--- + +# Table: aws_accessanalyzer_finding - Query AWS Access Analyzer Findings using SQL + +AWS Access Analyzer findings provide detailed information about potential security risks in your AWS environment. These findings are generated when Access Analyzer identifies resources that are shared with an external entity, highlighting potential unintended access. By analyzing the resource-based policies, Access Analyzer helps you understand how access to your resources is granted and suggests modifications to achieve desired access policies, enhancing your security posture. + +## Table Usage Guide + +The `aws_accessanalyzer_finding` table in Steampipe allows you to query information related to findings from the AWS IAM Access Analyzer. This table is essential for security and compliance teams, enabling them to identify, analyze, and manage findings related to resource access policies. Through this table, users can access detailed information about each finding, including the actions involved, the condition that led to the finding, the resource and principal involved, and the finding's status. By leveraging this table, you can efficiently address security and compliance issues in your AWS environment. + +## Examples + +### Basic Info + +Retrieve essential details of findings to understand potential access issues and their current status. This query helps in identifying the nature of each finding, the resources involved, and the actions recommended or taken to resolve these issues. + +```sql+postgres +select + id, + access_analyzer_arn, + analyzed_at, + resource_type, + status, + is_public +from + aws_accessanalyzer_finding; +``` + +```sql+sqlite +select + id, + analyzed_at, + resource_type, + status, + is_public +from + aws_accessanalyzer_finding; +``` + +### Findings involving public access + +Identify findings where resources are potentially exposed to public access. Highlighting such findings is critical for prioritizing issues that may lead to unauthorized access. This query helps in swiftly identifying and addressing potential vulnerabilities, ensuring that resources are adequately secured against public exposure. + +```sql+postgres +select + id, + resource_type, + access_analyzer_arn, + status, + is_public +from + aws_accessanalyzer_finding +where + is_public = true; +``` + +```sql+sqlite +select + id, + resource_type, + access_analyzer_arn, + status, + is_public +from + aws_accessanalyzer_finding +where + is_public = true; +``` + +### Findings by resource type + +Aggregate findings by resource type to focus remediation efforts on specific types of resources. This categorization helps in streamlining the security review process by allowing teams to prioritize resources based on their sensitivity and exposure. + +```sql+postgres +select + resource_type, + count(*) as findings_count +from + aws_accessanalyzer_finding +group by + resource_type; +``` + +```sql+sqlite +select + resource_type, + count(*) as findings_count +from + aws_accessanalyzer_finding +group by + resource_type; +``` + +### Recent findings + +Focus on findings that have been identified recently to address potentially new security risks. This query aids in maintaining an up-to-date security posture by ensuring that recent findings are promptly reviewed and addressed. + +```sql+postgres +select + id, + resource, + status, + analyzed_at +from + aws_accessanalyzer_finding +where + analyzed_at > current_date - interval '30 days'; +``` + +```sql+sqlite +select + id, + resource, + status, + analyzed_at +from + aws_accessanalyzer_finding +where + analyzed_at > date('now', '-30 day'); +```