Repository: turbot/steampipe-plugin-aws (language: go)

Issue:
Add table aws_rds_db_recommendation
**References**

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/describe-db-recommendations.html

Kindly implement a table which references this AWS CLI action (and associated web API).



Discussion / hints:

Hello @captainfalcon23, thank you for bringing this issue to our attention. Your engagement with Steampipe is greatly valued. I will take a look into it.
This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.
Not stale
Hello, @captainfalcon23, apologies for the delayed response on this matter.

We are currently utilizing the AWS RDS Go package version [v1.40.1](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/rds@v1.40.1), which unfortunately does not include support for the [DescribeDBRecommendations](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBRecommendations.html) API.

Support for this API was introduced in version [v1.65.0](https://github.com/aws/aws-sdk-go-v2/blob/main/service/rds/CHANGELOG.md#v1650-2023-12-19). However, it's important to note that there have been breaking changes since version [v1.49.0](https://github.com/aws/aws-sdk-go-v2/blob/main/service/rds/CHANGELOG.md#v1490-2023-07-31).

Our next step is to upgrade our plugin to utilize the latest version of the AWS Go SDK. After that, we'll revisit this issue.

Thanks for your understanding and patience!
All good @ParthaI, hopefully it gets revisited/reopened once that work is done.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebf99e77c..652423c55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## v0.143.0 [2024-07-05]
+
+_What's new?_
+
+- New tables added
+  - [aws_rds_db_recommendation](https://hub.steampipe.io/plugins/turbot/aws/tables/aws_rds_db_recommendation) ([#2238](https://github.com/turbot/steampipe-plugin-aws/pull/2238))
+
+_Bug fixes_
+
+- Fixed the caching issue in `aws_organizations_account` table. ([#2236](https://github.com/turbot/steampipe-plugin-aws/pull/2236))
+- Fixed typo (missing comma) in an example query of `aws_health_affected_entity` table doc. ([#2237](https://github.com/turbot/steampipe-plugin-aws/pull/2237)) (Thanks [@tieum](https://github.com/tieum) for the contribution!)
+
+## v0.142.0 [2024-07-04]
+
+_Enhancements_
+
+- Added 16 new columns to the `aws_lambda_version` table. ([#2229](https://github.com/turbot/steampipe-plugin-aws/pull/2229))
+
+_Bug fixes_
+
+- Fixed the export tool of the plugin to return a non-zero error code instead of 0 whenever an error occurred. ([#79](https://github.com/turbot/steampipe-export/pull/79))
+
 ## v0.141.0 [2024-07-01]
 
 _Bug fixes_
diff --git a/aws/plugin.go b/aws/plugin.go
index 04731681a..6c69f1260 100644
--- a/aws/plugin.go
+++ b/aws/plugin.go
@@ -416,6 +416,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
 			"aws_rds_db_option_group":                                      tableAwsRDSDBOptionGroup(ctx),
 			"aws_rds_db_parameter_group":                                   tableAwsRDSDBParameterGroup(ctx),
 			"aws_rds_db_proxy":                                             tableAwsRDSDBProxy(ctx),
+			"aws_rds_db_recommendation":                                    tableAwsRDSDBRecommendation(ctx),
 			"aws_rds_db_snapshot":                                          tableAwsRDSDBSnapshot(ctx),
 			"aws_rds_db_subnet_group":                                      tableAwsRDSDBSubnetGroup(ctx),
 			"aws_rds_reserved_db_instance":                                 tableAwsRDSReservedDBInstance(ctx),
diff --git a/aws/service.go b/aws/service.go
index 14626889f..3d47ef204 100644
--- a/aws/service.go
+++ b/aws/service.go
@@ -1149,6 +1149,37 @@ func RDSClient(ctx context.Context, d *plugin.QueryData) (*rds.Client, error) {
 	return rds.NewFromConfig(*cfg), nil
 }
 
+func RDSDBRecommendationClient(ctx context.Context, d *plugin.QueryData) (*rds.Client, error) {
+	// RDS DB Recommendation has the same endpoint information in the SDK as RDS, but
+	// is actually available in less regions. We have to manually remove them
+	// here.
+	// Source - https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UserRecommendationsView.html
+	excludeRegions := []string{
+		"af-south-1",     // Africa (Cape Town)
+		"ap-east-1",      // Asia Pacific (Hong Kong)
+		"ap-northeast-3", // Asia Pacific (Osaka)
+		"ap-southeast-3", // Asia Pacific (Jakarta)
+		"eu-north-1",     // Europe (Stockholm)
+		"eu-south-1",     // Europe (Milan)
+		"me-central-1",   // Middle East (UAE)
+		"me-south-1",     // Middle East (Bahrain)
+		"us-gov-west-1",  // AWS GovCloud (US-West)
+		"us-gov-east-1",  // AWS GovCloud (US-East)
+		"cn-north-1",     // China (Beijing)
+		"cn-northwest-1", // China (Ningxia)
+	}
+	excludeRegions = append(excludeRegions, awsChinaRegions()...)
+	excludeRegions = append(excludeRegions, awsUsGovRegions()...)
+	cfg, err := getClientForQuerySupportedRegionWithExclusions(ctx, d, rdsEndpoint.EndpointsID, excludeRegions)
+	if err != nil {
+		return nil, err
+	}
+	if cfg == nil {
+		return nil, nil
+	}
+	return rds.NewFromConfig(*cfg), nil
+}
+
 func RDSDBProxyClient(ctx context.Context, d *plugin.QueryData) (*rds.Client, error) {
 	// RDS DB Proxy has the same endpoint information in the SDK as RDS, but
 	// is actually available in less regions. We have to manually remove them
diff --git a/aws/table_aws_organizations_account.go b/aws/table_aws_organizations_account.go
index cb9cfa3cb..c26b5f279 100644
--- a/aws/table_aws_organizations_account.go
+++ b/aws/table_aws_organizations_account.go
@@ -2,7 +2,9 @@ package aws
 
 import (
 	"context"
+	"strings"
 
+	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/organizations"
 	"github.com/aws/aws-sdk-go-v2/service/organizations/types"
 
@@ -11,6 +13,23 @@ import (
 	"github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
 )
 
+// Table behavior:
+// 1. Uses `aws_organizations_root` as the parent hydrate function.
+// 2. Avoids using the "ListAccounts" API call to retrieve all accounts
+//    within the organization.
+// 3. If `parent_id` is specified in the query parameter,
+//    lists the accounts for that specific parent.
+// 4. If `parent_id` is not specified, lists the accounts under all
+//    Organizational Units (OUs), including the Root ID.
+
+// Reason for this table design:
+// 1. To address the issue described here:
+//    https://github.com/turbot/steampipe-plugin-aws/issues/2235
+// 2. As per the query plan for the query:
+//    select id, parent_id, title from aws_organizations_account WHERE parent_id IN (select id from aws_organizations_organizational_unit WHERE parent_id='ou-wxnb-wofu2g1q') limit 2
+//    Postgres does not provide the `parent_id` value with our previous design where the `parent_id` column value was populated from the query parameter.
+//    This resulted in an empty result set due to Steampipe's level filtration on the `parent_id` value ("FromQual()" returns a null value) and the value passed in the query parameter mismatch.
+
 func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table {
 	return &plugin.Table{
 		Name:        "aws_organizations_account",
@@ -24,7 +43,8 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table {
 			Tags:    map[string]string{"service": "organizations", "action": "DescribeAccount"},
 		},
 		List: &plugin.ListConfig{
-			Hydrate: listOrganizationsAccounts,
+			ParentHydrate: listOrganizationsRoots,
+			Hydrate:       listOrganizationsAccounts,
 			KeyColumns: plugin.KeyColumnSlice{
 				{Name: "parent_id", Require: plugin.Optional, CacheMatch: "exact"},
 			},
@@ -52,7 +72,6 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table {
 				Name:        "parent_id",
 				Description: "The unique identifier (ID) for the parent root or organization unit (OU) whose accounts you want to list.",
 				Type:        proto.ColumnType_STRING,
-				Transform:   transform.FromQual("parent_id"),
 			},
 			{
 				Name:        "arn",
@@ -110,10 +129,16 @@ func tableAwsOrganizationsAccount(_ context.Context) *plugin.Table {
 	}
 }
 
+type OrgAccount struct {
+	types.Account
+	ParentId *string
+}
+
 //// LIST FUNCTION
 
-func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
-	parentId := d.EqualsQualString("parent_id")
+func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
+
+	parentId := *h.Item.(types.Root).Id
 
 	// Get Client
 	svc, err := OrganizationClient(ctx, d)
@@ -129,63 +154,61 @@ func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plug
 	if d.QueryContext.Limit != nil {
 		limit := int32(*d.QueryContext.Limit)
 		if limit < maxItems {
-			if limit < 1 {
-				maxItems = int32(1)
-			} else {
-				maxItems = int32(limit)
-			}
+			maxItems = int32(limit)
 		}
 	}
 
+	var parents []types.OrganizationalUnit
+
 	// Lists the accounts in an organization that are contained by the specified target root or organizational unit (OU).
 	// If you specify the root, you get a list of all the accounts that aren't in any OU.
 	// If you specify an OU, you get a list of all the accounts in only that OU and not in any child OUs.
-	if parentId != "" {
-		op, err := listOrganizationsAccountsForParent(ctx, d, svc, maxItems, &organizations.ListAccountsForParentInput{
-			ParentId:   &parentId,
-			MaxResults: &maxItems,
-		})
+	if d.EqualsQualString("parent_id") != "" {
+		parents = []types.OrganizationalUnit{{
+			Id: aws.String(d.EqualsQualString("parent_id")),
+		}}
+	}
+
+	// Restrict listing of parents when "parent_id" is provided.
+	if len(parents) == 0 {
+		// Call the recursive function to list all nested OUs
+		rootPath := parentId
+		res, err := listAllOusByParent(ctx, d, svc, parentId, maxItems, rootPath)
 		if err != nil {
 			return nil, err
 		}
 
-		accounts := op.([]types.Account)
-		for _, account := range accounts {
-			d.StreamListItem(ctx, account)
-
-			// Context may get cancelled due to manual cancellation or if the limit has been reached
-			if d.RowsRemaining(ctx) == 0 {
-				return nil, nil
-			}
-		}
-
-		return nil, nil
+		parents = append(res, types.OrganizationalUnit{Id: aws.String(rootPath)})
 	}
 
-	params := &organizations.ListAccountsInput{}
-
-	params.MaxResults = &maxItems
-	paginator := organizations.NewListAccountsPaginator(svc, params, func(o *organizations.ListAccountsPaginatorOptions) {
-		o.Limit = maxItems
-		o.StopOnDuplicateToken = true
-	})
+	// Iterate the API call based on the parent entities
+	for _, p := range parents {
+		params := &organizations.ListAccountsForParentInput{
+			ParentId:   p.Id,
+			MaxResults: &maxItems,
+		}
+		paginator := organizations.NewListAccountsForParentPaginator(svc, params, func(o *organizations.ListAccountsForParentPaginatorOptions) {
+			o.Limit = maxItems
+			o.StopOnDuplicateToken = true
+		})
 
-	for paginator.HasMorePages() {
-		// apply rate limiting
-		d.WaitForListRateLimit(ctx)
+		for paginator.HasMorePages() {
+			// apply rate limiting
+			d.WaitForListRateLimit(ctx)
 
-		output, err := paginator.NextPage(ctx)
-		if err != nil {
-			plugin.Logger(ctx).Error("aws_organizations_account.listOrganizationsAccounts", "api_error", err)
-			return nil, err
-		}
+			output, err := paginator.NextPage(ctx)
+			if err != nil {
+				plugin.Logger(ctx).Error("aws_organizations_account.listOrganizationsAccounts", "api_error", err)
+				return nil, err
+			}
 
-		for _, account := range output.Accounts {
-			d.StreamListItem(ctx, account)
+			for _, account := range output.Accounts {
+				d.StreamListItem(ctx, OrgAccount{account, p.Id})
 
-			// Context may get cancelled due to manual cancellation or if the limit has been reached
-			if d.RowsRemaining(ctx) == 0 {
-				return nil, nil
+				// Context may get cancelled due to manual cancellation or if the limit has been reached
+				if d.RowsRemaining(ctx) == 0 {
+					return nil, nil
+				}
 			}
 		}
 	}
@@ -193,28 +216,6 @@ func listOrganizationsAccounts(ctx context.Context, d *plugin.QueryData, _ *plug
 	return nil, nil
 }
 
-func listOrganizationsAccountsForParent(ctx context.Context, d *plugin.QueryData, svc *organizations.Client, maxItems int32, params *organizations.ListAccountsForParentInput) (interface{}, error) {
-	paginator := organizations.NewListAccountsForParentPaginator(svc, params, func(o *organizations.ListAccountsForParentPaginatorOptions) {
-		o.Limit = maxItems
-		o.StopOnDuplicateToken = true
-	})
-
-	var accounts []types.Account
-	for paginator.HasMorePages() {
-		// apply rate limiting
-		d.WaitForListRateLimit(ctx)
-
-		output, err := paginator.NextPage(ctx)
-		if err != nil {
-			plugin.Logger(ctx).Error("aws_organizations_account.listOrganizationsAccountsForParent", "api_error", err)
-			return nil, err
-		}
-
-		accounts = append(accounts, output.Accounts...)
-	}
-	return accounts, nil
-}
-
 //// HYDRATE FUNCTIONS
 
 func getOrganizationsAccount(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
@@ -237,12 +238,75 @@ func getOrganizationsAccount(ctx context.Context, d *plugin.QueryData, _ *plugin
 		return nil, err
 	}
 
-	return *op.Account, nil
+	// The "parent_id" column value will not be populated by the GET API call because its response does not include parent ID information.
+	// We can populate this value by making a separate hydrated function.
+	// However, to avoid unnecessary API calls for all rows.
+	// If our LIST API call is executed, it will correctly populate the parent_id column value.
+	// In the case of a GET API call, the Parent ID will not be available. Therefore, we make an additional API call to populate the parent_id column value when only the GET configuration is used.
+	parent, err := getParentForAccount(ctx, svc, accountId)
+	if err != nil {
+		plugin.Logger(ctx).Error("aws_organizations_account.getParentForAccount", "api_error", err)
+		return nil, err
+	}
+
+	return OrgAccount{*op.Account, parent}, nil
+}
+
+func getParentForAccount(ctx context.Context, client *organizations.Client, accountId string) (*string, error) {
+	// Pagination is not needed here because an account will always have a single parent.
+	parent, err := client.ListParents(ctx, &organizations.ListParentsInput{ChildId: &accountId})
+	if err != nil {
+		return nil, err
+	}
+	if len(parent.Parents) > 0 {
+		return parent.Parents[0].Id, nil
+	}
+	return nil, nil
+}
+
+// List all nested Organizational Units (OUs)
+// We cannot use the existing table "aws_organizations_organizational_unit" as the parent hydrate because it already has a parent hydrate, and Steampipe does not support parent hydrate chaining.
+func listAllOusByParent(ctx context.Context, d *plugin.QueryData, svc *organizations.Client, parentId string, maxItems int32, currentPath string) ([]types.OrganizationalUnit, error) {
+	params := &organizations.ListOrganizationalUnitsForParentInput{
+		ParentId:   aws.String(parentId),
+		MaxResults: &maxItems,
+	}
+
+	var units []types.OrganizationalUnit
+	paginator := organizations.NewListOrganizationalUnitsForParentPaginator(svc, params, func(o *organizations.ListOrganizationalUnitsForParentPaginatorOptions) {
+		o.Limit = maxItems
+		o.StopOnDuplicateToken = true
+	})
+
+	for paginator.HasMorePages() {
+		// apply rate limiting
+		output, err := paginator.NextPage(ctx)
+		if err != nil {
+			plugin.Logger(ctx).Error("aws_organizations_account.listAllOusByParent", "api_error", err)
+			return nil, err
+		}
+
+		for _, unit := range output.OrganizationalUnits {
+			ouPath := strings.Replace(currentPath, "-", "_", -1) + "." + strings.Replace(*unit.Id, "-", "_", -1)
+			units = append(units, unit)
+
+			// Recursively list units for this child
+			childUnits, err := listAllOusByParent(ctx, d, svc, *unit.Id, maxItems, ouPath)
+			if err != nil {
+				plugin.Logger(ctx).Error("aws_organizations_account.listAllOusByParent", "recursive_api_error", err)
+				return nil, err
+			}
+			// Append child units to the main units slice
+			units = append(units, childUnits...)
+		}
+	}
+
+	return units, nil
 }
 
 func getOrganizationsAccountTags(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
 
-	resourceId := *h.Item.(types.Account).Id
+	resourceId := *h.Item.(OrgAccount).Id
 
 	tags, err := getOrganizationsResourceTags(ctx, d, resourceId)
 	return tags, err
diff --git a/aws/table_aws_rds_db_recomendation.go b/aws/table_aws_rds_db_recomendation.go
new file mode 100644
index 000000000..81cc188ab
--- /dev/null
+++ b/aws/table_aws_rds_db_recomendation.go
@@ -0,0 +1,260 @@
+package aws
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/rds"
+	"github.com/aws/aws-sdk-go-v2/service/rds/types"
+
+	rdsv1 "github.com/aws/aws-sdk-go/service/rds"
+
+	"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
+
+func tableAwsRDSDBRecommendation(_ context.Context) *plugin.Table {
+	return &plugin.Table{
+		Name:        "aws_rds_db_recommendation",
+		Description: "AWS RDS DB Recommendation",
+		List: &plugin.ListConfig{
+			Hydrate: listRDSDBRecommendations,
+			KeyColumns: plugin.KeyColumnSlice{
+				{Name: "recommendation_id", Require: plugin.Optional},
+				{Name: "status", Require: plugin.Optional},
+				{Name: "severity", Require: plugin.Optional},
+				{Name: "type_id", Require: plugin.Optional},
+				{Name: "updated_time", Require: plugin.Optional, Operators: []string{">=", "<="}},
+			},
+			Tags: map[string]string{"service": "rds", "action": "DescribeDBRecommendations"},
+		},
+		GetMatrixItemFunc: SupportedRegionMatrix(rdsv1.EndpointsID),
+		Columns: awsRegionalColumns([]*plugin.Column{
+			{
+				Name:        "recommendation_id",
+				Description: "The unique identifier for the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "resource_arn",
+				Description: "The Amazon Resource Name (ARN) of the RDS resource associated with the recommendation.",
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("ResourceArn"),
+			},
+			{
+				Name:        "category",
+				Description: "The category of the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "status",
+				Description: "The current status of the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "created_time",
+				Description: "The time when the recommendation was created.",
+				Type:        proto.ColumnType_TIMESTAMP,
+			},
+			{
+				Name:        "updated_time",
+				Description: "The time when the recommendation was last updated.",
+				Type:        proto.ColumnType_TIMESTAMP,
+				Transform:   transform.FromField("UpdatedTime").Transform(transform.NullIfZeroValue),
+			},
+			{
+				Name:        "description",
+				Description: "A detailed description of the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "detection",
+				Description: "A short description of the issue identified for this recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "impact",
+				Description: "A short description that explains the possible impact of an issue.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "reason",
+				Description: "The reason why this recommendation was created.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "recommendation",
+				Description: "A short description of the recommendation to resolve an issue.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "severity",
+				Description: "The severity level of the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "source",
+				Description: "The AWS service that generated the recommendations.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "type_detection",
+				Description: "A short description of the recommendation type.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "type_id",
+				Description: "A value that indicates the type of recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "type_recommendation",
+				Description: "A short description that summarizes the recommendation to fix all the issues of the recommendation type.",
+				Type:        proto.ColumnType_STRING,
+			},
+			{
+				Name:        "additional_info",
+				Description: "Additional information about the recommendation.",
+				Type:        proto.ColumnType_STRING,
+			},
+
+			// JSON fields
+			{
+				Name:        "issue_details",
+				Description: "Details of the issue that caused the recommendation.",
+				Type:        proto.ColumnType_JSON,
+			},
+			{
+				Name:        "links",
+				Description: "A link to documentation that provides additional information about the recommendation.",
+				Type:        proto.ColumnType_JSON,
+			},
+			{
+				Name:        "recommended_actions",
+				Description: "A list of recommended actions.",
+				Type:        proto.ColumnType_JSON,
+			},
+
+			// Steampipe standard columns
+			{
+				Name:        "title",
+				Description: resourceInterfaceDescription("title"),
+				Type:        proto.ColumnType_STRING,
+				Transform:   transform.FromField("RecommendationId"),
+			},
+		}),
+	}
+}
+
+//// LIST FUNCTION
+
+func listRDSDBRecommendations(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
+
+	// Create Session
+	svc, err := RDSDBRecommendationClient(ctx, d)
+	if err != nil {
+		plugin.Logger(ctx).Error("aws_rds_db_recommendation.listRDSDBRecommendations", "connection_error", err)
+		return nil, err
+	}
+	// Unsupported region check
+	if svc == nil {
+		return nil, nil
+	}
+
+	// Limiting the results
+	maxLimit := int32(50)
+	if d.QueryContext.Limit != nil {
+		limit := int32(*d.QueryContext.Limit)
+		if limit < maxLimit {
+			maxLimit = limit
+		}
+	}
+
+	input := &rds.DescribeDBRecommendationsInput{
+		MaxRecords: aws.Int32(maxLimit),
+	}
+
+	if d.Quals["updated_time"] != nil {
+		for _, q := range d.Quals["updated_time"].Quals {
+			value := q.Value.GetTimestampValue().AsTime()
+			if q.Operator == ">=" {
+				input.LastUpdatedAfter = &value
+			}
+			if q.Operator == "<=" {
+				input.LastUpdatedBefore = &value
+			}
+		}
+	}
+
+	// Build input  filter parameter
+	filters := buildRdsDBRecommendationFilter(d.Quals)
+
+	if len(filters) > 0 {
+		input.Filters = filters
+	}
+
+	paginator := rds.NewDescribeDBRecommendationsPaginator(svc, input, func(o *rds.DescribeDBRecommendationsPaginatorOptions) {
+		o.Limit = maxLimit
+		o.StopOnDuplicateToken = true
+	})
+
+	// List call
+	for paginator.HasMorePages() {
+		// apply rate limiting
+		d.WaitForListRateLimit(ctx)
+
+		output, err := paginator.NextPage(ctx)
+		if err != nil {
+			plugin.Logger(ctx).Error("aws_rds_db_recommendation.listRDSDBRecommendations", "api_error", err)
+			return nil, err
+		}
+
+		for _, recommendation := range output.DBRecommendations {
+			d.StreamListItem(ctx, recommendation)
+
+			// Context can be cancelled due to manual cancellation or the limit has been hit
+			if d.RowsRemaining(ctx) == 0 {
+				return nil, nil
+			}
+		}
+	}
+
+	return nil, err
+}
+
+//// UTILITY FUNCTIONS
+
+// Build RDS DB Recommendation list call input filter
+func buildRdsDBRecommendationFilter(quals plugin.KeyColumnQualMap) []types.Filter {
+	filters := make([]types.Filter, 0)
+	// We have intentionally avoided using the other filter parameters:
+	// - dbi-resource-id
+	// - cluster-resource-id
+	// - pg-arn
+	// - cluster-pg-arn
+	// because they are not part of the API response.
+	filterQuals := map[string]string{
+		"recommendation_id": "recommendation-id",
+		"status":            "status",
+		"severity":          "severity",
+		"type_id":           "type-id",
+	}
+
+	for columnName, filterName := range filterQuals {
+		if quals[columnName] != nil {
+			filter := types.Filter{
+				Name: aws.String(filterName),
+			}
+			value := getQualsValueByColumn(quals, columnName, "string")
+			val, ok := value.(string)
+			if ok {
+				filter.Values = []string{val}
+			}
+			filters = append(filters, filter)
+		}
+	}
+	return filters
+}
diff --git a/docs/tables/aws_health_affected_entity.md b/docs/tables/aws_health_affected_entity.md
index 9fc81b1c6..7a3a1c2b9 100644
--- a/docs/tables/aws_health_affected_entity.md
+++ b/docs/tables/aws_health_affected_entity.md
@@ -80,7 +80,7 @@ select
   e.entity_url,
   e.event_arn,
   v.event_type_category,
-  v.event_type_code
+  v.event_type_code,
   v.service
 from
   aws_health_affected_entity as e,
diff --git a/docs/tables/aws_rds_db_recommendation.md b/docs/tables/aws_rds_db_recommendation.md
new file mode 100644
index 000000000..1a99b992e
--- /dev/null
+++ b/docs/tables/aws_rds_db_recommendation.md
@@ -0,0 +1,213 @@
+---
+title: "Steampipe Table: aws_rds_db_recommendation - Query AWS RDS DB Recommendations using SQL"
+description: "Allows users to query AWS RDS DB Recommendations and retrieve valuable information about each recommendation's status, impact, and suggested actions."
+---
+
+# Table: aws_rds_db_recommendation - Query AWS RDS DB Recommendations using SQL
+
+The AWS RDS DB Recommendation table provides insights and suggestions for improving the configuration and performance of your RDS DB instances and clusters. This table allows you, as a DevOps engineer, to query specific recommendations, understand their impact, and take appropriate actions to optimize your database setup.
+
+## Table Usage Guide
+
+The `aws_rds_db_recommendation` table in Steampipe offers detailed information about recommendations generated by AWS for your RDS DB instances and clusters. You can query this table to gather insights on issues, suggested improvements, and the status of each recommendation. The schema includes various attributes such as recommendation ID, description, severity, creation and update times, and links to further documentation.
+
+## Examples
+
+### List all recommendations with high severity
+Identify all recommendations with a high severity level to prioritize actions that address the most critical issues in your RDS setup.
+
+```sql+postgres
+select
+  recommendation_id,
+  title,
+  severity,
+  description,
+  created_time
+from
+  aws_rds_db_recommendation
+where
+  severity = 'high';
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  title,
+  severity,
+  description,
+  created_time
+from
+  aws_rds_db_recommendation
+where
+  severity = 'high';
+```
+
+### List all recommendations created in the last 30 days
+Find recommendations that were created recently to stay updated on new issues and suggested improvements.
+
+```sql+postgres
+select
+  recommendation_id,
+  title,
+  created_time,
+  description
+from
+  aws_rds_db_recommendation
+where
+  created_time >= now() - interval '30 days';
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  title,
+  created_time,
+  description
+from
+  aws_rds_db_recommendation
+where
+  created_time >= datetime('now', '-30 days');
+```
+
+### List recommendations with their recommended actions
+Get details of recommendations along with the actions suggested by AWS to resolve the identified issues.
+
+```sql+postgres
+select
+  recommendation_id,
+  title,
+  recommendation,
+  recommended_actions
+from
+  aws_rds_db_recommendation;
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  title,
+  recommendation,
+  recommended_actions
+from
+  aws_rds_db_recommendation;
+```
+
+### List recommendations by the order of impact
+Understand the potential impact of each recommendation to prioritize actions based on the severity of the issues.
+
+```sql+postgres
+select
+  recommendation_id,
+  title,
+  impact,
+  description
+from
+  aws_rds_db_recommendation
+order by
+  impact desc;
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  title,
+  impact,
+  description
+from
+  aws_rds_db_recommendation
+order by
+  impact desc;
+```
+
+### List recommendations with unresolved status
+Identify recommendations that are still unresolved to track pending issues that need attention.
+
+```sql+postgres
+select
+  recommendation_id,
+  title,
+  status,
+  updated_time
+from
+  aws_rds_db_recommendation
+where
+  status != 'resolved';
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  title,
+  status,
+  updated_time
+from
+  aws_rds_db_recommendation
+where
+  status != 'resolved';
+```
+
+### Get the issue details that triggered the recommendation
+Analyze performance issues for the recommendation.
+
+```sql+postgres
+select
+  recommendation_id,
+  issue_details -> 'PerformanceIssueDetails' ->> 'Analysis' as analysis,
+  issue_details -> 'PerformanceIssueDetails' ->> 'EndTime' as end_time,
+  issue_details -> 'PerformanceIssueDetails' ->> 'StartTime' as start_time,
+  issue_details -> 'PerformanceIssueDetails' -> 'Metrics' as metrics
+from
+  aws_rds_db_recommendation
+where
+  issue_details is not null;
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  json_extract(issue_details, '$.PerformanceIssueDetails.Analysis') as analysis,
+  json_extract(issue_details, '$.PerformanceIssueDetails.EndTime') as end_time,
+  json_extract(issue_details, '$.PerformanceIssueDetails.StartTime') as start_time,
+  json_extract(issue_details, '$.PerformanceIssueDetails.Metrics') as metrics
+from
+  aws_rds_db_recommendation
+where
+  issue_details is not null;
+```
+
+### List the recommended action details for a particular recommendation
+View all relevant information clearly and organized, facilitating better planning and decision-making for database maintenance and optimization.
+
+```sql+postgres
+select
+  recommendation_id,
+  action ->> 'Title' as title,
+  action ->> 'Status' as status,
+  action ->> 'ActionId' as action_id,
+  action ->> 'Operation' as operation,
+  action -> 'ApplyModes' as apply_modes,
+  action -> 'Parameters' as parameters,
+  action ->> 'Description' as description,
+  action ->> 'IssueDetails' as issue_details,
+  action -> 'ContextAttributes' as context_attributes
+from
+  aws_rds_db_recommendation,
+  jsonb_array_elements(recommended_actions) as action;
+```
+
+```sql+sqlite
+select
+  recommendation_id,
+  json_extract(action.value, '$.Title') as title,
+  json_extract(action.value, '$.Status') as status,
+  json_extract(action.value, '$.ActionId') as action_id,
+  json_extract(action.value, '$.Operation') as operation,
+  json_extract(action.value, '$.ApplyModes') as apply_modes,
+  json_extract(action.value, '$.Parameters') as parameters,
+  json_extract(action.value, '$.Description') as description,
+  json_extract(action.value, '$.IssueDetails') as issue_details,
+  json_extract(action.value, '$.ContextAttributes') as context_attributes
+from
+  aws_rds_db_recommendation,
+  json_each(recommended_actions) as action;
+```
diff --git a/scripts/generate_parliament_iam_permissions/requirements.txt b/scripts/generate_parliament_iam_permissions/requirements.txt
index fef96107e..edc67a93e 100644
--- a/scripts/generate_parliament_iam_permissions/requirements.txt
+++ b/scripts/generate_parliament_iam_permissions/requirements.txt
@@ -1,7 +1,7 @@
 beautifulsoup4==4.9.3
 certifi==2020.12.5
 chardet==4.0.0
-idna==2.10
+idna==3.7
 requests==2.32.2
 soupsieve==2.1
 urllib3==1.26.19


Test patch: