diff --git a/api/alert_channels.go b/api/alert_channels.go index 20f91864d..7b4ec9a72 100644 --- a/api/alert_channels.go +++ b/api/alert_channels.go @@ -152,9 +152,8 @@ func (svc *AlertChannelsService) Test(guid string) error { // Get(guid) // // Where is the Alert Channel integration type. -func (svc *AlertChannelsService) Get(guid string) (response AlertChannelResponse, err error) { - err = svc.get(guid, &response) - return +func (svc *AlertChannelsService) Get(guid string, response interface{}) error { + return svc.get(guid, &response) } type AlertChannelRaw struct { diff --git a/api/alert_channels_test.go b/api/alert_channels_test.go index 0300f80bc..4b4f8a0e7 100644 --- a/api/alert_channels_test.go +++ b/api/alert_channels_test.go @@ -89,7 +89,8 @@ func TestAlertChannelsGet(t *testing.T) { assert.Nil(t, err) t.Run("when alert channel exists", func(t *testing.T) { - response, err := c.V2.AlertChannels.Get(intgGUID) + var response api.AlertChannelResponse + err := c.V2.AlertChannels.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -99,7 +100,8 @@ func TestAlertChannelsGet(t *testing.T) { }) t.Run("when alert channel does NOT exist", func(t *testing.T) { - response, err := c.V2.AlertChannels.Get("UNKNOWN_INTG_GUID") + var response api.AlertChannelResponse + err := c.V2.AlertChannels.Get("UNKNOWN_INTG_GUID", response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/AlertChannels/UNKNOWN_INTG_GUID") @@ -154,7 +156,8 @@ func TestAlertChannelsDelete(t *testing.T) { assert.Nil(t, err) t.Run("verify alert channel exists", func(t *testing.T) { - response, err := c.V2.AlertChannels.Get(intgGUID) + var response api.AlertChannelResponse + err := c.V2.AlertChannels.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -167,7 +170,8 @@ func TestAlertChannelsDelete(t *testing.T) { err := c.V2.AlertChannels.Delete(intgGUID) assert.Nil(t, err) - response, err := c.V2.AlertChannels.Get(intgGUID) + var response api.AlertChannelResponse + err = c.V2.AlertChannels.Get(intgGUID, &response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/AlertChannels/MOCK_") diff --git a/api/cloud_accounts.go b/api/cloud_accounts.go index 06da66f3e..ad9518929 100644 --- a/api/cloud_accounts.go +++ b/api/cloud_accounts.go @@ -146,9 +146,8 @@ func (svc *CloudAccountsService) Delete(guid string) error { // Get(guid) // // Where is the Cloud Account integration type. -func (svc *CloudAccountsService) Get(guid string) (response CloudAccountResponse, err error) { - err = svc.get(guid, &response) - return +func (svc *CloudAccountsService) Get(guid string, response interface{}) error { + return svc.get(guid, &response) } type CloudAccountRaw struct { diff --git a/api/cloud_accounts_test.go b/api/cloud_accounts_test.go index b40291a14..e2e77007c 100644 --- a/api/cloud_accounts_test.go +++ b/api/cloud_accounts_test.go @@ -89,7 +89,8 @@ func TestCloudAccountsGet(t *testing.T) { assert.Nil(t, err) t.Run("when cloud account exists", func(t *testing.T) { - response, err := c.V2.CloudAccounts.Get(intgGUID) + var response api.CloudAccountResponse + err := c.V2.CloudAccounts.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -99,7 +100,8 @@ func TestCloudAccountsGet(t *testing.T) { }) t.Run("when cloud account does NOT exist", func(t *testing.T) { - response, err := c.V2.CloudAccounts.Get("UNKNOWN_INTG_GUID") + var response api.CloudAccountResponse + err := c.V2.CloudAccounts.Get("UNKNOWN_INTG_GUID", response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/CloudAccounts/UNKNOWN_INTG_GUID") @@ -154,7 +156,8 @@ func TestCloudAccountsDelete(t *testing.T) { assert.Nil(t, err) t.Run("verify cloud account exists", func(t *testing.T) { - response, err := c.V2.CloudAccounts.Get(intgGUID) + var response api.CloudAccountResponse + err := c.V2.CloudAccounts.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -167,7 +170,8 @@ func TestCloudAccountsDelete(t *testing.T) { err := c.V2.CloudAccounts.Delete(intgGUID) assert.Nil(t, err) - response, err := c.V2.CloudAccounts.Get(intgGUID) + var response api.CloudAccountResponse + err = c.V2.CloudAccounts.Get(intgGUID, response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/CloudAccounts/MOCK_") diff --git a/api/container_registries.go b/api/container_registries.go index 14196c6e3..8f99a1fa7 100644 --- a/api/container_registries.go +++ b/api/container_registries.go @@ -157,9 +157,8 @@ func (svc *ContainerRegistriesService) Delete(guid string) error { // Get(guid) // // Where is the Container Registry integration type. -func (svc *ContainerRegistriesService) Get(guid string) (response ContainerRegistryResponse, err error) { - err = svc.get(guid, &response) - return +func (svc *ContainerRegistriesService) Get(guid string, response interface{}) error { + return svc.get(guid, &response) } type ContainerRegistryRaw struct { diff --git a/api/container_registries_test.go b/api/container_registries_test.go index edc45d48c..e8af30769 100644 --- a/api/container_registries_test.go +++ b/api/container_registries_test.go @@ -108,7 +108,8 @@ func TestContainerRegistriesGet(t *testing.T) { assert.Nil(t, err) t.Run("when container registry exists", func(t *testing.T) { - response, err := c.V2.ContainerRegistries.Get(intgGUID) + var response api.ContainerRegistryResponse + err := c.V2.ContainerRegistries.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -118,7 +119,8 @@ func TestContainerRegistriesGet(t *testing.T) { }) t.Run("when container registry does NOT exist", func(t *testing.T) { - response, err := c.V2.ContainerRegistries.Get("UNKNOWN_INTG_GUID") + var response api.ContainerRegistriesResponse + err := c.V2.ContainerRegistries.Get("UNKNOWN_INTG_GUID", response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/ContainerRegistries/UNKNOWN_INTG_GUID") @@ -173,7 +175,8 @@ func TestContainerRegistriesDelete(t *testing.T) { assert.Nil(t, err) t.Run("verify container registry exists", func(t *testing.T) { - response, err := c.V2.ContainerRegistries.Get(intgGUID) + var response api.ContainerRegistryResponse + err := c.V2.ContainerRegistries.Get(intgGUID, &response) assert.Nil(t, err) if assert.NotNil(t, response) { assert.Equal(t, intgGUID, response.Data.IntgGuid) @@ -186,7 +189,8 @@ func TestContainerRegistriesDelete(t *testing.T) { err := c.V2.ContainerRegistries.Delete(intgGUID) assert.Nil(t, err) - response, err := c.V2.ContainerRegistries.Get(intgGUID) + var response api.ContainerRegistriesResponse + err = c.V2.ContainerRegistries.Get(intgGUID, response) assert.Empty(t, response) if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "api/v2/ContainerRegistries/MOCK_") diff --git a/api/integrations.go b/api/integrations.go index c1f139cee..73a84f435 100644 --- a/api/integrations.go +++ b/api/integrations.go @@ -110,46 +110,56 @@ const ( WebhookIntegration ) +type integration struct { + name string + schema integrationSchema +} + // IntegrationTypes is the list of available integration types -var IntegrationTypes = map[integrationType]string{ - NoneIntegration: "NONE", - AwsCfgIntegration: "AWS_CFG", - AwsCloudTrailIntegration: "AWS_CT_SQS", - AwsGovCloudCfgIntegration: "AWS_US_GOV_CFG", - AwsGovCloudCTIntegration: "AWS_US_GOV_CT_SQS", - AwsS3ChannelIntegration: "AWS_S3", - CiscoWebexChannelIntegration: "CISCO_SPARK_WEBHOOK", - DatadogChannelIntegration: "DATADOG", - GcpCfgIntegration: "GCP_CFG", - GcpAuditLogIntegration: "GCP_AT_SES", - GcpPubSubChannelIntegration: "GCP_PUBSUB", - NewRelicChannelIntegration: "NEW_RELIC_INSIGHTS", - AzureCfgIntegration: "AZURE_CFG", - AzureActivityLogIntegration: "AZURE_AL_SEQ", - ContainerRegistryIntegration: "CONT_VULN_CFG", - QRadarChannelIntegration: "IBM_QRADAR", - MicrosoftTeamsChannelIntegration: "MICROSOFT_TEAMS", - SlackChannelIntegration: "SLACK_CHANNEL", - SplunkIntegration: "SPLUNK_HEC", - ServiceNowChannelIntegration: "SERVICE_NOW_REST", - AwsCloudWatchIntegration: "CLOUDWATCH_EB", - PagerDutyIntegration: "PAGER_DUTY_API", - JiraIntegration: "JIRA", - EmailIntegration: "EMAIL_USER", - VictorOpsChannelIntegration: "VICTOR_OPS", - WebhookIntegration: "WEBHOOK", +var IntegrationTypes = map[integrationType]integration{ + NoneIntegration: {"NONE", None}, + AwsCfgIntegration: {"AWS_CFG", CloudAccounts}, + AwsCloudTrailIntegration: {"AWS_CT_SQS", CloudAccounts}, + AwsGovCloudCfgIntegration: {"AWS_US_GOV_CFG", CloudAccounts}, + AwsGovCloudCTIntegration: {"AWS_US_GOV_CT_SQS", CloudAccounts}, + AwsS3ChannelIntegration: {"AWS_S3", AlertChannels}, + CiscoWebexChannelIntegration: {"CISCO_SPARK_WEBHOOK", AlertChannels}, + DatadogChannelIntegration: {"DATADOG", AlertChannels}, + GcpCfgIntegration: {"GCP_CFG", CloudAccounts}, + GcpAuditLogIntegration: {"GCP_AT_SES", CloudAccounts}, + GcpPubSubChannelIntegration: {"GCP_PUBSUB", AlertChannels}, + NewRelicChannelIntegration: {"NEW_RELIC_INSIGHTS", AlertChannels}, + AzureCfgIntegration: {"AZURE_CFG", CloudAccounts}, + AzureActivityLogIntegration: {"AZURE_AL_SEQ", CloudAccounts}, + ContainerRegistryIntegration: {"CONT_VULN_CFG", ContainerRegistries}, + QRadarChannelIntegration: {"IBM_QRADAR", AlertChannels}, + MicrosoftTeamsChannelIntegration: {"MICROSOFT_TEAMS", AlertChannels}, + SlackChannelIntegration: {"SLACK_CHANNEL", AlertChannels}, + SplunkIntegration: {"SPLUNK_HEC", AlertChannels}, + ServiceNowChannelIntegration: {"SERVICE_NOW_REST", AlertChannels}, + AwsCloudWatchIntegration: {"CLOUDWATCH_EB", AlertChannels}, + PagerDutyIntegration: {"PAGER_DUTY_API", AlertChannels}, + JiraIntegration: {"JIRA", AlertChannels}, + EmailIntegration: {"EMAIL_USER", AlertChannels}, + VictorOpsChannelIntegration: {"VICTOR_OPS", AlertChannels}, + WebhookIntegration: {"WEBHOOK", AlertChannels}, } // String returns the string representation of an integration type func (i integrationType) String() string { - return IntegrationTypes[i] + return IntegrationTypes[i].name +} + +// Schema returns the integration type +func (i integrationType) Schema() integrationSchema { + return IntegrationTypes[i].schema } // FindIntegrationType looks up inside the list of available integration types // the matching type from the provided string, if none, returns NoneIntegration func FindIntegrationType(t string) (integrationType, bool) { for iType, str := range IntegrationTypes { - if str == t { + if str.name == t { return iType, true } } @@ -263,9 +273,10 @@ func (c commonIntegrationData) StateString() string { } type IntegrationState struct { - Ok bool `json:"ok"` - LastUpdatedTime string `json:"lastUpdatedTime"` - LastSuccessfulTime string `json:"lastSuccessfulTime"` + Ok bool `json:"ok"` + LastUpdatedTime string `json:"lastUpdatedTime"` + LastSuccessfulTime string `json:"lastSuccessfulTime"` + Details map[string]interface{} `json:"details,omitempty"` } type RawIntegration struct { diff --git a/api/schemas.go b/api/schemas.go new file mode 100644 index 000000000..d24ddbaca --- /dev/null +++ b/api/schemas.go @@ -0,0 +1,38 @@ +// +// Author:: Darren Murray () +// Copyright:: Copyright 2021, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package api + +// SchemaService is the service that retrieves schemas for v2 +type SchemasService struct { + client *Client + Services map[integrationSchema]V2Service +} + +type integrationSchema int + +const ( + None integrationSchema = iota + AlertChannels + ContainerRegistries + CloudAccounts +) + +func (svc *SchemasService) GetService(schemaName integrationSchema) V2Service { + return svc.Services[schemaName] +} diff --git a/api/v2.go b/api/v2.go index 1be74f02b..6dff69108 100644 --- a/api/v2.go +++ b/api/v2.go @@ -31,10 +31,11 @@ type V2Endpoints struct { AgentAccessTokens *AgentAccessTokensService Query *QueryService Policy *PolicyService + Schemas *SchemasService } func NewV2Endpoints(c *Client) *V2Endpoints { - return &V2Endpoints{c, + v2 := &V2Endpoints{c, &UserProfileService{c}, &AlertChannelsService{c}, &CloudAccountsService{c}, @@ -42,5 +43,22 @@ func NewV2Endpoints(c *Client) *V2Endpoints { &AgentAccessTokensService{c}, &QueryService{c}, &PolicyService{c}, + &SchemasService{c, map[integrationSchema]V2Service{}}, } + + v2.Schemas.Services = map[integrationSchema]V2Service{ + AlertChannels: &AlertChannelsService{c}, + CloudAccounts: &CloudAccountsService{c}, + ContainerRegistries: &ContainerRegistriesService{c}, + } + return v2 +} + +type V2Service interface { + Get(string, interface{}) error + Delete(string) error +} + +type V2CommonIntegration struct { + Data v2CommonIntegrationData `json:"data"` } diff --git a/cli/cmd/integration.go b/cli/cmd/integration.go index e94c8a042..cb901ba3d 100644 --- a/cli/cmd/integration.go +++ b/cli/cmd/integration.go @@ -19,6 +19,7 @@ package cmd import ( + "encoding/json" "fmt" "strings" @@ -93,6 +94,7 @@ var ( Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { integration, err := cli.LwApi.Integrations.Get(args[0]) + if err != nil { return errors.Wrap(err, "unable to get integration") } @@ -103,6 +105,17 @@ var ( return errors.New(msg) } + integrationType, _ := api.FindIntegrationType(integration.Data[0].Type) + var resp api.V2CommonIntegration + err = cli.LwApi.V2.Schemas.GetService(integrationType.Schema()).Get(args[0], &resp) + + if err != nil { + cli.Log.Debugw("unable to get integration service", "error", err.Error()) + } + + if resp.Data.State != nil { + integration.Data[0].State.Details = resp.Data.State.Details + } if cli.JSONOutput() { return cli.OutputJSON(integration.Data[0]) } @@ -352,10 +365,26 @@ func buildIntDetailsTable(integrations []api.RawIntegration) string { func buildIntegrationState(state *api.IntegrationState) [][]string { if state != nil { - return [][]string{ + details := [][]string{ {"STATE UPDATED AT", state.LastUpdatedTime}, {"LAST SUCCESSFUL STATE", state.LastSuccessfulTime}, } + + if len(state.Details) != 0 { + detailsStr, err := json.Marshal(state.Details) + if err != nil { + cli.Log.Debugw("unable to marshall state details", "error", err.Error()) + return details + } + + detailsJSON, err := cli.FormatJSONString(string(detailsStr)) + if err != nil { + cli.Log.Debugw("unable to json format state details", "error", err.Error()) + return details + } + details = append(details, []string{"STATE DETAILS", detailsJSON}) + } + return details } return [][]string{}