From ef65e232b7c7c650562db5cded0b3ee31a461a55 Mon Sep 17 00:00:00 2001 From: Darren <75614232+dmurray-lacework@users.noreply.github.com> Date: Fri, 7 Jan 2022 13:20:53 +0000 Subject: [PATCH] feat(api): Vulnerability Exceptions v2 Service (#627) --- .../vulnerability-exceptions/main.go | 72 +++ api/api.go | 3 + api/schemas.go | 3 +- api/v2.go | 43 +- api/vulnerability_exceptions.go | 450 ++++++++++++++++++ api/vulnerability_exceptions_container.go | 85 ++++ api/vulnerability_exceptions_host.go | 84 ++++ api/vulnerability_exceptions_test.go | 446 +++++++++++++++++ 8 files changed, 1165 insertions(+), 21 deletions(-) create mode 100644 api/_examples/vulnerability-exceptions/main.go create mode 100644 api/vulnerability_exceptions.go create mode 100644 api/vulnerability_exceptions_container.go create mode 100644 api/vulnerability_exceptions_host.go create mode 100644 api/vulnerability_exceptions_test.go diff --git a/api/_examples/vulnerability-exceptions/main.go b/api/_examples/vulnerability-exceptions/main.go new file mode 100644 index 000000000..b1f9a9c0e --- /dev/null +++ b/api/_examples/vulnerability-exceptions/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/lacework/go-sdk/api" +) + +func main() { + lacework, err := api.NewClient(os.Getenv("LW_ACCOUNT"), + api.WithSubaccount(os.Getenv("LW_SUBACCOUNT")), + api.WithApiKeys(os.Getenv("LW_API_KEY"), os.Getenv("LW_API_SECRET")), + api.WithApiV2(), + ) + if err != nil { + log.Fatal(err) + } + + res, err := lacework.V2.VulnerabilityExceptions.List() + if err != nil { + log.Fatal(err) + } + + for _, exception := range res.Data { + support := "Unsupported" + switch exception.ExceptionType { + case api.VulnerabilityExceptionTypeHost.String(): + support = "Supported" + case api.VulnerabilityExceptionTypeContainer.String(): + support = "Supported" + } + + // Output: GUID:VULN_EXCEPTION_TYPE:[Supported|Unsupported] + fmt.Printf("%s:%s:%s\n", exception.Guid, exception.ExceptionType, support) + } + + exception := api.VulnerabilityExceptionConfig{ + Type: api.VulnerabilityExceptionTypeHost, + Description: "This is a vuln exception", + ExceptionReason: api.VulnerabilityExceptionReasonCompensatingControls, + Severities: api.VulnerabilityExceptionSeverities{api.VulnerabilityExceptionSeverityCritical}, + Fixable: true, + Package: []api.VulnerabilityExceptionPackage{{Name: "PackageOne", Version: "1.0.0"}}, + ResourceScope: api.VulnerabilityExceptionContainerResourceScope{ + ImageTag: []string{"MyImage"}, + }, + ExpiryTime: time.Now().AddDate(0, 1, 0), + } + + myVulnException := api.NewVulnerabilityException("MyVulnException", + exception, + ) + + response, err := lacework.V2.VulnerabilityExceptions.Create(myVulnException) + if err != nil { + log.Fatal(err) + } + + // Output: Vulnerability Exception created: GUID + fmt.Printf("Vulnerability Exception created: %s", response.Data.Guid) + + err = lacework.V2.VulnerabilityExceptions.Delete(response.Data.Guid) + if err != nil { + log.Fatal(err) + } + + // Output: Vulnerability Exception deleted: GUID + fmt.Printf("Vulnerability Exception deleted: %s", response.Data.Guid) +} diff --git a/api/api.go b/api/api.go index f8e39a318..fa3bb9e8b 100644 --- a/api/api.go +++ b/api/api.go @@ -116,6 +116,9 @@ const ( apiV2TeamMembers = "v2/TeamMembers" apiV2TeamMembersFromGUID = "v2/TeamMembers/%s" apiV2TeamMembersSearch = "v2/TeamMembers/search" + + apiV2VulnerabilityExceptions = "v2/VulnerabilityExceptions" + apiV2VulnerabilityExceptionFromGUID = "v2/VulnerabilityExceptions/%s" ) // WithApiV2 configures the client to use the API version 2 (/api/v2) diff --git a/api/schemas.go b/api/schemas.go index 53fe9abd0..50f83dea2 100644 --- a/api/schemas.go +++ b/api/schemas.go @@ -33,8 +33,9 @@ const ( ContainerRegistries CloudAccounts ResourceGroups - TeamMembers ReportRules + TeamMembers + VulnerabilityExceptions ) func (svc *SchemasService) GetService(schemaName integrationSchema) V2Service { diff --git a/api/v2.go b/api/v2.go index aede19755..b17d31daa 100644 --- a/api/v2.go +++ b/api/v2.go @@ -24,19 +24,20 @@ type V2Endpoints struct { client *Client // Every schema must have its own service - UserProfile *UserProfileService - AlertChannels *AlertChannelsService - AlertRules *AlertRulesService - ReportRules *ReportRulesService - CloudAccounts *CloudAccountsService - ContainerRegistries *ContainerRegistriesService - ResourceGroups *ResourceGroupsService - AgentAccessTokens *AgentAccessTokensService - Query *QueryService - Policy *PolicyService - Schemas *SchemasService - Datasources *DatasourcesService - TeamMembers *TeamMembersService + UserProfile *UserProfileService + AlertChannels *AlertChannelsService + AlertRules *AlertRulesService + ReportRules *ReportRulesService + CloudAccounts *CloudAccountsService + ContainerRegistries *ContainerRegistriesService + ResourceGroups *ResourceGroupsService + AgentAccessTokens *AgentAccessTokensService + Query *QueryService + Policy *PolicyService + Schemas *SchemasService + Datasources *DatasourcesService + TeamMembers *TeamMembersService + VulnerabilityExceptions *VulnerabilityExceptionsService } func NewV2Endpoints(c *Client) *V2Endpoints { @@ -54,16 +55,18 @@ func NewV2Endpoints(c *Client) *V2Endpoints { &SchemasService{c, map[integrationSchema]V2Service{}}, &DatasourcesService{c}, &TeamMembersService{c}, + &VulnerabilityExceptionsService{c}, } v2.Schemas.Services = map[integrationSchema]V2Service{ - AlertChannels: &AlertChannelsService{c}, - AlertRules: &AlertRulesService{c}, - CloudAccounts: &CloudAccountsService{c}, - ContainerRegistries: &ContainerRegistriesService{c}, - ResourceGroups: &ResourceGroupsService{c}, - TeamMembers: &TeamMembersService{c}, - ReportRules: &ReportRulesService{c}, + AlertChannels: &AlertChannelsService{c}, + AlertRules: &AlertRulesService{c}, + CloudAccounts: &CloudAccountsService{c}, + ContainerRegistries: &ContainerRegistriesService{c}, + ResourceGroups: &ResourceGroupsService{c}, + TeamMembers: &TeamMembersService{c}, + ReportRules: &ReportRulesService{c}, + VulnerabilityExceptions: &VulnerabilityExceptionsService{c}, } return v2 } diff --git a/api/vulnerability_exceptions.go b/api/vulnerability_exceptions.go new file mode 100644 index 000000000..3a44e4f9e --- /dev/null +++ b/api/vulnerability_exceptions.go @@ -0,0 +1,450 @@ +// +// 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 + +import ( + "fmt" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/lacework/go-sdk/lwtime" +) + +// VulnerabilityExceptionsService is the service that interacts with +// the VulnerabilityExceptions schema from the Lacework APIv2 Server +type VulnerabilityExceptionsService struct { + client *Client +} + +// vulnerabilityExceptionResourceScope is an interface for the 2 types of vulnerability exceptions resource scopes: +// 'VulnerabilityExceptionContainerResourceScope' or 'VulnerabilityExceptionHostResourceScope' +type vulnerabilityExceptionResourceScope interface { + Type() vulnerabilityExceptionType + Scope() VulnerabilityExceptionResourceScope +} + +// vulnerabilityExceptionReason represents the types of vulnerability exceptions reasons: +//'False Positive', 'Accepted Risk', 'Compensating Controls', 'Fix Pending' or 'Other' +type vulnerabilityExceptionReason int + +const ( + VulnerabilityExceptionReasonAcceptedRisk vulnerabilityExceptionReason = iota + VulnerabilityExceptionReasonAcceptedFalsePositive + VulnerabilityExceptionReasonCompensatingControls + VulnerabilityExceptionReasonFixPending + VulnerabilityExceptionReasonOther + VulnerabilityExceptionReasonUnknown +) + +var VulnerabilityExceptionReasons = map[vulnerabilityExceptionReason]string{ + VulnerabilityExceptionReasonAcceptedRisk: "Accepted Risk", + VulnerabilityExceptionReasonAcceptedFalsePositive: "False Positive", + VulnerabilityExceptionReasonCompensatingControls: "Compensating Controls", + VulnerabilityExceptionReasonFixPending: "Fix Pending", + VulnerabilityExceptionReasonOther: "Other", + VulnerabilityExceptionReasonUnknown: "Unknown", +} + +func (i vulnerabilityExceptionReason) String() string { + return VulnerabilityExceptionReasons[i] +} + +func NewVulnerabilityExceptionReason(reason string) vulnerabilityExceptionReason { + switch reason { + case "Accepted Risk": + return VulnerabilityExceptionReasonAcceptedRisk + case "False Positive": + return VulnerabilityExceptionReasonAcceptedFalsePositive + case "Compensating Controls": + return VulnerabilityExceptionReasonCompensatingControls + case "Fix Pending": + return VulnerabilityExceptionReasonFixPending + case "Other": + return VulnerabilityExceptionReasonOther + default: + return VulnerabilityExceptionReasonUnknown + } +} + +// vulnerabilityExceptionType represents the types of vulnerability exceptions 'Host' or 'Container' +type vulnerabilityExceptionType int + +const ( + VulnerabilityExceptionTypeHost vulnerabilityExceptionType = iota + VulnerabilityExceptionTypeContainer +) + +var VulnerabilityExceptionTypes = map[vulnerabilityExceptionType]string{ + VulnerabilityExceptionTypeHost: "Host", + VulnerabilityExceptionTypeContainer: "Container", +} + +func (i vulnerabilityExceptionType) String() string { + return VulnerabilityExceptionTypes[i] +} + +// vulnerabilityExceptionSeverity represents the types of vulnerability severities: +// 'Critical', 'High', 'Medium', 'Low' or 'Info' +type vulnerabilityExceptionSeverity string + +type VulnerabilityExceptionSeverities []vulnerabilityExceptionSeverity + +func (sevs VulnerabilityExceptionSeverities) ToStringSlice() []string { + var res []string + for _, i := range sevs { + switch i { + case VulnerabilityExceptionSeverityCritical: + res = append(res, "Critical") + case VulnerabilityExceptionSeverityHigh: + res = append(res, "High") + case VulnerabilityExceptionSeverityMedium: + res = append(res, "Medium") + case VulnerabilityExceptionSeverityLow: + res = append(res, "Low") + case VulnerabilityExceptionSeverityInfo: + res = append(res, "Info") + default: + continue + } + } + return res +} + +func NewVulnerabilityExceptionSeverities(sevSlice []string) VulnerabilityExceptionSeverities { + var res VulnerabilityExceptionSeverities + for _, i := range sevSlice { + sev := convertVulnerabilityExceptionSeverity(i) + if sev != VulnerabilityExceptionSeverityUnknown { + res = append(res, sev) + } + } + return res +} + +func convertVulnerabilityExceptionSeverity(sev string) vulnerabilityExceptionSeverity { + switch strings.ToLower(sev) { + case "critical": + return VulnerabilityExceptionSeverityCritical + case "high": + return VulnerabilityExceptionSeverityHigh + case "medium": + return VulnerabilityExceptionSeverityMedium + case "low": + return VulnerabilityExceptionSeverityLow + case "info": + return VulnerabilityExceptionSeverityInfo + default: + return VulnerabilityExceptionSeverityUnknown + } +} + +const ( + VulnerabilityExceptionSeverityCritical vulnerabilityExceptionSeverity = "Critical" + VulnerabilityExceptionSeverityHigh vulnerabilityExceptionSeverity = "High" + VulnerabilityExceptionSeverityMedium vulnerabilityExceptionSeverity = "Medium" + VulnerabilityExceptionSeverityLow vulnerabilityExceptionSeverity = "Low" + VulnerabilityExceptionSeverityInfo vulnerabilityExceptionSeverity = "Info" + VulnerabilityExceptionSeverityUnknown vulnerabilityExceptionSeverity = "Unknown" +) + +// NewVulnerabilityException returns an instance of the VulnerabilityException struct +// +// Basic usage: Initialize a new VulnerabilityException struct, then +// use the new instance to do CRUD operations +// +// client, err := api.NewClient("account") +// if err != nil { +// return err +// } +// +// exception := api.VulnerabilityExceptionConfig{ +// Type: api.VulnerabilityExceptionTypeHost, +// Description: "This is a vuln exception", +// ExceptionReason: api.VulnerabilityExceptionReasonCompensatingControls, +// Severities: api.VulnerabilityExceptionSeverities{api.VulnerabilityExceptionSeverityCritical}, +// Fixable: true, +// ResourceScope: api.VulnerabilityExceptionContainerResourceScope{ +// ImageID: []string{""}, +// ImageTag: []string{""}, +// Registry: []string{""}, +// Repository: []string{""}, +// Namespace: []string{""}, +// }, +// ExpiryTime: time.Now().AddDate(0, 1, 0), +// } +// +// vulnerabilityException := api.NewVulnerabilityException("vulnerabilityException", exception) +// +// client.V2.VulnerabilityExceptions.Create(vulnerabilityException) +// +func NewVulnerabilityException(name string, exception VulnerabilityExceptionConfig) VulnerabilityException { + packages := aggregatePackages(exception.Package) + vulnException := VulnerabilityException{ + Enabled: 1, + ExceptionName: name, + ExceptionReason: exception.ExceptionReason.String(), + Props: VulnerabilityExceptionProps{Description: exception.Description}, + VulnerabilityCriteria: VulnerabilityExceptionCriteria{ + Severity: exception.Severities.ToStringSlice(), + Package: packages, + Cve: exception.Cve, + Fixable: exception.FixableEnabled(), + }, + ExpiryTime: exception.ExpiryTime.UTC().Format(lwtime.RFC3339Milli), + } + vulnException.setResourceScope(exception.ResourceScope) + return vulnException +} + +func aggregatePackages(packages []VulnerabilityExceptionPackage) []map[string][]string { + var packs []map[string][]string + for _, pck := range packages { + var packagesMap = make(map[string][]string) + //aggregate packages with same name + if len(packs) > 0 { + if _, ok := packs[0][pck.Name]; ok { + packs[0][pck.Name] = append(packs[0][pck.Name], pck.Version) + continue + } + } + packagesMap[pck.Name] = []string{pck.Version} + packs = append(packs, packagesMap) + } + return packs +} + +func (exception *VulnerabilityException) setResourceScope(scope vulnerabilityExceptionResourceScope) { + switch scope.Type() { + case VulnerabilityExceptionTypeContainer: + ctr := scope.Scope() + exception.ExceptionType = VulnerabilityExceptionTypeContainer.String() + exception.ResourceScope = VulnerabilityExceptionResourceScope{ + ImageID: ctr.ImageID, + ImageTag: ctr.ImageTag, + Registry: ctr.Registry, + Repository: ctr.Repository, + Namespace: ctr.Namespace, + } + case VulnerabilityExceptionTypeHost: + host := scope.Scope() + exception.ExceptionType = VulnerabilityExceptionTypeHost.String() + exception.ResourceScope = VulnerabilityExceptionResourceScope{ + Hostname: host.Hostname, + ClusterName: host.ClusterName, + ExternalIP: host.ExternalIP, + Namespace: host.Namespace, + } + default: + exception.ResourceScope = VulnerabilityExceptionResourceScope{} + } +} + +func (exception VulnerabilityException) Status() string { + if exception.Enabled == 1 { + return "Enabled" + } + return "Disabled" +} + +func (cfg VulnerabilityExceptionConfig) FixableEnabled() []int { + if cfg.Fixable { + return []int{1} + } + return []int{0} +} + +// List returns a list of Vulnerability Exceptions +func (svc *VulnerabilityExceptionsService) List() (response VulnerabilityExceptionsResponse, err error) { + err = svc.client.RequestDecoder("GET", apiV2VulnerabilityExceptions, nil, &response) + return +} + +// Create creates a single Vulnerability Exception +func (svc *VulnerabilityExceptionsService) Create(vuln VulnerabilityException) ( + response VulnerabilityExceptionResponse, + err error, +) { + err = svc.client.RequestEncoderDecoder("POST", apiV2VulnerabilityExceptions, vuln, &response) + return +} + +// Delete deletes a Vulnerability Exception that matches the provided guid +func (svc *VulnerabilityExceptionsService) Delete(guid string) error { + if guid == "" { + return errors.New("specify an intgGuid") + } + + return svc.client.RequestDecoder( + "DELETE", + fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, guid), + nil, + nil, + ) +} + +// Update updates a single Vulnerability Exception. +func (svc *VulnerabilityExceptionsService) Update(data VulnerabilityException) ( + response VulnerabilityExceptionResponse, + err error, +) { + if data.Guid == "" { + err = errors.New("specify a Guid") + return + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, data.Guid) + // Request is invalid if it contains the ID field. We set the id field to empty + data.Guid = "" + err = svc.client.RequestEncoderDecoder("PATCH", apiPath, data, &response) + return +} + +// Get returns a raw response of the Vulnerability Exception with the matching guid. +func (svc *VulnerabilityExceptionsService) Get(guid string, response interface{}) error { + if guid == "" { + return errors.New("specify a Guid") + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, guid) + return svc.client.RequestDecoder("GET", apiPath, nil, &response) +} + +type VulnerabilityExceptionConfig struct { + Description string + Type vulnerabilityExceptionType + ExceptionReason vulnerabilityExceptionReason + Severities VulnerabilityExceptionSeverities + Cve []string + Package []VulnerabilityExceptionPackage + Fixable bool + ResourceScope vulnerabilityExceptionResourceScope + ExpiryTime time.Time +} + +type VulnerabilityExceptionContainerResourceScope struct { + ImageID []string `json:"imageId,omitempty"` + ImageTag []string `json:"imageTag,omitempty"` + Registry []string `json:"registry,omitempty"` + Repository []string `json:"repository,omitempty"` + Namespace []string `json:"namespace,omitempty"` +} + +func (ctr VulnerabilityExceptionContainerResourceScope) Type() vulnerabilityExceptionType { + return VulnerabilityExceptionTypeContainer +} + +func (ctr VulnerabilityExceptionContainerResourceScope) Scope() VulnerabilityExceptionResourceScope { + return VulnerabilityExceptionResourceScope{ + ImageID: ctr.ImageID, + ImageTag: ctr.ImageTag, + Registry: ctr.Registry, + Repository: ctr.Repository, + Namespace: ctr.Namespace, + } +} + +func (host VulnerabilityExceptionHostResourceScope) Scope() VulnerabilityExceptionResourceScope { + return VulnerabilityExceptionResourceScope{ + Hostname: host.Hostname, + ExternalIP: host.ExternalIP, + ClusterName: host.ClusterName, + Namespace: host.Namespace, + } +} + +type VulnerabilityExceptionHostResourceScope struct { + Hostname []string `json:"hostname,omitempty"` + ExternalIP []string `json:"externalIp,omitempty"` + ClusterName []string `json:"clusterName,omitempty"` + Namespace []string `json:"namespace,omitempty"` +} + +func (host VulnerabilityExceptionHostResourceScope) Type() vulnerabilityExceptionType { + return VulnerabilityExceptionTypeHost +} + +type VulnerabilityException struct { + Guid string `json:"exceptionGuid,omitempty"` + Enabled int `json:"state"` + ExceptionName string `json:"exceptionName"` + ExceptionType string `json:"exceptionType"` + ExceptionReason string `json:"exceptionReason"` + Props VulnerabilityExceptionProps `json:"props"` + VulnerabilityCriteria VulnerabilityExceptionCriteria `json:"vulnerabilityCriteria"` + ResourceScope VulnerabilityExceptionResourceScope `json:"resourceScope,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + UpdatedTime string `json:"updatedTime,omitempty"` + ExpiryTime string `json:"expiryTime,omitempty"` +} + +type VulnerabilityExceptionProps struct { + Description string `json:"description,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` +} + +type VulnerabilityExceptionResourceScope struct { + // Container properties + ImageID []string `json:"imageId,omitempty"` + ImageTag []string `json:"imageTag,omitempty"` + Registry []string `json:"registry,omitempty"` + Repository []string `json:"repository,omitempty"` + + // Host properties + Hostname []string `json:"hostname,omitempty"` + ExternalIP []string `json:"externalIp,omitempty"` + ClusterName []string `json:"clusterName,omitempty"` + + // Shared properties + Namespace []string `json:"namespace,omitempty"` +} + +type VulnerabilityExceptionCriteria struct { + Cve []string `json:"cve,omitempty"` + Package []map[string][]string `json:"package,omitempty"` + Severity []string `json:"severity,omitempty"` + Fixable []int `json:"fixable,omitempty"` +} + +type VulnerabilityExceptionResponse struct { + Data VulnerabilityException `json:"data"` +} + +type VulnerabilityExceptionsResponse struct { + Data []VulnerabilityException `json:"data"` +} + +type VulnerabilityExceptionPackage struct { + Name string + Version string +} + +func NewVulnerabilityExceptionPackages(packageMap []map[string]string) []VulnerabilityExceptionPackage { + var packages []VulnerabilityExceptionPackage + for _, m := range packageMap { + for k, v := range m { + pck := VulnerabilityExceptionPackage{ + Name: k, + Version: v, + } + packages = append(packages, pck) + } + } + return packages +} diff --git a/api/vulnerability_exceptions_container.go b/api/vulnerability_exceptions_container.go new file mode 100644 index 000000000..eb04ef843 --- /dev/null +++ b/api/vulnerability_exceptions_container.go @@ -0,0 +1,85 @@ +// +// 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 + +import ( + "fmt" + + "github.com/pkg/errors" +) + +func (svc *VulnerabilityExceptionsService) CreateVulnerabilityExceptionsContainer(vuln VulnerabilityException) ( + response VulnerabilityExceptionContainerResponse, err error) { + err = svc.client.RequestEncoderDecoder("POST", apiV2VulnerabilityExceptions, vuln, &response) + return +} + +func (svc *VulnerabilityExceptionsService) GetVulnerabilityExceptionsContainer(guid string) (response VulnerabilityExceptionContainerResponse, err error) { + if guid == "" { + err = errors.New("specify a Guid") + return + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, guid) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *VulnerabilityExceptionsService) ListVulnerabilityExceptionsContainers() (response VulnerabilityExceptionContainerResponse, err error) { + err = svc.client.RequestDecoder("GET", apiV2VulnerabilityExceptions, nil, &response) + return +} + +func (svc *VulnerabilityExceptionsService) UpdateVulnerabilityExceptionsContainer(data VulnerabilityException, id string) ( + response VulnerabilityExceptionContainerResponse, + err error, +) { + if id == "" { + err = errors.New("specify a Guid") + return + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, id) + err = svc.client.RequestEncoderDecoder("PATCH", apiPath, data, &response) + return +} + +type VulnerabilityExceptionContainerResponse struct { + Data VulnerabilityExceptionContainer `json:"data"` +} + +type VulnerabilityExceptionContainer struct { + Guid string `json:"exceptionGuid,omitempty"` + Enabled int `json:"state"` + ExceptionName string `json:"exceptionName"` + ExceptionType string `json:"exceptionType"` + ExceptionReason string `json:"exceptionReason"` + Props VulnerabilityExceptionProps `json:"props"` + VulnerabilityCriteria VulnerabilityExceptionCriteria `json:"vulnerabilityCriteria"` + ResourceScope VulnerabilityExceptionResourceScopeContainer `json:"resourceScope,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + UpdatedTime string `json:"updatedTime,omitempty"` + ExpiryTime string `json:"expiryTime,omitempty"` +} + +type VulnerabilityExceptionResourceScopeContainer struct { + ImageID []string `json:"imageId,omitempty"` + ImageTag []string `json:"imageTag,omitempty"` + Registry []string `json:"registry,omitempty"` + Repository []string `json:"repository,omitempty"` + Namespace []string `json:"namespace,omitempty"` +} diff --git a/api/vulnerability_exceptions_host.go b/api/vulnerability_exceptions_host.go new file mode 100644 index 000000000..c090d3b6a --- /dev/null +++ b/api/vulnerability_exceptions_host.go @@ -0,0 +1,84 @@ +// +// 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 + +import ( + "fmt" + + "github.com/pkg/errors" +) + +func (svc *VulnerabilityExceptionsService) CreateVulnerabilityExceptionsHost(vuln VulnerabilityException) ( + response VulnerabilityExceptionHostResponse, err error) { + err = svc.client.RequestEncoderDecoder("POST", apiV2VulnerabilityExceptions, vuln, &response) + return +} + +func (svc *VulnerabilityExceptionsService) GetVulnerabilityExceptionsHost(guid string) (response VulnerabilityExceptionHostResponse, err error) { + if guid == "" { + err = errors.New("specify a Guid") + return + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, guid) + err = svc.client.RequestDecoder("GET", apiPath, nil, &response) + return +} + +func (svc *VulnerabilityExceptionsService) ListVulnerabilityExceptionsHosts() (response VulnerabilityExceptionHostResponse, err error) { + err = svc.client.RequestDecoder("GET", apiV2VulnerabilityExceptions, nil, &response) + return +} + +func (svc *VulnerabilityExceptionsService) UpdateVulnerabilityExceptionsHost(data VulnerabilityException, id string) ( + response VulnerabilityExceptionHostResponse, + err error, +) { + if id == "" { + err = errors.New("specify a Guid") + return + } + apiPath := fmt.Sprintf(apiV2VulnerabilityExceptionFromGUID, id) + err = svc.client.RequestEncoderDecoder("PATCH", apiPath, data, &response) + return +} + +type VulnerabilityExceptionHostResponse struct { + Data VulnerabilityExceptionHost `json:"data"` +} + +type VulnerabilityExceptionHost struct { + Guid string `json:"exceptionGuid,omitempty"` + Enabled int `json:"state"` + ExceptionName string `json:"exceptionName"` + ExceptionType string `json:"exceptionType"` + ExceptionReason string `json:"exceptionReason"` + Props VulnerabilityExceptionProps `json:"props"` + VulnerabilityCriteria VulnerabilityExceptionCriteria `json:"vulnerabilityCriteria"` + ResourceScope VulnerabilityExceptionResourceScopeHost `json:"resourceScope,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + UpdatedTime string `json:"updatedTime,omitempty"` + ExpiryTime string `json:"expiryTime,omitempty"` +} + +type VulnerabilityExceptionResourceScopeHost struct { + Hostname []string `json:"hostname,omitempty"` + ExternalIP []string `json:"externalIp,omitempty"` + ClusterName []string `json:"clusterName,omitempty"` + Namespace []string `json:"namespace,omitempty"` +} diff --git a/api/vulnerability_exceptions_test.go b/api/vulnerability_exceptions_test.go new file mode 100644 index 000000000..e88a77663 --- /dev/null +++ b/api/vulnerability_exceptions_test.go @@ -0,0 +1,446 @@ +// +// 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_test + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/lacework/go-sdk/api" + "github.com/lacework/go-sdk/internal/intgguid" + "github.com/lacework/go-sdk/internal/lacework" +) + +func TestNewVulnerabilityException(t *testing.T) { + mypackages := []api.VulnerabilityExceptionPackage{ + { + Name: "PackageOne", + Version: "1.2.3", + }, + { + Name: "PackageOne", + Version: "1.2.4", + }, { + Name: "PackageTwo", + Version: "1.2.3", + }, + } + + vulnerabilityException := api.NewVulnerabilityException("MyVulnException", + api.VulnerabilityExceptionConfig{ + Type: api.VulnerabilityExceptionTypeHost, + Description: "This is a vuln exception", + ExceptionReason: api.VulnerabilityExceptionReasonCompensatingControls, + Severities: api.VulnerabilityExceptionSeverities{api.VulnerabilityExceptionSeverityCritical}, + Fixable: true, + Package: mypackages, + ResourceScope: api.VulnerabilityExceptionHostResourceScope{ + Hostname: []string{"exampleHost1"}, + }, + ExpiryTime: time.UnixMilli(1642187718414), + }) + + vulnJson, err := json.Marshal(vulnerabilityException) + + assert.NoError(t, err) + assert.Equal(t, mockVulnHostRequest, string(vulnJson)) + assert.Equal(t, []string{"1.2.3", "1.2.4"}, vulnerabilityException.VulnerabilityCriteria.Package[0]["PackageOne"]) + assert.Equal(t, []string{"Critical"}, vulnerabilityException.VulnerabilityCriteria.Severity) +} + +func TestHostVulnerabilityExceptionGet(t *testing.T) { + var ( + intgGUID = intgguid.New() + apiPath = fmt.Sprintf("VulnerabilityExceptions/%s", intgGUID) + vulnException = singleMockHostVulnerabilityException(intgGUID) + fakeServer = lacework.MockServer() + ) + fakeServer.UseApiV2() + fakeServer.MockToken("TOKEN") + defer fakeServer.Close() + + fakeServer.MockAPI(apiPath, + func(w http.ResponseWriter, r *http.Request) { + if assert.Equal(t, "GET", r.Method, "Get() should be a GET method") { + fmt.Fprintf(w, generateVulnerabilityExceptionResponse(vulnException)) + } + }, + ) + + fakeServer.MockAPI("VulnerabilityExceptions/UNKNOWN_INTG_GUID", + func(w http.ResponseWriter, r *http.Request) { + if assert.Equal(t, "GET", r.Method, "Get() should be a GET method") { + http.Error(w, "{ \"message\": \"Not Found\"}", 404) + } + }, + ) + + c, err := api.NewClient("test", + api.WithApiV2(), + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + t.Run("when vulnerability exception exists", func(t *testing.T) { + var response api.VulnerabilityExceptionResponse + err := c.V2.VulnerabilityExceptions.Get(intgGUID, &response) + assert.Nil(t, err) + if assert.NotNil(t, response) { + assert.Equal(t, intgGUID, response.Data.Guid) + assert.Equal(t, "Example Vuln Exception", response.Data.ExceptionName) + assert.Equal(t, "Host", response.Data.ExceptionType) + } + }) + + t.Run("when vulnerability exception does NOT exist", func(t *testing.T) { + var response api.VulnerabilityExceptionResponse + err := c.V2.VulnerabilityExceptions.Get("UNKNOWN_INTG_GUID", response) + assert.Empty(t, response) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "api/v2/VulnerabilityExceptions/UNKNOWN_INTG_GUID") + assert.Contains(t, err.Error(), "[404] Not Found") + } + }) +} + +func TestCtrVulnerabilityExceptionGet(t *testing.T) { + var ( + intgGUID = intgguid.New() + apiPath = fmt.Sprintf("VulnerabilityExceptions/%s", intgGUID) + vulnException = singleMockCtrVulnerabilityException(intgGUID) + fakeServer = lacework.MockServer() + ) + fakeServer.UseApiV2() + fakeServer.MockToken("TOKEN") + defer fakeServer.Close() + + fakeServer.MockAPI(apiPath, + func(w http.ResponseWriter, r *http.Request) { + if assert.Equal(t, "GET", r.Method, "Get() should be a GET method") { + fmt.Fprintf(w, generateVulnerabilityExceptionResponse(vulnException)) + } + }, + ) + + fakeServer.MockAPI("VulnerabilityExceptions/UNKNOWN_INTG_GUID", + func(w http.ResponseWriter, r *http.Request) { + if assert.Equal(t, "GET", r.Method, "Get() should be a GET method") { + http.Error(w, "{ \"message\": \"Not Found\"}", 404) + } + }, + ) + + c, err := api.NewClient("test", + api.WithApiV2(), + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + t.Run("when vulnerability exception exists", func(t *testing.T) { + var response api.VulnerabilityExceptionResponse + err := c.V2.VulnerabilityExceptions.Get(intgGUID, &response) + assert.Nil(t, err) + if assert.NotNil(t, response) { + assert.Equal(t, intgGUID, response.Data.Guid) + assert.Equal(t, "Example Vuln Exception", response.Data.ExceptionName) + assert.Equal(t, "Container", response.Data.ExceptionType) + assert.Equal(t, []string{"test1", "test2"}, response.Data.ResourceScope.Repository) + assert.Equal(t, []string{"test1", "test2"}, response.Data.ResourceScope.Registry) + assert.Equal(t, []string{"test1", "test2"}, response.Data.ResourceScope.ImageID) + assert.Equal(t, []string{"test1", "test2"}, response.Data.ResourceScope.Namespace) + } + }) + + t.Run("when vulnerability exception does NOT exist", func(t *testing.T) { + var response api.VulnerabilityExceptionResponse + err := c.V2.VulnerabilityExceptions.Get("UNKNOWN_INTG_GUID", response) + assert.Empty(t, response) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "api/v2/VulnerabilityExceptions/UNKNOWN_INTG_GUID") + assert.Contains(t, err.Error(), "[404] Not Found") + } + }) +} + +func TestVulnerabilityExceptionsList(t *testing.T) { + var ( + allGUIDs []string + vulnerabilityExceptions = generateGuids(&allGUIDs, 3) + expectedLen = len(allGUIDs) + fakeServer = lacework.MockServer() + ) + + fakeServer.UseApiV2() + fakeServer.MockToken("TOKEN") + fakeServer.MockAPI("VulnerabilityExceptions", + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "List() should be a GET method") + vulnerabilityExceptions := []string{ + generateVulnerabilityExceptions(vulnerabilityExceptions), + } + fmt.Fprintf(w, + generateVulnerabilityExceptionsResponse( + strings.Join(vulnerabilityExceptions, ", "), + ), + ) + }, + ) + defer fakeServer.Close() + + c, err := api.NewClient("test", + api.WithApiV2(), + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + response, err := c.V2.VulnerabilityExceptions.List() + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, expectedLen, len(response.Data)) + for _, d := range response.Data { + assert.Contains(t, allGUIDs, d.Guid) + } +} + +func TestVulnerabilityExceptionUpdate(t *testing.T) { + var ( + intgGUID = intgguid.New() + apiPath = fmt.Sprintf("VulnerabilityExceptions/%s", intgGUID) + fakeServer = lacework.MockServer() + ) + fakeServer.UseApiV2() + fakeServer.MockToken("TOKEN") + defer fakeServer.Close() + + fakeServer.MockAPI(apiPath, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method, "Update() should be a PATCH method") + + if assert.NotNil(t, r.Body) { + body := httpBodySniffer(r) + assert.Contains(t, body, "MyVulnException", "vulnerability exception name is missing") + assert.Contains(t, body, "Host", "wrong vulnerability exception type") + } + + fmt.Fprintf(w, generateVulnerabilityExceptionResponse(singleMockHostVulnerabilityException(intgGUID))) + }) + + c, err := api.NewClient("test", + api.WithApiV2(), + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + vulnerabilityException := api.NewVulnerabilityException("MyVulnException", + api.VulnerabilityExceptionConfig{ + Type: api.VulnerabilityExceptionTypeHost, + Description: "This is a vuln exception", + ExceptionReason: api.VulnerabilityExceptionReasonCompensatingControls, + Severities: api.VulnerabilityExceptionSeverities{api.VulnerabilityExceptionSeverityCritical}, + Fixable: true, + Package: []api.VulnerabilityExceptionPackage{{"pck", "1.0.0"}}, + ResourceScope: api.VulnerabilityExceptionHostResourceScope{ + Hostname: []string{"exampleHost1"}, + }, + ExpiryTime: time.UnixMilli(1642187874425), + }) + + assert.Equal(t, "MyVulnException", vulnerabilityException.ExceptionName, "vulnerability exception name mismatch") + assert.Equal(t, "Host", vulnerabilityException.ExceptionType, "a new vulnerability exception should match its type") + assert.Equal(t, 1, vulnerabilityException.Enabled, "a new vulnerability exception should be enabled") + + vulnerabilityException.Guid = intgGUID + response, err := c.V2.VulnerabilityExceptions.Update(vulnerabilityException) + if assert.NoError(t, err) { + assert.NotNil(t, response) + assert.Equal(t, intgGUID, response.Data.Guid) + assert.Equal(t, response.Data.VulnerabilityCriteria.Fixable[0], 1) + assert.Equal(t, response.Data.ResourceScope.Hostname[0], "exampleHost1") + assert.Equal(t, response.Data.VulnerabilityCriteria.Package[0]["pck"][0], "1.0.0") + } +} + +func generateVulnerabilityExceptions(guids []string) string { + vulnerabilityExceptions := make([]string, len(guids)) + for i, guid := range guids { + vulnerabilityExceptions[i] = singleMockHostVulnerabilityException(guid) + } + return strings.Join(vulnerabilityExceptions, ", ") +} + +func generateVulnerabilityExceptionsResponse(data string) string { + return ` + { + "data": [` + data + `] + } + ` +} + +func generateVulnerabilityExceptionResponse(data string) string { + return ` + { + "data": ` + data + ` + } + ` +} + +func singleMockHostVulnerabilityException(id string) string { + return fmt.Sprintf(` +{ + "createdTime": "2021-12-02T18:47:24.897Z", + "envGuid": "TECHALLY_3F75784E9C7660B1F8B4A72E80B2EECCE8EDA92DBE37E5D", + "exceptionGuid": %q, + "exceptionName": "Example Vuln Exception", + "exceptionReason": "Accepted Risk", + "exceptionType": "Host", + "expiryTime": "2021-12-31T18:47:07Z", + "props": { + "description": "This is a comment", + "createdBy": "darren.murray@lacework.net", + "updatedBy": "darren.murray@lacework.net" + }, + "resourceScope": { + "hostname": [ + "exampleHost1", + "exampleHost2" + ], + "externalIp": [ + "test1", + "test2" + ], + "clusterName": [ + "test1", + "test2" + ], + "namespace": [ + "test1", + "test2" + ] + }, + "state": 1, + "updatedTime": "2021-12-03T13:27:33.169Z", + "vulnerabilityCriteria": { + "cve": [ + "cve1", + "cve2" + ], + "fixable": [ + 1 + ], + "package": [ + { + "pck": [ + "1.0.0", + "2.0.0", + "3.0.0" + ] + } + ], + "severity": [ + "High", + "Medium", + "Low", + "Critical" + ] + } +} + `, id) +} + +func singleMockCtrVulnerabilityException(id string) string { + return fmt.Sprintf(` +{ + "createdTime": "2021-12-02T18:47:24.897Z", + "envGuid": "TECHALLY_3F75784E9C7660B1F8B4A72E80B2EECCE8EDA92DBE37E5D", + "exceptionGuid": %q, + "exceptionName": "Example Vuln Exception", + "exceptionReason": "Accepted Risk", + "exceptionType": "Container", + "expiryTime": "2021-12-31T18:47:07Z", + "props": { + "description": "This is a comment", + "createdBy": "darren.murray@lacework.net", + "updatedBy": "darren.murray@lacework.net" + }, + "resourceScope": { + "imageId": [ + "exampleId", + "exampleId2" + ], + "imageId": [ + "test1", + "test2" + ], + "imageTag": [ + "test1", + "test2" + ], + "registry": [ + "test1", + "test2" + ], + "repository": [ + "test1", + "test2" + ], + "namespace": [ + "test1", + "test2" + ] + }, + "state": 1, + "updatedTime": "2021-12-03T13:27:33.169Z", + "vulnerabilityCriteria": { + "cve": [ + "cve1", + "cve2" + ], + "fixable": [ + 1 + ], + "package": [ + { + "pck": [ + "1.0.0", + "2.0.0", + "3.0.0" + ] + } + ], + "severity": [ + "High", + "Medium", + "Low", + "Critical" + ] + } +} + `, id) +} + +var mockVulnHostRequest = `{"state":1,"exceptionName":"MyVulnException","exceptionType":"Host","exceptionReason":"Compensating Controls","props":{"description":"This is a vuln exception"},"vulnerabilityCriteria":{"package":[{"PackageOne":["1.2.3","1.2.4"]},{"PackageTwo":["1.2.3"]}],"severity":["Critical"],"fixable":[1]},"resourceScope":{"hostname":["exampleHost1"]},"expiryTime":"2022-01-14T19:15:18.414Z"}`