diff --git a/api/alert_channels_splunk_test.go b/api/alert_channels_splunk_test.go index 9855c2c67..1f2cc9bd9 100644 --- a/api/alert_channels_splunk_test.go +++ b/api/alert_channels_splunk_test.go @@ -19,9 +19,12 @@ package api_test import ( + "encoding/json" "fmt" + "log" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" @@ -139,6 +142,21 @@ func TestAlertChannelSplunkUpdate(t *testing.T) { } } +func TestMarshallAlertChannelLastUpdatedTime(t *testing.T) { + var res api.SplunkHecAlertChannelResponseV2 + err := json.Unmarshal([]byte(generateAlertChannelResponse(singleSplunkAlertChannel("test"))), &res) + if err != nil { + log.Fatal("Unable to unmarshall splunk string") + } + jsonString, err := json.Marshal(res) + if err != nil { + log.Fatal("Unable to marshall splunk string") + } + + assert.Equal(t, res.Data.State.LastUpdatedTime.ToTime().UnixNano()/int64(time.Millisecond), int64(1627895573122)) + assert.Contains(t, string(jsonString), "1627895573122") +} + func singleSplunkAlertChannel(id string) string { return ` { diff --git a/api/resource_groups.go b/api/resource_groups.go index 637366cfb..53b7cd4ab 100644 --- a/api/resource_groups.go +++ b/api/resource_groups.go @@ -24,6 +24,8 @@ import ( "strconv" "github.com/pkg/errors" + + "github.com/lacework/go-sdk/lwtime" ) // ResourceGroupsService is the service that interacts with @@ -32,6 +34,16 @@ type ResourceGroupsService struct { client *Client } +type ResourceGroupProps interface { + GetBaseProps() ResourceGroupPropsBase +} + +type ResourceGroupPropsBase struct { + Description string `json:"description"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` +} + type ResourceGroup interface { ID() string ResourceGroupType() ResourceGroupType diff --git a/api/resource_groups_aws.go b/api/resource_groups_aws.go index fac6c5ac9..20c3c8e48 100644 --- a/api/resource_groups_aws.go +++ b/api/resource_groups_aws.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -120,16 +121,39 @@ type AwsResourceGroupData struct { } type AwsResourceGroupProps struct { - Description string `json:"description,omitempty"` - AccountIDs []string `json:"accountIds"` - UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + Description string `json:"description,omitempty"` + AccountIDs []string `json:"accountIds"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string type AwsResourceJsonStringGroupProps struct { - Description string `json:"DESCRIPTION,omitempty"` - AccountIDs []string `json:"ACCOUNT_IDS"` - UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + Description string `json:"DESCRIPTION,omitempty"` + AccountIDs []string `json:"ACCOUNT_IDS"` + UpdatedBy string `json:"UPDATED_BY,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props AwsResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props AwsResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + AccountIDs []string `json:"accountIds"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + AccountIDs: props.AccountIDs, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/api/resource_groups_aws_test.go b/api/resource_groups_aws_test.go index 3b29ecb62..c676d690d 100644 --- a/api/resource_groups_aws_test.go +++ b/api/resource_groups_aws_test.go @@ -19,9 +19,12 @@ package api_test import ( + "encoding/json" "fmt" + "log" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" @@ -110,6 +113,21 @@ func TestResourceGroupsAwsUpdate(t *testing.T) { assert.Equal(t, resourceGUID, response.Data.ResourceGuid) } +func TestMarshallResourceGroupLastUpdatedTime(t *testing.T) { + var res api.AwsResourceGroupResponse + err := json.Unmarshal([]byte(generateResourceGroupResponse(singleAwsResourceGroupUpdateResponse("test"))), &res) + if err != nil { + log.Fatal("Unable to unmarshall aws resource group string") + } + jsonString, err := json.Marshal(res) + if err != nil { + log.Fatal("Unable to marshall aws resource group string") + } + + assert.Equal(t, res.Data.Props.LastUpdated.ToTime().UnixNano()/int64(time.Millisecond), int64(1586453993470)) + assert.Contains(t, string(jsonString), "2020-04-09T17:39:53Z") +} + func singleAwsResourceGroup(id string) string { return ` { diff --git a/api/resource_groups_azure.go b/api/resource_groups_azure.go index fe29cf646..a99270cc6 100644 --- a/api/resource_groups_azure.go +++ b/api/resource_groups_azure.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -124,18 +125,43 @@ type AzureResourceGroupData struct { } type AzureResourceGroupProps struct { - Description string `json:"description,omitempty"` - Tenant string `json:"tenant"` - Subscriptions []string `json:"subscriptions"` - UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + Description string `json:"description,omitempty"` + Tenant string `json:"tenant"` + Subscriptions []string `json:"subscriptions"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string type AzureResourceJsonStringGroupProps struct { - Description string `json:"DESCRIPTION,omitempty"` - Tenant string `json:"TENANT"` - Subscriptions []string `json:"SUBSCRIPTIONS"` - UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + Description string `json:"DESCRIPTION,omitempty"` + Tenant string `json:"TENANT"` + Subscriptions []string `json:"SUBSCRIPTIONS"` + UpdatedBy string `json:"UPDATED_BY,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props AzureResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props AzureResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + Tenant string `json:"tenant"` + Subscriptions []string `json:"subscriptions"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + Tenant: props.Tenant, + Subscriptions: props.Subscriptions, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/api/resource_groups_container.go b/api/resource_groups_container.go index 5e6b7fc8b..0688642e4 100644 --- a/api/resource_groups_container.go +++ b/api/resource_groups_container.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -128,7 +129,7 @@ type ContainerResourceGroupProps struct { ContainerLabels []map[string]string `json:"containerLabels"` ContainerTags []string `json:"containerTags"` UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string @@ -137,5 +138,30 @@ type ContainerResourceJsonStringGroupProps struct { ContainerLabels []map[string]string `json:"CONTAINER_LABELS"` ContainerTags []string `json:"CONTAINER_TAGS"` UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props ContainerResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props ContainerResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + ContainerLabels []map[string]string `json:"containerLabels"` + ContainerTags []string `json:"containerTags"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + ContainerLabels: props.ContainerLabels, + ContainerTags: props.ContainerTags, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/api/resource_groups_gcp.go b/api/resource_groups_gcp.go index add451574..c259fc516 100644 --- a/api/resource_groups_gcp.go +++ b/api/resource_groups_gcp.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -122,18 +123,43 @@ type GcpResourceGroupData struct { } type GcpResourceGroupProps struct { - Description string `json:"description,omitempty"` - Organization string `json:"organization"` - Projects []string `json:"projects"` - UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + Description string `json:"description,omitempty"` + Organization string `json:"organization"` + Projects []string `json:"projects"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string type GcpResourceGroupJsonStringProps struct { - Description string `json:"DESCRIPTION,omitempty"` - Organization string `json:"ORGANIZATION"` - Projects []string `json:"PROJECTS"` - UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + Description string `json:"DESCRIPTION,omitempty"` + Organization string `json:"ORGANIZATION"` + Projects []string `json:"PROJECTS"` + UpdatedBy string `json:"UPDATED_BY,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props GcpResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props GcpResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + Organization string `json:"organization"` + Projects []string `json:"projects"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + Organization: props.Organization, + Projects: props.Projects, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/api/resource_groups_lw_account.go b/api/resource_groups_lw_account.go index 7261a606b..8b31bee0e 100644 --- a/api/resource_groups_lw_account.go +++ b/api/resource_groups_lw_account.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -122,16 +123,39 @@ type LwAccountResourceGroupData struct { } type LwAccountResourceGroupProps struct { - Description string `json:"description,omitempty"` - LwAccounts []string `json:"lwAccounts"` - UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + Description string `json:"description,omitempty"` + LwAccounts []string `json:"lwAccounts"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string type LwAccountResourceGroupJsonStringProps struct { - Description string `json:"DESCRIPTION,omitempty"` - LwAccounts []string `json:"LW_ACCOUNTS"` - UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + Description string `json:"DESCRIPTION,omitempty"` + LwAccounts []string `json:"LW_ACCOUNTS"` + UpdatedBy string `json:"UPDATED_BY,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props LwAccountResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props LwAccountResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + LwAccounts []string `json:"lwAccounts"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + LwAccounts: props.LwAccounts, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/api/resource_groups_machine.go b/api/resource_groups_machine.go index 3debc4d53..e6c58b8c2 100644 --- a/api/resource_groups_machine.go +++ b/api/resource_groups_machine.go @@ -22,6 +22,7 @@ import ( "encoding/json" "strconv" + "github.com/lacework/go-sdk/lwtime" "github.com/pkg/errors" ) @@ -125,7 +126,7 @@ type MachineResourceGroupProps struct { Description string `json:"description,omitempty"` MachineTags []map[string]string `json:"machineTags"` UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` + LastUpdated *lwtime.Epoch `json:"lastUpdated,omitempty"` } // Workaround for props being returned as a json string @@ -133,5 +134,28 @@ type MachineResourceGroupJsonStringProps struct { Description string `json:"DESCRIPTION,omitempty"` MachineTags []map[string]string `json:"MACHINE_TAGS"` UpdatedBy string `json:"UPDATED_BY,omitempty"` - LastUpdated int `json:"LAST_UPDATED,omitempty"` + LastUpdated *lwtime.Epoch `json:"LAST_UPDATED,omitempty"` +} + +func (props MachineResourceGroupProps) GetBaseProps() ResourceGroupPropsBase { + return ResourceGroupPropsBase{ + Description: props.Description, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated, + } +} + +func (props MachineResourceGroupProps) MarshalJSON() ([]byte, error) { + res := struct { + Description string `json:"description,omitempty"` + MachineTags []map[string]string `json:"machineTags"` + UpdatedBy string `json:"updatedBy,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + }{ + Description: props.Description, + MachineTags: props.MachineTags, + UpdatedBy: props.UpdatedBy, + LastUpdated: props.LastUpdated.String(), + } + return json.Marshal(&res) } diff --git a/cli/cmd/resource_groups.go b/cli/cmd/resource_groups.go index 96c426df2..7c98fab73 100644 --- a/cli/cmd/resource_groups.go +++ b/cli/cmd/resource_groups.go @@ -21,7 +21,6 @@ package cmd import ( "encoding/json" "fmt" - "strconv" "github.com/AlecAivazis/survey/v2" "github.com/lacework/go-sdk/api" @@ -70,6 +69,7 @@ Then navigate to Settings > Resource Groups. groups := make([]resourceGroup, 0) for _, g := range resourceGroups.Data { + props, _ := parsePropsType(g) groups = append(groups, resourceGroup{ Id: g.ResourceGuid, @@ -78,6 +78,7 @@ Then navigate to Settings > Resource Groups. status: g.Status(), Enabled: g.Enabled, IsDefault: g.IsDefault, + Props: props, }) } @@ -110,7 +111,7 @@ Then navigate to Settings > Resource Groups. return errors.Wrap(err, "unable to get resource group") } - props, _ := parsePropsType(response) + props, _ := parsePropsType(response.Data) group := resourceGroup{ Id: response.Data.ResourceGuid, @@ -177,10 +178,10 @@ Then navigate to Settings > Resource Groups. ) // parsePropsType converts props json string to interface of resource group props type -func parsePropsType(response api.ResourceGroupResponse) (interface{}, error) { - propsString := response.Data.Props.(string) +func parsePropsType(response api.ResourceGroupData) (api.ResourceGroupProps, error) { + propsString := response.Props.(string) - switch response.Data.Type { + switch response.Type { case api.AwsResourceGroup.String(): return unmarshallAwsPropString([]byte(propsString)) case api.AzureResourceGroup.String(): @@ -195,7 +196,6 @@ func parsePropsType(response api.ResourceGroupResponse) (interface{}, error) { return unmarshallMachinePropString([]byte(propsString)) } return nil, errors.New("Unable to determine resource group props type") - } func promptCreateResourceGroup() error { @@ -255,12 +255,12 @@ func buildResourceGroupPropsTable(group resourceGroup) string { ) } -func determineResourceGroupProps(resType string, props interface{}) [][]string { +func determineResourceGroupProps(resType string, props api.ResourceGroupProps) [][]string { propsString, err := json.Marshal(props) if err != nil { return [][]string{} } - details := setBaseProps(propsString) + details := setBaseProps(props) switch resType { case api.AwsResourceGroup.String(): @@ -280,20 +280,14 @@ func determineResourceGroupProps(resType string, props interface{}) [][]string { return details } -func setBaseProps(props []byte) [][]string { +func setBaseProps(props api.ResourceGroupProps) [][]string { var ( - baseProps resourceGroupPropsBase - details [][]string + details [][]string ) - - err := json.Unmarshal(props, &baseProps) - if err != nil { - return [][]string{} - } - - details = append(details, []string{"DESCRIPTION", baseProps.Description}) - details = append(details, []string{"UPDATED BY", baseProps.UpdatedBy}) - details = append(details, []string{"LAST UPDATED", strconv.Itoa(baseProps.LastUpdated)}) + lastUpdated := props.GetBaseProps().LastUpdated + details = append(details, []string{"DESCRIPTION", props.GetBaseProps().Description}) + details = append(details, []string{"UPDATED BY", props.GetBaseProps().UpdatedBy}) + details = append(details, []string{"LAST UPDATED", lastUpdated.String()}) return details } @@ -316,17 +310,11 @@ func IsDefault(isDefault int) string { } type resourceGroup struct { - Id string `json:"resource_guid"` - ResType string `json:"type"` - Name string `json:"name"` - Props interface{} `json:"props"` - Enabled int `json:"enabled"` - IsDefault int `json:"isDefault"` + Id string `json:"resource_guid"` + ResType string `json:"type"` + Name string `json:"name"` + Props api.ResourceGroupProps `json:"props"` + Enabled int `json:"enabled"` + IsDefault int `json:"isDefault"` status string } - -type resourceGroupPropsBase struct { - Description string `json:"description"` - UpdatedBy string `json:"updatedBy,omitempty"` - LastUpdated int `json:"lastUpdated,omitempty"` -} diff --git a/lwtime/epoch.go b/lwtime/epoch.go index ae19f29da..4a924ce67 100644 --- a/lwtime/epoch.go +++ b/lwtime/epoch.go @@ -1,6 +1,7 @@ package lwtime import ( + "fmt" "strconv" "time" ) @@ -16,9 +17,9 @@ func (epoch *Epoch) UnmarshalJSON(b []byte) error { return nil } -func (epoch *Epoch) MarshalJSON() ([]byte, error) { - // @afiune we might have problems changing the location :( - return epoch.ToTime().UTC().MarshalJSON() +func (epoch Epoch) MarshalJSON() ([]byte, error) { + epochJson := fmt.Sprintf("%v", epoch.ToTime().UnixNano()/int64(time.Millisecond)) + return []byte(epochJson), nil } // A few format functions for printing and manipulating the custom date @@ -31,3 +32,10 @@ func (epoch Epoch) Format(s string) string { func (epoch Epoch) UTC() time.Time { return epoch.ToTime().UTC() } + +func (epoch *Epoch) String() string { + if epoch != nil { + return epoch.UTC().Format(time.RFC3339) + } + return "" +} diff --git a/lwtime/epoch_test.go b/lwtime/epoch_test.go new file mode 100644 index 000000000..9f3436184 --- /dev/null +++ b/lwtime/epoch_test.go @@ -0,0 +1,25 @@ +package lwtime + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockLastUpdatedResponse struct { + LastUpdatedTime Epoch `json:"last_updated"` +} + +type mockLastUpdatedIntResponse struct { + LastUpdatedTime int `json:"last_updated"` +} + +func TestMarshallEpoch(t *testing.T) { + var res mockLastUpdatedResponse + lastUpdated := mockLastUpdatedIntResponse{LastUpdatedTime: 1635604492078} + lastUpdatedString, _ := json.Marshal(lastUpdated) + json.Unmarshal(lastUpdatedString, &res) + expected, _ := json.Marshal(lastUpdated) + assert.Equal(t, "{\"last_updated\":1635604492078}", string(expected)) +}