diff --git a/client/client.go b/client/client.go index 35fac4a89..f97cda9d8 100644 --- a/client/client.go +++ b/client/client.go @@ -53,6 +53,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sagemaker" "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" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/aws-sdk-go-v2/service/waf" "github.com/aws/aws-sdk-go-v2/service/wafv2" @@ -127,6 +128,7 @@ type Services struct { S3 S3Client S3Control S3ControlClient S3Manager S3ManagerClient + SSM SSMClient SageMaker SageMakerClient SQS SQSClient Apigateway ApigatewayClient @@ -393,6 +395,7 @@ func initServices(region string, c aws.Config) Services { S3Manager: newS3ManagerFromConfig(awsCfg), SageMaker: sagemaker.NewFromConfig(awsCfg), SNS: sns.NewFromConfig(awsCfg), + SSM: ssm.NewFromConfig(awsCfg), SQS: sqs.NewFromConfig(awsCfg), Waf: waf.NewFromConfig(awsCfg), WafV2: wafv2.NewFromConfig(awsCfg), diff --git a/client/mocks/mock_ssm.go b/client/mocks/mock_ssm.go new file mode 100644 index 000000000..6565b2141 --- /dev/null +++ b/client/mocks/mock_ssm.go @@ -0,0 +1,76 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/cloudquery/cq-provider-aws/client (interfaces: SSMClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + ssm "github.com/aws/aws-sdk-go-v2/service/ssm" + gomock "github.com/golang/mock/gomock" +) + +// MockSSMClient is a mock of SSMClient interface. +type MockSSMClient struct { + ctrl *gomock.Controller + recorder *MockSSMClientMockRecorder +} + +// MockSSMClientMockRecorder is the mock recorder for MockSSMClient. +type MockSSMClientMockRecorder struct { + mock *MockSSMClient +} + +// NewMockSSMClient creates a new mock instance. +func NewMockSSMClient(ctrl *gomock.Controller) *MockSSMClient { + mock := &MockSSMClient{ctrl: ctrl} + mock.recorder = &MockSSMClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSSMClient) EXPECT() *MockSSMClientMockRecorder { + return m.recorder +} + +// DescribeInstanceInformation mocks base method. +func (m *MockSSMClient) DescribeInstanceInformation(arg0 context.Context, arg1 *ssm.DescribeInstanceInformationInput, arg2 ...func(*ssm.Options)) (*ssm.DescribeInstanceInformationOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeInstanceInformation", varargs...) + ret0, _ := ret[0].(*ssm.DescribeInstanceInformationOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeInstanceInformation indicates an expected call of DescribeInstanceInformation. +func (mr *MockSSMClientMockRecorder) DescribeInstanceInformation(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, "DescribeInstanceInformation", reflect.TypeOf((*MockSSMClient)(nil).DescribeInstanceInformation), varargs...) +} + +// ListComplianceItems mocks base method. +func (m *MockSSMClient) ListComplianceItems(arg0 context.Context, arg1 *ssm.ListComplianceItemsInput, arg2 ...func(*ssm.Options)) (*ssm.ListComplianceItemsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListComplianceItems", varargs...) + ret0, _ := ret[0].(*ssm.ListComplianceItemsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListComplianceItems indicates an expected call of ListComplianceItems. +func (mr *MockSSMClientMockRecorder) ListComplianceItems(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, "ListComplianceItems", reflect.TypeOf((*MockSSMClient)(nil).ListComplianceItems), varargs...) +} diff --git a/client/services.go b/client/services.go index e4d68432a..c8251289a 100644 --- a/client/services.go +++ b/client/services.go @@ -43,6 +43,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sagemaker" "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" "github.com/aws/aws-sdk-go-v2/service/waf" "github.com/aws/aws-sdk-go-v2/service/wafv2" ) @@ -459,6 +460,12 @@ type GuardDutyClient interface { GetDetector(ctx context.Context, params *guardduty.GetDetectorInput, optFns ...func(*guardduty.Options)) (*guardduty.GetDetectorOutput, error) } +//go:generate mockgen -package=mocks -destination=./mocks/mock_ssm.go . SSMClient +type SSMClient interface { + DescribeInstanceInformation(ctx context.Context, params *ssm.DescribeInstanceInformationInput, optFns ...func(*ssm.Options)) (*ssm.DescribeInstanceInformationOutput, error) + ListComplianceItems(ctx context.Context, params *ssm.ListComplianceItemsInput, optFns ...func(*ssm.Options)) (*ssm.ListComplianceItemsOutput, error) +} + //go:generate mockgen -package=mocks -destination=./mocks/mock_sagemaker.go . SageMakerClient type SageMakerClient interface { ListNotebookInstances(ctx context.Context, params *sagemaker.ListNotebookInstancesInput, optFns ...func(*sagemaker.Options)) (*sagemaker.ListNotebookInstancesOutput, error) diff --git a/docs/tables/aws_ssm_instance_compliance_items.md b/docs/tables/aws_ssm_instance_compliance_items.md new file mode 100644 index 000000000..e1a1ca23b --- /dev/null +++ b/docs/tables/aws_ssm_instance_compliance_items.md @@ -0,0 +1,18 @@ + +# Table: aws_ssm_instance_compliance_items +Information about the compliance as defined by the resource type +## Columns +| Name | Type | Description | +| ------------- | ------------- | ----- | +|instance_cq_id|uuid|Unique CloudQuery ID of aws_ssm_instances table (FK)| +|compliance_type|text|The compliance type| +|details|jsonb|A "Key": "Value" tag combination for the compliance item.| +|execution_summary_execution_time|timestamp without time zone|The time the execution ran as a datetime object that is saved in the following format: yyyy-MM-dd'T'HH:mm:ss'Z'.| +|execution_summary_execution_id|text|An ID created by the system when PutComplianceItems was called| +|execution_summary_execution_type|text|The type of execution| +|id|text|An ID for the compliance item| +|resource_id|text|An ID for the resource| +|resource_type|text|The type of resource| +|severity|text|The severity of the compliance status| +|status|text|The status of the compliance item| +|title|text|A title for the compliance item| diff --git a/docs/tables/aws_ssm_instances.md b/docs/tables/aws_ssm_instances.md new file mode 100644 index 000000000..19d92b548 --- /dev/null +++ b/docs/tables/aws_ssm_instances.md @@ -0,0 +1,29 @@ + +# Table: aws_ssm_instances +Describes a filter for a specific list of instances. +## Columns +| Name | Type | Description | +| ------------- | ------------- | ----- | +|account_id|text|The AWS Account ID of the resource.| +|region|text|The AWS Region of the resource.| +|arn|text|The Amazon Resource Name (ARN) of the managed instance.| +|activation_id|text|The activation ID created by Amazon Web Services Systems Manager when the server or virtual machine (VM) was registered.| +|agent_version|text|The version of SSM Agent running on your Linux instance.| +|association_overview_detailed_status|text|Detailed status information about the aggregated associations.| +|association_instance_status_aggregated_count|jsonb|The number of associations for the instance(s).| +|association_status|text|The status of the association.| +|computer_name|text|The fully qualified host name of the managed instance.| +|ip_address|inet|The IP address of the managed instance.| +|iam_role|text|The Identity and Access Management (IAM) role assigned to the on-premises Systems Manager managed instance| +|instance_id|text|The instance ID.| +|is_latest_version|boolean|Indicates whether the latest version of SSM Agent is running on your Linux Managed Instance| +|last_association_execution_date|timestamp without time zone|The date the association was last run.| +|last_ping_date_time|timestamp without time zone|The date and time when the agent last pinged the Systems Manager service.| +|last_successful_association_execution_date|timestamp without time zone|The last date the association was successfully run.| +|name|text|The name assigned to an on-premises server or virtual machine (VM) when it is activated as a Systems Manager managed instance| +|ping_status|text|Connection status of SSM Agent| +|platform_name|text|The name of the operating system platform running on your instance.| +|platform_type|text|The operating system platform type.| +|platform_version|text|The version of the OS platform running on your instance.| +|registration_date|timestamp without time zone|The date the server or VM was registered with Amazon Web Services as a managed instance.| +|resource_type|text|The type of instance| diff --git a/go.mod b/go.mod index 8d0bb58b5..727e93f62 100644 --- a/go.mod +++ b/go.mod @@ -136,6 +136,11 @@ require ( gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) +require ( + github.com/aws/aws-sdk-go-v2/service/s3control v1.14.1 + github.com/aws/aws-sdk-go-v2/service/ssm v1.16.0 +) + require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect diff --git a/go.sum b/go.sum index 0dddeafcb..12370ea55 100644 --- a/go.sum +++ b/go.sum @@ -257,6 +257,8 @@ github.com/aws/aws-sdk-go-v2/service/sns v1.1.2 h1:1U/FujyBEkNwrvANUcZFuVnAQqy0E 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= github.com/aws/aws-sdk-go-v2/service/sqs v1.9.1/go.mod h1:nbjBtoH25NLQ7Pv/QqmB94JLDdy3kSGvys2iH2OBspk= +github.com/aws/aws-sdk-go-v2/service/ssm v1.16.0 h1:LP8DuA8sYKOf37HAEIyBFbcdeyD/ceqlARJD2LnVNvI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.16.0/go.mod h1:0CzdxtFRsppljClOL0+1hXEz4C+i+nKfzMRh7LP3pNY= github.com/aws/aws-sdk-go-v2/service/sso v1.1.5/go.mod h1:bpGz0tidC4y39sZkQSkpO/J0tzWCMXHbw6FZ0j1GkWM= github.com/aws/aws-sdk-go-v2/service/sso v1.2.1 h1:alpXc5UG7al7QnttHe/9hfvUfitV8r3w0onPpPkGzi0= github.com/aws/aws-sdk-go-v2/service/sso v1.2.1/go.mod h1:VimPFPltQ/920i1X0Sb0VJBROLIHkDg2MNP10D46OGs= diff --git a/resources/provider.go b/resources/provider.go index 806f5bdc3..3f3ad95c9 100644 --- a/resources/provider.go +++ b/resources/provider.go @@ -120,6 +120,7 @@ func Provider() *provider.Provider { "sns.subscriptions": SnsSubscriptions(), "sns.topics": SnsTopics(), "sqs.queues": SQSQueues(), + "ssm.instances": SsmInstances(), "waf.rule_groups": WafRuleGroups(), "waf.rules": WafRules(), "waf.subscribed_rule_groups": WafSubscribedRuleGroups(), diff --git a/resources/ssm_instances.go b/resources/ssm_instances.go new file mode 100644 index 000000000..c0791185a --- /dev/null +++ b/resources/ssm_instances.go @@ -0,0 +1,282 @@ +package resources + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/cloudquery/cq-provider-aws/client" + "github.com/cloudquery/cq-provider-sdk/provider/schema" +) + +func SsmInstances() *schema.Table { + return &schema.Table{ + Name: "aws_ssm_instances", + Description: "Describes a filter for a specific list of instances.", + Resolver: fetchSsmInstances, + 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: "arn", + Description: "The Amazon Resource Name (ARN) of the managed instance.", + Type: schema.TypeString, + Resolver: resolveSSMInstanceARN, + }, + { + Name: "activation_id", + Description: "The activation ID created by Amazon Web Services Systems Manager when the server or virtual machine (VM) was registered.", + Type: schema.TypeString, + }, + { + Name: "agent_version", + Description: "The version of SSM Agent running on your Linux instance.", + Type: schema.TypeString, + }, + { + Name: "association_overview_detailed_status", + Description: "Detailed status information about the aggregated associations.", + Type: schema.TypeString, + Resolver: schema.PathResolver("AssociationOverview.DetailedStatus"), + }, + { + Name: "association_instance_status_aggregated_count", + Description: "The number of associations for the instance(s).", + Type: schema.TypeJSON, + Resolver: schema.PathResolver("AssociationOverview.InstanceAssociationStatusAggregatedCount"), + }, + { + Name: "association_status", + Description: "The status of the association.", + Type: schema.TypeString, + }, + { + Name: "computer_name", + Description: "The fully qualified host name of the managed instance.", + Type: schema.TypeString, + }, + { + Name: "ip_address", + Description: "The IP address of the managed instance.", + Type: schema.TypeInet, + Resolver: schema.IPAddressResolver("IPAddress"), + }, + { + Name: "iam_role", + Description: "The Identity and Access Management (IAM) role assigned to the on-premises Systems Manager managed instance", + Type: schema.TypeString, + }, + { + Name: "instance_id", + Description: "The instance ID.", + Type: schema.TypeString, + }, + { + Name: "is_latest_version", + Description: "Indicates whether the latest version of SSM Agent is running on your Linux Managed Instance", + Type: schema.TypeBool, + }, + { + Name: "last_association_execution_date", + Description: "The date the association was last run.", + Type: schema.TypeTimestamp, + }, + { + Name: "last_ping_date_time", + Description: "The date and time when the agent last pinged the Systems Manager service.", + Type: schema.TypeTimestamp, + }, + { + Name: "last_successful_association_execution_date", + Description: "The last date the association was successfully run.", + Type: schema.TypeTimestamp, + }, + { + Name: "name", + Description: "The name assigned to an on-premises server or virtual machine (VM) when it is activated as a Systems Manager managed instance", + Type: schema.TypeString, + }, + { + Name: "ping_status", + Description: "Connection status of SSM Agent", + Type: schema.TypeString, + }, + { + Name: "platform_name", + Description: "The name of the operating system platform running on your instance.", + Type: schema.TypeString, + }, + { + Name: "platform_type", + Description: "The operating system platform type.", + Type: schema.TypeString, + }, + { + Name: "platform_version", + Description: "The version of the OS platform running on your instance.", + Type: schema.TypeString, + }, + { + Name: "registration_date", + Description: "The date the server or VM was registered with Amazon Web Services as a managed instance.", + Type: schema.TypeTimestamp, + }, + { + Name: "resource_type", + Description: "The type of instance", + Type: schema.TypeString, + }, + }, + Relations: []*schema.Table{ + { + Name: "aws_ssm_instance_compliance_items", + Description: "Information about the compliance as defined by the resource type", + Resolver: fetchSsmInstanceComplianceItems, + IgnoreError: client.IgnoreAccessDeniedServiceDisabled, + Options: schema.TableCreationOptions{PrimaryKeys: []string{"instance_cq_id", "resource_id", "id"}}, + Columns: []schema.Column{ + { + Name: "instance_cq_id", + Description: "Unique CloudQuery ID of aws_ssm_instances table (FK)", + Type: schema.TypeUUID, + Resolver: schema.ParentIdResolver, + }, + { + Name: "compliance_type", + Description: "The compliance type", + Type: schema.TypeString, + }, + { + Name: "details", + Description: "A \"Key\": \"Value\" tag combination for the compliance item.", + Type: schema.TypeJSON, + }, + { + Name: "execution_summary_execution_time", + Description: "The time the execution ran as a datetime object that is saved in the following format: yyyy-MM-dd'T'HH:mm:ss'Z'.", + Type: schema.TypeTimestamp, + Resolver: schema.PathResolver("ExecutionSummary.ExecutionTime"), + }, + { + Name: "execution_summary_execution_id", + Description: "An ID created by the system when PutComplianceItems was called", + Type: schema.TypeString, + Resolver: schema.PathResolver("ExecutionSummary.ExecutionId"), + }, + { + Name: "execution_summary_execution_type", + Description: "The type of execution", + Type: schema.TypeString, + Resolver: schema.PathResolver("ExecutionSummary.ExecutionType"), + }, + { + Name: "id", + Description: "An ID for the compliance item", + Type: schema.TypeString, + }, + { + Name: "resource_id", + Description: "An ID for the resource", + Type: schema.TypeString, + }, + { + Name: "resource_type", + Description: "The type of resource", + Type: schema.TypeString, + }, + { + Name: "severity", + Description: "The severity of the compliance status", + Type: schema.TypeString, + }, + { + Name: "status", + Description: "The status of the compliance item", + Type: schema.TypeString, + }, + { + Name: "title", + Description: "A title for the compliance item", + Type: schema.TypeString, + }, + }, + }, + }, + } +} + +// ==================================================================================================================== +// Table Resolver Functions +// ==================================================================================================================== +func fetchSsmInstances(ctx context.Context, meta schema.ClientMeta, parent *schema.Resource, res chan interface{}) error { + client := meta.(*client.Client) + svc := client.Services().SSM + optsFn := func(o *ssm.Options) { + o.Region = client.Region + } + var input ssm.DescribeInstanceInformationInput + for { + output, err := svc.DescribeInstanceInformation(ctx, &input, optsFn) + if err != nil { + return err + } + res <- output.InstanceInformationList + if aws.ToString(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + return nil +} + +func fetchSsmInstanceComplianceItems(ctx context.Context, meta schema.ClientMeta, parent *schema.Resource, res chan interface{}) error { + instance, ok := parent.Item.(types.InstanceInformation) + if !ok { + return fmt.Errorf("not a %T instance: %T", instance, parent.Item) + } + client := meta.(*client.Client) + svc := client.Services().SSM + optsFn := func(o *ssm.Options) { + o.Region = client.Region + } + input := ssm.ListComplianceItemsInput{ + ResourceIds: []string{*instance.InstanceId}, + } + for { + output, err := svc.ListComplianceItems(ctx, &input, optsFn) + if err != nil { + return err + } + res <- output.ComplianceItems + if aws.ToString(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + return nil +} + +func resolveSSMInstanceARN(_ context.Context, meta schema.ClientMeta, resource *schema.Resource, c schema.Column) error { + instance, ok := resource.Item.(types.InstanceInformation) + if !ok { + return fmt.Errorf("not a %T instance: %T", instance, resource.Item) + } + cl := meta.(*client.Client) + return resource.Set(c.Name, client.GenerateResourceARN("ssm", "managed-instance", *instance.InstanceId, cl.Region, cl.AccountID)) +} diff --git a/resources/ssm_instances_test.go b/resources/ssm_instances_test.go new file mode 100644 index 000000000..02d45882b --- /dev/null +++ b/resources/ssm_instances_test.go @@ -0,0 +1,48 @@ +package resources + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/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 buildSSMInstances(t *testing.T, ctrl *gomock.Controller) client.Services { + mock := mocks.NewMockSSMClient(ctrl) + + var i types.InstanceInformation + if err := faker.FakeData(&i); err != nil { + t.Fatal(err) + } + i.IPAddress = aws.String("192.168.1.1") + mock.EXPECT().DescribeInstanceInformation( + gomock.Any(), + &ssm.DescribeInstanceInformationInput{}, + gomock.Any(), + ).Return( + &ssm.DescribeInstanceInformationOutput{InstanceInformationList: []types.InstanceInformation{i}}, + nil, + ) + + var c types.ComplianceItem + if err := faker.FakeData(&c); err != nil { + t.Fatal(err) + } + mock.EXPECT().ListComplianceItems(gomock.Any(), + &ssm.ListComplianceItemsInput{ResourceIds: []string{*i.InstanceId}}, + gomock.Any(), + ).Return( + &ssm.ListComplianceItemsOutput{ComplianceItems: []types.ComplianceItem{c}}, + nil, + ) + return client.Services{SSM: mock} +} + +func TestSSMInstances(t *testing.T) { + awsTestHelper(t, SsmInstances(), buildSSMInstances, TestOptions{}) +}