diff --git a/client/client.go b/client/client.go index b1c0db087..07a98688b 100644 --- a/client/client.go +++ b/client/client.go @@ -52,6 +52,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" s3control "github.com/aws/aws-sdk-go-v2/service/s3control" "github.com/aws/aws-sdk-go-v2/service/sagemaker" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/ssm" @@ -132,6 +133,7 @@ type Services struct { S3Manager S3ManagerClient SSM SSMClient SageMaker SageMakerClient + SecretsManager SecretsManagerClient SQS SQSClient Apigateway ApigatewayClient Apigatewayv2 Apigatewayv2Client @@ -397,6 +399,7 @@ func initServices(region string, c aws.Config) Services { S3Control: s3control.NewFromConfig(awsCfg), S3Manager: newS3ManagerFromConfig(awsCfg), SageMaker: sagemaker.NewFromConfig(awsCfg), + SecretsManager: secretsmanager.NewFromConfig(awsCfg), SNS: sns.NewFromConfig(awsCfg), SSM: ssm.NewFromConfig(awsCfg), SQS: sqs.NewFromConfig(awsCfg), diff --git a/client/mocks/mock_secrets_manager.go b/client/mocks/mock_secrets_manager.go new file mode 100644 index 000000000..ad61b5c1e --- /dev/null +++ b/client/mocks/mock_secrets_manager.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/cloudquery/cq-provider-aws/client (interfaces: SecretsManagerClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + secretsmanager "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + gomock "github.com/golang/mock/gomock" +) + +// MockSecretsManagerClient is a mock of SecretsManagerClient interface. +type MockSecretsManagerClient struct { + ctrl *gomock.Controller + recorder *MockSecretsManagerClientMockRecorder +} + +// MockSecretsManagerClientMockRecorder is the mock recorder for MockSecretsManagerClient. +type MockSecretsManagerClientMockRecorder struct { + mock *MockSecretsManagerClient +} + +// NewMockSecretsManagerClient creates a new mock instance. +func NewMockSecretsManagerClient(ctrl *gomock.Controller) *MockSecretsManagerClient { + mock := &MockSecretsManagerClient{ctrl: ctrl} + mock.recorder = &MockSecretsManagerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSecretsManagerClient) EXPECT() *MockSecretsManagerClientMockRecorder { + return m.recorder +} + +// DescribeSecret mocks base method. +func (m *MockSecretsManagerClient) DescribeSecret(arg0 context.Context, arg1 *secretsmanager.DescribeSecretInput, arg2 ...func(*secretsmanager.Options)) (*secretsmanager.DescribeSecretOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeSecret", varargs...) + ret0, _ := ret[0].(*secretsmanager.DescribeSecretOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeSecret indicates an expected call of DescribeSecret. +func (mr *MockSecretsManagerClientMockRecorder) DescribeSecret(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecret", reflect.TypeOf((*MockSecretsManagerClient)(nil).DescribeSecret), varargs...) +} + +// GetResourcePolicy mocks base method. +func (m *MockSecretsManagerClient) GetResourcePolicy(arg0 context.Context, arg1 *secretsmanager.GetResourcePolicyInput, arg2 ...func(*secretsmanager.Options)) (*secretsmanager.GetResourcePolicyOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetResourcePolicy", varargs...) + ret0, _ := ret[0].(*secretsmanager.GetResourcePolicyOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourcePolicy indicates an expected call of GetResourcePolicy. +func (mr *MockSecretsManagerClientMockRecorder) GetResourcePolicy(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourcePolicy", reflect.TypeOf((*MockSecretsManagerClient)(nil).GetResourcePolicy), varargs...) +} + +// ListSecrets mocks base method. +func (m *MockSecretsManagerClient) ListSecrets(arg0 context.Context, arg1 *secretsmanager.ListSecretsInput, arg2 ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListSecrets", varargs...) + ret0, _ := ret[0].(*secretsmanager.ListSecretsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecrets indicates an expected call of ListSecrets. +func (mr *MockSecretsManagerClientMockRecorder) ListSecrets(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockSecretsManagerClient)(nil).ListSecrets), varargs...) +} diff --git a/client/services.go b/client/services.go index eeddc43f2..b7084e992 100644 --- a/client/services.go +++ b/client/services.go @@ -42,6 +42,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" s3control "github.com/aws/aws-sdk-go-v2/service/s3control" "github.com/aws/aws-sdk-go-v2/service/sagemaker" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/ssm" @@ -502,3 +503,10 @@ type CodebuildClient interface { BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) } + +//go:generate mockgen -package=mocks -destination=./mocks/mock_secrets_manager.go . SecretsManagerClient +type SecretsManagerClient interface { + ListSecrets(ctx context.Context, params *secretsmanager.ListSecretsInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) + DescribeSecret(ctx context.Context, params *secretsmanager.DescribeSecretInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.DescribeSecretOutput, error) + GetResourcePolicy(ctx context.Context, params *secretsmanager.GetResourcePolicyInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetResourcePolicyOutput, error) +} diff --git a/docs/tables/aws_secretsmanager_secrets.md b/docs/tables/aws_secretsmanager_secrets.md new file mode 100644 index 000000000..f0463a49e --- /dev/null +++ b/docs/tables/aws_secretsmanager_secrets.md @@ -0,0 +1,26 @@ + +# Table: aws_secretsmanager_secrets +A structure that contains the details about a secret +## Columns +| Name | Type | Description | +| ------------- | ------------- | ----- | +|account_id|text|The AWS Account ID of the resource.| +|region|text|The AWS Region of the resource.| +|policy|jsonb|A JSON-formatted string that describes the permissions that are associated with the attached secret.| +|replication_status|jsonb|A replication object consisting of a RegionReplicationStatus object and includes a Region, KMSKeyId, status, and status message.| +|arn|text|The Amazon Resource Name (ARN) of the secret| +|created_date|timestamp without time zone|The date and time when a secret was created.| +|deleted_date|timestamp without time zone|The date and time the deletion of the secret occurred| +|description|text|The user-provided description of the secret.| +|kms_key_id|text|The ARN or alias of the Amazon Web Services KMS customer master key (CMK) used to encrypt the SecretString and SecretBinary fields in each version of the secret| +|last_accessed_date|timestamp without time zone|The last date that this secret was accessed| +|last_changed_date|timestamp without time zone|The last date and time that this secret was modified in any way.| +|last_rotated_date|timestamp without time zone|The most recent date and time that the Secrets Manager rotation process was successfully completed| +|name|text|The friendly name of the secret| +|owning_service|text|Returns the name of the service that created the secret.| +|primary_region|text|The Region where Secrets Manager originated the secret.| +|rotation_enabled|boolean|Indicates whether automatic, scheduled rotation is enabled for this secret.| +|rotation_lambda_arn|text|The ARN of an Amazon Web Services Lambda function invoked by Secrets Manager to rotate and expire the secret either automatically per the schedule or manually by a call to RotateSecret.| +|rotation_rules_automatically_after_days|bigint|Specifies the number of days between automatic scheduled rotations of the secret| +|secret_versions_to_stages|jsonb|A list of all of the currently assigned SecretVersionStage staging labels and the SecretVersionId attached to each one| +|tags|jsonb|The list of user-defined tags associated with the secret| diff --git a/go.mod b/go.mod index dd8cd389e..a4f467bb7 100644 --- a/go.mod +++ b/go.mod @@ -137,7 +137,7 @@ require ( ) require ( - github.com/aws/aws-sdk-go v1.17.7 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.10.1 github.com/aws/aws-sdk-go-v2/service/ssm v1.16.0 ) diff --git a/go.sum b/go.sum index 36ecdf1fc..e86ee0895 100644 --- a/go.sum +++ b/go.sum @@ -255,6 +255,8 @@ github.com/aws/aws-sdk-go-v2/service/s3control v1.14.1 h1:Nmcb6pxJtjJof+mmF9TJvy github.com/aws/aws-sdk-go-v2/service/s3control v1.14.1/go.mod h1:dTnxIRqR69JUZobQDUh47rlbYe8PzTd0k4o+gDkHeV4= github.com/aws/aws-sdk-go-v2/service/sagemaker v1.19.1 h1:cy6fUlP94vzD/0VUD3SWGUBfYrOr+zP+ChsTxUtZydQ= github.com/aws/aws-sdk-go-v2/service/sagemaker v1.19.1/go.mod h1:G9AcXDbGtZVA8XBdmpbVQv1lvmiuk4I9n2MQlp1FJ9k= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.10.1 h1:e0gg30cCKsNHV+WD17zbzipx5nYRrnb+4Y5wO5pap80= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.10.1/go.mod h1:vUIn46AiFjPEm4/ALXV1weLTEWu37mF6OfLMw5vxG2Q= github.com/aws/aws-sdk-go-v2/service/sns v1.1.2 h1:1U/FujyBEkNwrvANUcZFuVnAQqy0EAUEGToso5Dcijs= github.com/aws/aws-sdk-go-v2/service/sns v1.1.2/go.mod h1:/vvAGyo3/TG5CSrJQarIlwzjE6O/DjBIvJTRkpYkvwA= github.com/aws/aws-sdk-go-v2/service/sqs v1.9.1 h1:8m+6iuSldxMrVQbjHRcWPnUxdpD3RCPtacmFFNkR4Vw= diff --git a/resources/integration_tests/aws_secretsmanager_secrets_test.go b/resources/integration_tests/aws_secretsmanager_secrets_test.go new file mode 100644 index 000000000..d2b4de5a4 --- /dev/null +++ b/resources/integration_tests/aws_secretsmanager_secrets_test.go @@ -0,0 +1,28 @@ +package integration_tests + +import ( + "fmt" + "testing" + + "github.com/cloudquery/cq-provider-aws/resources" + providertest "github.com/cloudquery/cq-provider-sdk/provider/testing" +) + +func TestIntegrationSecretsmanagerSecrets(t *testing.T) { + awsTestIntegrationHelper(t, resources.SecretsmanagerSecrets(), []string{"aws_secretsmanager_secrets.tf"}, func(res *providertest.ResourceIntegrationTestData) providertest.ResourceIntegrationVerification { + return providertest.ResourceIntegrationVerification{ + Name: "aws_secretsmanager_secrets", + ExpectedValues: []providertest.ExpectedValue{{ + Count: 1, + Data: map[string]interface{}{ + "name": fmt.Sprintf("secretsmanager-secret-%s%s", res.Prefix, res.Suffix), + "tags": map[string]interface{}{ + "TestId": res.Suffix, + "Type": "integration_test", + "Name": fmt.Sprintf("secretsmanager-secret-%s%s", res.Prefix, res.Suffix), + }, + }, + }}, + } + }) +} diff --git a/resources/integration_tests/infra/aws_secretsmanager_secrets.tf b/resources/integration_tests/infra/aws_secretsmanager_secrets.tf new file mode 100644 index 000000000..9d58ba81c --- /dev/null +++ b/resources/integration_tests/infra/aws_secretsmanager_secrets.tf @@ -0,0 +1,209 @@ +resource "aws_secretsmanager_secret" "secretsmanager_secret" { + name = "secretsmanager-secret-${var.test_prefix}${var.test_suffix}" + tags = { + Name = "secretsmanager-secret-${var.test_prefix}${var.test_suffix}" + } +} + +data "aws_iam_policy_document" "secretsmanager_secret_iam_policy" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "secretsmanager_secret_iam_role" { + name = "secretsmanager-secret-iam-role-${var.test_prefix}${var.test_suffix}" + assume_role_policy = data.aws_iam_policy_document.secretsmanager_secret_iam_policy.json +} + +resource "aws_lambda_permission" "secretsmanager_secret_lambda_permission" { + function_name = aws_lambda_function.secretsmanager_secret_rotation_function.function_name + statement_id = "AllowExecutionSecretManager" + action = "lambda:InvokeFunction" + principal = "secretsmanager.amazonaws.com" +} + +resource "aws_lambda_function" "secretsmanager_secret_rotation_function" { + filename = data.archive_file.rotation_lambda_func_zip_inline.output_path + source_code_hash = data.archive_file.rotation_lambda_func_zip_inline.output_base64sha256 + function_name = "secretsmanager-secret-rotation-function-${var.test_prefix}${var.test_suffix}" + handler = "lambda_function.lambda_handler" + runtime = "python3.9" + timeout = 30 + role = aws_iam_role.secretsmanager_secret_iam_role.arn + + environment { + variables = { #https://docs.aws.amazon.com/general/latest/gr/rande.html#asm_region + SECRETS_MANAGER_ENDPOINT = "https://secretsmanager.${data.aws_region.current.name}.amazonaws.com" + } + } +} + +resource "aws_secretsmanager_secret_rotation" "secretsmanager_secret_rotation" { + secret_id = aws_secretsmanager_secret.secretsmanager_secret.id + rotation_lambda_arn = aws_lambda_function.secretsmanager_secret_rotation_function.arn + + rotation_rules { + automatically_after_days = 30 + } +} + +data "archive_file" "rotation_lambda_func_zip_inline" { + type = "zip" + output_path = "./tmp/rotation_lambda_zip_inline.zip" + source { + content = <<EOF +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import boto3 +import logging +import os + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def lambda_handler(event, context): + """Secrets Manager Rotation Template + This is a template for creating an AWS Secrets Manager rotation lambda + Args: + event (dict): Lambda dictionary of event parameters. These keys must include the following: + - SecretId: The secret ARN or identifier + - ClientRequestToken: The ClientRequestToken of the secret version + - Step: The rotation step (one of createSecret, setSecret, testSecret, or finishSecret) + context (LambdaContext): The Lambda runtime information + Raises: + ResourceNotFoundException: If the secret with the specified arn and stage does not exist + ValueError: If the secret is not properly configured for rotation + KeyError: If the event parameters do not contain the expected keys + """ + arn = event['SecretId'] + token = event['ClientRequestToken'] + step = event['Step'] + + # Setup the client + service_client = boto3.client('secretsmanager', endpoint_url=os.environ['SECRETS_MANAGER_ENDPOINT']) + + # Make sure the version is staged correctly + metadata = service_client.describe_secret(SecretId=arn) + if not metadata['RotationEnabled']: + logger.error("Secret %s is not enabled for rotation" % arn) + raise ValueError("Secret %s is not enabled for rotation" % arn) + versions = metadata['VersionIdsToStages'] + if token not in versions: + logger.error("Secret version %s has no stage for rotation of secret %s." % (token, arn)) + raise ValueError("Secret version %s has no stage for rotation of secret %s." % (token, arn)) + if "AWSCURRENT" in versions[token]: + logger.info("Secret version %s already set as AWSCURRENT for secret %s." % (token, arn)) + return + elif "AWSPENDING" not in versions[token]: + logger.error("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn)) + raise ValueError("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn)) + + if step == "createSecret": + create_secret(service_client, arn, token) + + elif step == "setSecret": + set_secret(service_client, arn, token) + + elif step == "testSecret": + test_secret(service_client, arn, token) + + elif step == "finishSecret": + finish_secret(service_client, arn, token) + + else: + raise ValueError("Invalid step parameter") + + +def create_secret(service_client, arn, token): + """Create the secret + This method first checks for the existence of a secret for the passed in token. If one does not exist, it will generate a + new secret and put it with the passed in token. + Args: + service_client (client): The secrets manager service client + arn (string): The secret ARN or other identifier + token (string): The ClientRequestToken associated with the secret version + Raises: + ResourceNotFoundException: If the secret with the specified arn and stage does not exist + """ + # Make sure the current secret exists + service_client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT") + + # Now try to get the secret version, if that fails, put a new secret + try: + service_client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING") + logger.info("createSecret: Successfully retrieved secret for %s." % arn) + except service_client.exceptions.ResourceNotFoundException: + # Get exclude characters from environment variable + exclude_characters = os.environ['EXCLUDE_CHARACTERS'] if 'EXCLUDE_CHARACTERS' in os.environ else '/@"\'\\' + # Generate a random password + passwd = service_client.get_random_password(ExcludeCharacters=exclude_characters) + + # Put the secret + service_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=passwd['RandomPassword'], VersionStages=['AWSPENDING']) + logger.info("createSecret: Successfully put secret for ARN %s and version %s." % (arn, token)) + + +def set_secret(service_client, arn, token): + """Set the secret + This method should set the AWSPENDING secret in the service that the secret belongs to. For example, if the secret is a database + credential, this method should take the value of the AWSPENDING secret and set the user's password to this value in the database. + Args: + service_client (client): The secrets manager service client + arn (string): The secret ARN or other identifier + token (string): The ClientRequestToken associated with the secret version + """ + # This is where the secret should be set in the service + raise NotImplementedError + + +def test_secret(service_client, arn, token): + """Test the secret + This method should validate that the AWSPENDING secret works in the service that the secret belongs to. For example, if the secret + is a database credential, this method should validate that the user can login with the password in AWSPENDING and that the user has + all of the expected permissions against the database. + Args: + service_client (client): The secrets manager service client + arn (string): The secret ARN or other identifier + token (string): The ClientRequestToken associated with the secret version + """ + # This is where the secret should be tested against the service + raise NotImplementedError + + +def finish_secret(service_client, arn, token): + """Finish the secret + This method finalizes the rotation process by marking the secret version passed in as the AWSCURRENT secret. + Args: + service_client (client): The secrets manager service client + arn (string): The secret ARN or other identifier + token (string): The ClientRequestToken associated with the secret version + Raises: + ResourceNotFoundException: If the secret with the specified arn does not exist + """ + # First describe the secret to get the current version + metadata = service_client.describe_secret(SecretId=arn) + current_version = None + for version in metadata["VersionIdsToStages"]: + if "AWSCURRENT" in metadata["VersionIdsToStages"][version]: + if version == token: + # The correct version is already marked as current, return + logger.info("finishSecret: Version %s already marked as AWSCURRENT for %s" % (version, arn)) + return + current_version = version + break + + # Finalize by staging the secret version current + service_client.update_secret_version_stage(SecretId=arn, VersionStage="AWSCURRENT", MoveToVersionId=token, RemoveFromVersionId=current_version) + logger.info("finishSecret: Successfully set AWSCURRENT stage to version %s for secret %s." % (token, arn)) +EOF + filename = "lambda_function.py" + } +} \ No newline at end of file diff --git a/resources/provider.go b/resources/provider.go index 6ce9dc774..1bad6b5b5 100644 --- a/resources/provider.go +++ b/resources/provider.go @@ -119,6 +119,7 @@ func Provider() *provider.Provider { "sagemaker.models": SagemakerModels(), "sagemaker.endpoint_configurations": SagemakerEndpointConfigurations(), "sagemaker.training_jobs": SagemakerTrainingJobs(), + "secretsmanager.secrets": SecretsmanagerSecrets(), "sns.subscriptions": SnsSubscriptions(), "sns.topics": SnsTopics(), "sqs.queues": SQSQueues(), diff --git a/resources/secretsmanager_secrets.go b/resources/secretsmanager_secrets.go new file mode 100644 index 000000000..8a5ae4dc0 --- /dev/null +++ b/resources/secretsmanager_secrets.go @@ -0,0 +1,247 @@ +package resources + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/cloudquery/cq-provider-aws/client" + "github.com/cloudquery/cq-provider-sdk/provider/schema" +) + +func SecretsmanagerSecrets() *schema.Table { + return &schema.Table{ + Name: "aws_secretsmanager_secrets", + Description: "A structure that contains the details about a secret", + Resolver: fetchSecretsmanagerSecrets, + Multiplex: client.AccountRegionMultiplex, + IgnoreError: client.IgnoreAccessDeniedServiceDisabled, + DeleteFilter: client.DeleteAccountRegionFilter, + Options: schema.TableCreationOptions{PrimaryKeys: []string{"arn"}}, + Columns: []schema.Column{ + { + Name: "account_id", + Description: "The AWS Account ID of the resource.", + Type: schema.TypeString, + Resolver: client.ResolveAWSAccount, + }, + { + Name: "region", + Description: "The AWS Region of the resource.", + Type: schema.TypeString, + Resolver: client.ResolveAWSRegion, + }, + { + Name: "policy", + Description: "A JSON-formatted string that describes the permissions that are associated with the attached secret.", + Type: schema.TypeJSON, + Resolver: fetchSecretsmanagerSecretPolicy, + }, + { + Name: "replication_status", + Description: "A replication object consisting of a RegionReplicationStatus object and includes a Region, KMSKeyId, status, and status message.", + Type: schema.TypeJSON, + Resolver: resolveSecretsmanagerSecretReplicationStatus, + }, + { + Name: "arn", + Description: "The Amazon Resource Name (ARN) of the secret", + Type: schema.TypeString, + Resolver: schema.PathResolver("ARN"), + }, + { + Name: "created_date", + Description: "The date and time when a secret was created.", + Type: schema.TypeTimestamp, + }, + { + Name: "deleted_date", + Description: "The date and time the deletion of the secret occurred", + Type: schema.TypeTimestamp, + }, + { + Name: "description", + Description: "The user-provided description of the secret.", + Type: schema.TypeString, + }, + { + Name: "kms_key_id", + Description: "The ARN or alias of the Amazon Web Services KMS customer master key (CMK) used to encrypt the SecretString and SecretBinary fields in each version of the secret", + Type: schema.TypeString, + }, + { + Name: "last_accessed_date", + Description: "The last date that this secret was accessed", + Type: schema.TypeTimestamp, + }, + { + Name: "last_changed_date", + Description: "The last date and time that this secret was modified in any way.", + Type: schema.TypeTimestamp, + }, + { + Name: "last_rotated_date", + Description: "The most recent date and time that the Secrets Manager rotation process was successfully completed", + Type: schema.TypeTimestamp, + }, + { + Name: "name", + Description: "The friendly name of the secret", + Type: schema.TypeString, + }, + { + Name: "owning_service", + Description: "Returns the name of the service that created the secret.", + Type: schema.TypeString, + }, + { + Name: "primary_region", + Description: "The Region where Secrets Manager originated the secret.", + Type: schema.TypeString, + }, + { + Name: "rotation_enabled", + Description: "Indicates whether automatic, scheduled rotation is enabled for this secret.", + Type: schema.TypeBool, + }, + { + Name: "rotation_lambda_arn", + Description: "The ARN of an Amazon Web Services Lambda function invoked by Secrets Manager to rotate and expire the secret either automatically per the schedule or manually by a call to RotateSecret.", + Type: schema.TypeString, + Resolver: schema.PathResolver("RotationLambdaARN"), + }, + { + Name: "rotation_rules_automatically_after_days", + Description: "Specifies the number of days between automatic scheduled rotations of the secret", + Type: schema.TypeBigInt, + Resolver: schema.PathResolver("RotationRules.AutomaticallyAfterDays"), + }, + { + Name: "secret_versions_to_stages", + Description: "A list of all of the currently assigned SecretVersionStage staging labels and the SecretVersionId attached to each one", + Type: schema.TypeJSON, + }, + { + Name: "tags", + Description: "The list of user-defined tags associated with the secret", + Type: schema.TypeJSON, + Resolver: resolveSecretsmanagerSecretsTags, + }, + }, + } +} + +// ==================================================================================================================== +// Table Resolver Functions +// ==================================================================================================================== + +func fetchSecretsmanagerSecrets(ctx context.Context, meta schema.ClientMeta, _ *schema.Resource, res chan interface{}) error { + c := meta.(*client.Client) + svc := c.Services().SecretsManager + cfg := secretsmanager.ListSecretsInput{} + for { + response, err := svc.ListSecrets(ctx, &cfg, func(options *secretsmanager.Options) { + options.Region = c.Region + }) + if err != nil { + return err + } + + // get more details about the secret + for _, n := range response.SecretList { + + cfg := secretsmanager.DescribeSecretInput{ + SecretId: n.ARN, + } + response, err := svc.DescribeSecret(ctx, &cfg, func(options *secretsmanager.Options) { + options.Region = c.Region + }) + if err != nil { + return err + } + + secret := WrappedSecret{ + SecretListEntry: &n, + ReplicationStatus: response.ReplicationStatus, + RotationRules: response.RotationRules, + } + + res <- secret + } + + if aws.ToString(response.NextToken) == "" { + break + } + cfg.NextToken = response.NextToken + } + return nil +} + +func fetchSecretsmanagerSecretPolicy(ctx context.Context, meta schema.ClientMeta, resource *schema.Resource, c schema.Column) error { + r, ok := resource.Item.(WrappedSecret) + + if !ok { + return fmt.Errorf("expected WrappedSecret but got %T", r) + } + + cl := meta.(*client.Client) + svc := cl.Services().SecretsManager + cfg := secretsmanager.GetResourcePolicyInput{ + SecretId: r.ARN, + } + response, err := svc.GetResourcePolicy(ctx, &cfg, func(options *secretsmanager.Options) { + options.Region = cl.Region + }) + if err != nil { + return err + } + b, err := json.Marshal(response.ResourcePolicy) + if err != nil { + return err + } + return resource.Set(c.Name, b) +} + +func resolveSecretsmanagerSecretReplicationStatus(_ context.Context, _ schema.ClientMeta, resource *schema.Resource, c schema.Column) error { + r, ok := resource.Item.(WrappedSecret) + if !ok { + return fmt.Errorf("expected WrappedSecret but got %T", r) + } + var replicationStatus = make([]map[string]interface{}, len(r.ReplicationStatus)) + + for i, replication := range r.ReplicationStatus { + replicationStatus[i] = map[string]interface{}{ + "kms_key_id": aws.ToString(replication.KmsKeyId), + "last_accessed_date": aws.ToTime(replication.LastAccessedDate), + "region": aws.ToString(replication.Region), + "status": replication.Status, + "status_massage": aws.ToString(replication.StatusMessage), + } + } + b, err := json.Marshal(replicationStatus) + if err != nil { + return err + } + return resource.Set(c.Name, b) +} + +func resolveSecretsmanagerSecretsTags(_ context.Context, _ schema.ClientMeta, resource *schema.Resource, c schema.Column) error { + r, ok := resource.Item.(WrappedSecret) + if !ok { + return fmt.Errorf("expected SecretListEntry but got %T", r) + } + tags := map[string]*string{} + for _, t := range r.Tags { + tags[*t.Key] = t.Value + } + return resource.Set(c.Name, tags) +} + +type WrappedSecret struct { + *types.SecretListEntry + RotationRules *types.RotationRulesType + ReplicationStatus []types.ReplicationStatusType +} diff --git a/resources/secretsmanager_secrets_test.go b/resources/secretsmanager_secrets_test.go new file mode 100644 index 000000000..139a569d5 --- /dev/null +++ b/resources/secretsmanager_secrets_test.go @@ -0,0 +1,53 @@ +package resources + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + types "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/cloudquery/cq-provider-aws/client" + "github.com/cloudquery/cq-provider-aws/client/mocks" + "github.com/cloudquery/faker/v3" + "github.com/golang/mock/gomock" +) + +func buildSecretsmanagerModels(t *testing.T, ctrl *gomock.Controller) client.Services { + m := mocks.NewMockSecretsManagerClient(ctrl) + + secret := types.SecretListEntry{} + if err := faker.FakeData(&secret); err != nil { + t.Fatal(err) + } + + m.EXPECT().ListSecrets(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &secretsmanager.ListSecretsOutput{SecretList: []types.SecretListEntry{secret}}, + nil, + ) + + dsecret := secretsmanager.DescribeSecretOutput{} + if err := faker.FakeData(&dsecret); err != nil { + t.Fatal(err) + } + + m.EXPECT().DescribeSecret(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &dsecret, + nil, + ) + + var policy secretsmanager.GetResourcePolicyOutput + if err := faker.FakeData(&policy); err != nil { + t.Fatal(err) + } + m.EXPECT().GetResourcePolicy(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &policy, + nil, + ) + + return client.Services{ + SecretsManager: m, + } +} + +func TestSecretsManagerModels(t *testing.T) { + awsTestHelper(t, SecretsmanagerSecrets(), buildSecretsmanagerModels, TestOptions{}) +}