Skip to content

Commit

Permalink
feat(cli): Output v2 integration state details (#505)
Browse files Browse the repository at this point in the history
Issue: https://lacework.atlassian.net/browse/ALLY-586

- Fetch state details from V2 api.
- Register AlertChannels, CloudAccounts & ContainerRegistries services in Schema Services
- Map Schema to Integrations in IntegrationsTypes map

Usage
```
❯ lacework int show <guid>    
                      INTEGRATION GUID                            NAME         TYPE     STATUS    STATE  
-----------------------------------------------------------+----------------+---------+---------+--------
  GUID_11111111111111111111111111111111111111111111111111   project-id-123   GCP_CFG   Enabled   Ok     

                                            INTEGRATION DETAILS                                            
-----------------------------------------------------------------------------------------------------------
    LEVEL                   PROJECT                                                                        
    ORG/PROJECT ID          project-id-123                                                   
    CLIENT ID               111111111111111111111111111                                                         
    CLIENT EMAIL            [email protected]    
    PRIVATE KEY ID          111111111111111111111111111                                       
    UPDATED AT              2021-Jun-01 18:03:19 UTC                                                       
    UPDATED BY              [email protected]                                                  
    STATE UPDATED AT        2021-Aug-05 13:32:20 UTC                                                       
    LAST SUCCESSFUL STATE   2021-Aug-05 13:32:20 UTC                                                       
    STATE DETAILS           {                                                                              
                              "projectErrors": {                                                           
                                "project-id-123": {                                           
                                  "opsDeniedAccess": []                                                    
                                }                                                                          
                              }                                                                            
                            }  
```

Signed-off-by: Darren Murray <[email protected]>
  • Loading branch information
dmurray-lacework authored Aug 6, 2021
1 parent f5eaf36 commit e2cc6f1
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 55 deletions.
5 changes: 2 additions & 3 deletions api/alert_channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,8 @@ func (svc *AlertChannelsService) Test(guid string) error {
// Get<Type>(guid)
//
// Where <Type> 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 {
Expand Down
12 changes: 8 additions & 4 deletions api/alert_channels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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_")
Expand Down
5 changes: 2 additions & 3 deletions api/cloud_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,8 @@ func (svc *CloudAccountsService) Delete(guid string) error {
// Get<Type>(guid)
//
// Where <Type> 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 {
Expand Down
12 changes: 8 additions & 4 deletions api/cloud_accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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_")
Expand Down
5 changes: 2 additions & 3 deletions api/container_registries.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,8 @@ func (svc *ContainerRegistriesService) Delete(guid string) error {
// Get<Type>(guid)
//
// Where <Type> 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 {
Expand Down
12 changes: 8 additions & 4 deletions api/container_registries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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_")
Expand Down
75 changes: 43 additions & 32 deletions api/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions api/schemas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Author:: Darren Murray (<[email protected]>)
// 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]
}
20 changes: 19 additions & 1 deletion api/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,34 @@ 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},
&ContainerRegistriesService{c},
&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"`
}
31 changes: 30 additions & 1 deletion cli/cmd/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package cmd

import (
"encoding/json"
"fmt"
"strings"

Expand Down Expand Up @@ -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")
}
Expand All @@ -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])
}
Expand Down Expand Up @@ -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{}
Expand Down

0 comments on commit e2cc6f1

Please sign in to comment.