Skip to content

Commit 76f3589

Browse files
feat(VULN-1588): Update CLI vuln container list-assessments and list-registries to use new ImageSummary API (#1754)
* feat: First steps at using new api for container vuln assessments * feat: vulnerability observation api file * feat: use new vuln observations image summary api * style: fix linting * style: fix linting * test: update vuln container list assessments unit tests * feat: return registries alphabetically * feat: remove duplicate assessments * fix: remove tag from list-assessments * style: fix import order * test: update integration tests * test: fix container vuln integration tests * feat: switch to RFC3339 timestamp
1 parent 7375d7b commit 76f3589

10 files changed

+442
-451
lines changed

api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ const (
116116
apiV2VulnerabilitiesHostsSearch = "v2/Vulnerabilities/Hosts/search"
117117
apiV2VulnerabilitiesSoftwarePackagesScan = "v2/Vulnerabilities/SoftwarePackages/scan"
118118

119+
apiV2VulnerabilityObservationsImageSummarySearch = "v2/VulnerabilityObservations/ImageSummary/search"
120+
119121
apiV2VulnerabilityExceptions = "v2/VulnerabilityExceptions"
120122
apiV2VulnerabilityExceptionFromGUID = "v2/VulnerabilityExceptions/%s"
121123

api/v2.go

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,40 @@ type V2Endpoints struct {
3232
client *Client
3333

3434
// Every schema must have its own service
35-
UserProfile *UserProfileService
36-
AlertChannels *AlertChannelsService
37-
Alert *v2alertProfilesService
38-
AlertRules *AlertRulesService
39-
ReportRules *ReportRulesService
40-
CloudAccounts *CloudAccountsService
41-
Components *ComponentsService
42-
ComponentData *ComponentDataService
43-
ContainerRegistries *ContainerRegistriesService
44-
Configs *v2ConfigService
45-
FeatureFlags *FeatureFlagsService
46-
ResourceGroups *ResourceGroupsService
47-
AgentAccessTokens *AgentAccessTokensService
48-
AgentInfo *AgentInfoService
49-
Inventory *InventoryService
50-
ComplianceEvaluations *ComplianceEvaluationService
51-
Query *QueryService
52-
OrganizationInfo *OrganizationInfoService
53-
Policy *PolicyService
54-
Reports *ReportsService
55-
ReportDefinitions *ReportDefinitionsService
56-
Metrics *MetricsService
57-
ReportDistributions *ReportDistributionsService
58-
Entities *EntitiesService
59-
Schemas *SchemasService
60-
Datasources *DatasourcesService
61-
DataExportRules *DataExportRulesService
62-
TeamMembers *TeamMembersService
63-
VulnerabilityExceptions *VulnerabilityExceptionsService
64-
Vulnerabilities *v2VulnerabilitiesService
65-
Alerts *AlertsService
66-
Suppressions *SuppressionsServiceV2
67-
Recommendations *RecommendationsServiceV2
35+
UserProfile *UserProfileService
36+
AlertChannels *AlertChannelsService
37+
Alert *v2alertProfilesService
38+
AlertRules *AlertRulesService
39+
ReportRules *ReportRulesService
40+
CloudAccounts *CloudAccountsService
41+
Components *ComponentsService
42+
ComponentData *ComponentDataService
43+
ContainerRegistries *ContainerRegistriesService
44+
Configs *v2ConfigService
45+
FeatureFlags *FeatureFlagsService
46+
ResourceGroups *ResourceGroupsService
47+
AgentAccessTokens *AgentAccessTokensService
48+
AgentInfo *AgentInfoService
49+
Inventory *InventoryService
50+
ComplianceEvaluations *ComplianceEvaluationService
51+
Query *QueryService
52+
OrganizationInfo *OrganizationInfoService
53+
Policy *PolicyService
54+
Reports *ReportsService
55+
ReportDefinitions *ReportDefinitionsService
56+
Metrics *MetricsService
57+
ReportDistributions *ReportDistributionsService
58+
Entities *EntitiesService
59+
Schemas *SchemasService
60+
Datasources *DatasourcesService
61+
DataExportRules *DataExportRulesService
62+
TeamMembers *TeamMembersService
63+
VulnerabilityExceptions *VulnerabilityExceptionsService
64+
Vulnerabilities *v2VulnerabilitiesService
65+
VulnerabilityObservations *v2VulnerabilityObservationsService
66+
Alerts *AlertsService
67+
Suppressions *SuppressionsServiceV2
68+
Recommendations *RecommendationsServiceV2
6869
}
6970

7071
func NewV2Endpoints(c *Client) *V2Endpoints {
@@ -99,6 +100,7 @@ func NewV2Endpoints(c *Client) *V2Endpoints {
99100
&TeamMembersService{c},
100101
&VulnerabilityExceptionsService{c},
101102
NewV2VulnerabilitiesService(c),
103+
NewV2VulnerabilityObservationsService(c),
102104
&AlertsService{c},
103105
&SuppressionsServiceV2{c,
104106
&AwsSuppressionsV2{c},
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package api
2+
3+
const TimestampLayout = "2006-01-02 15:04:05.000 Z"
4+
5+
type v2VulnerabilityObservationsService struct {
6+
client *Client
7+
ImageSummary *v2VulnerabilityObservationImageSummaryService
8+
}
9+
10+
func NewV2VulnerabilityObservationsService(c *Client) *v2VulnerabilityObservationsService {
11+
return &v2VulnerabilityObservationsService{c,
12+
&v2VulnerabilityObservationImageSummaryService{c},
13+
}
14+
}
15+
16+
type v2VulnerabilityObservationImageSummaryService struct {
17+
client *Client
18+
}
19+
20+
func (svc *v2VulnerabilityObservationImageSummaryService) Search(filters SearchFilter) (
21+
response VulnerabilityObservationsImageSummaryResponse, err error,
22+
) {
23+
err = svc.client.RequestEncoderDecoder(
24+
"POST", apiV2VulnerabilityObservationsImageSummarySearch,
25+
filters, &response,
26+
)
27+
return
28+
}
29+
30+
func (svc *v2VulnerabilityObservationImageSummaryService) SearchAllPages(filters SearchFilter) (
31+
response VulnerabilityObservationsImageSummaryResponse, err error,
32+
) {
33+
response, err = svc.Search(filters)
34+
if err != nil {
35+
return
36+
}
37+
38+
var (
39+
all []VulnerabilityObservationsImageSummary
40+
pageOk bool
41+
)
42+
for {
43+
all = append(all, response.Data...)
44+
45+
pageOk, err = svc.client.NextPage(&response)
46+
if err == nil && pageOk {
47+
continue
48+
}
49+
break
50+
}
51+
52+
response.ResetPaging()
53+
response.Data = all
54+
return
55+
}
56+
57+
type VulnerabilityObservationsImageSummaryResponse struct {
58+
Data []VulnerabilityObservationsImageSummary `json:"data"`
59+
Paging V2Pagination `json:"paging"`
60+
61+
v2PageMetadata `json:"-"`
62+
}
63+
64+
type VulnerabilityObservationsImageSummary struct {
65+
ContainerCount int `json:"containerCount"`
66+
Digest string `json:"digest"`
67+
ImageId string `json:"imageId"`
68+
LastScanTime string `json:"lastScanTime"`
69+
Registry string `json:"registry"`
70+
Repository string `json:"repository"`
71+
Tag string `json:"tag"`
72+
ScanStatus string `json:"scanStatus"`
73+
VulnCountCritical int `json:"vulnCountCritical"`
74+
VulnCountCriticalFixable int `json:"vulnCountCriticalFixable"`
75+
VulnCountHigh int `json:"vulnCountHigh"`
76+
VulnCountHighFixable int `json:"vulnCountHighFixable"`
77+
VulnCountMedium int `json:"vulnCountMedium"`
78+
VulnCountMediumFixable int `json:"vulnCountMediumFixable"`
79+
VulnCountLow int `json:"vulnCountLow"`
80+
VulnCountLowFixable int `json:"vulnCountLowFixable"`
81+
VulnCountInfo int `json:"vulnCountInfo"`
82+
VulnCountInfoFixable int `json:"vulnCountInfoFixable"`
83+
}
84+
85+
func (r VulnerabilityObservationsImageSummaryResponse) PageInfo() *V2Pagination {
86+
return &r.Paging
87+
}
88+
func (r *VulnerabilityObservationsImageSummaryResponse) ResetPaging() {
89+
r.Paging = V2Pagination{}
90+
r.Data = nil
91+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package api_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"github.com/lacework/go-sdk/v2/api"
12+
"github.com/lacework/go-sdk/v2/internal/lacework"
13+
)
14+
15+
func TestV2VulnerabilityObservations_ImageSummary_SearchAllPages_EmptyData(t *testing.T) {
16+
fakeServer := lacework.MockServer()
17+
fakeServer.MockToken("TOKEN")
18+
fakeServer.MockAPI("VulnerabilityObservations/ImageSummary/search",
19+
func(w http.ResponseWriter, r *http.Request) {
20+
assert.Equal(t, "POST", r.Method, "Search() should be a POST method")
21+
fmt.Fprintf(w, mockPaginationEmptyResponse())
22+
},
23+
)
24+
defer fakeServer.Close()
25+
26+
c, err := api.NewClient("test",
27+
api.WithToken("TOKEN"),
28+
api.WithURL(fakeServer.URL()),
29+
)
30+
assert.NoError(t, err)
31+
32+
response, err := c.V2.VulnerabilityObservations.ImageSummary.SearchAllPages(api.SearchFilter{})
33+
assert.NoError(t, err)
34+
assert.NotNil(t, response)
35+
assert.Equal(t, 0, len(response.Data))
36+
}
37+
38+
func TestV2VulnerabilityObservations_ImageSummary_SearchAllPages(t *testing.T) {
39+
fakeServer := lacework.MockServer()
40+
fakeServer.MockToken("TOKEN")
41+
fakeServer.MockAPI("VulnerabilityObservations/ImageSummary/search",
42+
func(w http.ResponseWriter, r *http.Request) {
43+
assert.Equal(t, "POST", r.Method, "Search() should be a POST method")
44+
fmt.Fprintf(w, mockVulnerabilityObservationsImageSummaryResponse())
45+
},
46+
)
47+
defer fakeServer.Close()
48+
49+
c, err := api.NewClient("test",
50+
api.WithToken("TOKEN"),
51+
api.WithURL(fakeServer.URL()),
52+
)
53+
assert.NoError(t, err)
54+
55+
response, err := c.V2.VulnerabilityObservations.ImageSummary.SearchAllPages(api.SearchFilter{})
56+
assert.NoError(t, err)
57+
assert.NotNil(t, response)
58+
assert.Equal(t, 3, len(response.Data))
59+
}
60+
61+
func TestV2VulnerabilityObservations_ImageSummary_UnmarshalResponse(t *testing.T) {
62+
var mockResponse api.VulnerabilityObservationsImageSummaryResponse
63+
err := json.Unmarshal([]byte(mockVulnerabilityObservationsImageSummaryResponse()), &mockResponse)
64+
assert.NoError(t, err)
65+
66+
paging := mockResponse.Paging
67+
assert.Equal(t, 3, paging.Rows)
68+
assert.Equal(t, 3, paging.TotalRows)
69+
assert.Empty(t, paging.Urls.NextPage)
70+
71+
data := mockResponse.Data
72+
assert.Equal(t, 3, len(data))
73+
74+
summary := data[0]
75+
assert.Equal(t, 0, summary.ContainerCount)
76+
assert.Equal(t, 0, summary.ContainerCount)
77+
assert.Equal(t, "sha256:8182c226d7d5bc4ce596f31017e62442fd6fdf4796595073d5342094f1b778df", summary.Digest)
78+
assert.Equal(t, "sha256:3fe1c77b23ca802abf84be74215344a2401457e9112d5f560ea50097679155e9", summary.ImageId)
79+
assert.Equal(t, "2025-08-06T13:05:05Z", summary.LastScanTime)
80+
assert.Equal(t, "docker.io", summary.Registry)
81+
assert.Equal(t, "lacework/jre", summary.Repository)
82+
assert.Equal(t, "Success", summary.ScanStatus)
83+
assert.Equal(t, "amazoncorretto8-alpine3.15-stable", summary.Tag)
84+
assert.Equal(t, 0, summary.VulnCountCritical)
85+
assert.Equal(t, 0, summary.VulnCountCriticalFixable)
86+
assert.Equal(t, 1, summary.VulnCountHigh)
87+
assert.Equal(t, 1, summary.VulnCountHighFixable)
88+
assert.Equal(t, 0, summary.VulnCountInfo)
89+
assert.Equal(t, 0, summary.VulnCountInfoFixable)
90+
assert.Equal(t, 0, summary.VulnCountLow)
91+
assert.Equal(t, 0, summary.VulnCountLowFixable)
92+
assert.Equal(t, 0, summary.VulnCountMedium)
93+
assert.Equal(t, 0, summary.VulnCountMediumFixable)
94+
}
95+
96+
func mockVulnerabilityObservationsImageSummaryResponse() string {
97+
return `
98+
{
99+
"paging": {
100+
"rows": 3,
101+
"totalRows": 3,
102+
"urls": {
103+
"nextPage": null
104+
}
105+
},
106+
"data": [
107+
{
108+
"containerCount": 0,
109+
"digest": "sha256:8182c226d7d5bc4ce596f31017e62442fd6fdf4796595073d5342094f1b778df",
110+
"imageId": "sha256:3fe1c77b23ca802abf84be74215344a2401457e9112d5f560ea50097679155e9",
111+
"lastScanTime": "2025-08-06T13:05:05Z",
112+
"registry": "docker.io",
113+
"repository": "lacework/jre",
114+
"scanStatus": "Success",
115+
"tag": "amazoncorretto8-alpine3.15-stable",
116+
"vulnCountCritical": 0,
117+
"vulnCountCriticalFixable": 0,
118+
"vulnCountHigh": 1,
119+
"vulnCountHighFixable": 1,
120+
"vulnCountInfo": 0,
121+
"vulnCountInfoFixable": 0,
122+
"vulnCountLow": 0,
123+
"vulnCountLowFixable": 0,
124+
"vulnCountMedium": 0,
125+
"vulnCountMediumFixable": 0
126+
},
127+
{
128+
"containerCount": 0,
129+
"digest": "sha256:8e6596ca0b60dc3464e286097b33f39012760cca51ba6976c7e8f2ff7a9bce82",
130+
"imageId": "sha256:819963f636cf5c396c5d7254e00678563e9197b7f16fdf69e7b4858e8a1fdf52",
131+
"lastScanTime": "2025-08-06T13:05:15Z",
132+
"registry": "docker.io",
133+
"repository": "lacework/jre",
134+
"scanStatus": "Success",
135+
"tag": "8-alpine3.15-test",
136+
"vulnCountCritical": 0,
137+
"vulnCountCriticalFixable": 0,
138+
"vulnCountHigh": 8,
139+
"vulnCountHighFixable": 8,
140+
"vulnCountInfo": 0,
141+
"vulnCountInfoFixable": 0,
142+
"vulnCountLow": 1,
143+
"vulnCountLowFixable": 1,
144+
"vulnCountMedium": 0,
145+
"vulnCountMediumFixable": 0
146+
},
147+
{
148+
"containerCount": 0,
149+
"digest": "sha256:a41ec54e6450ccc66d9f2ff975a0004d889349f3e8f5b086ebe8704e7ae962ac",
150+
"imageId": "sha256:b167326fa5f713a3cf7d742852967303b1b9301a147f84a0132ae58c47086fb4",
151+
"lastScanTime": "2025-08-06T13:05:11Z",
152+
"registry": "docker.io",
153+
"repository": "lacework/jre",
154+
"scanStatus": "Success",
155+
"tag": "alpine-test",
156+
"vulnCountCritical": 0,
157+
"vulnCountCriticalFixable": 0,
158+
"vulnCountHigh": 8,
159+
"vulnCountHighFixable": 8,
160+
"vulnCountInfo": 0,
161+
"vulnCountInfoFixable": 0,
162+
"vulnCountLow": 1,
163+
"vulnCountLowFixable": 1,
164+
"vulnCountMedium": 0,
165+
"vulnCountMediumFixable": 0
166+
}
167+
]
168+
}
169+
`
170+
}

cli/cmd/vuln_container.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
package cmd
2020

2121
import (
22-
"github.com/lacework/go-sdk/v2/internal/array"
22+
"sort"
23+
24+
"github.com/lacework/go-sdk/v2/api"
2325
"github.com/pkg/errors"
2426
flag "github.com/spf13/pflag"
2527
)
@@ -122,21 +124,23 @@ func setPollFlag(cmds ...*flag.FlagSet) {
122124

123125
func getContainerRegistries() ([]string, error) {
124126
var (
125-
registries = make([]string, 0)
126-
regsIntegrations, err = cli.LwApi.V2.ContainerRegistries.List()
127+
imageSummary, err = cli.LwApi.V2.VulnerabilityObservations.ImageSummary.SearchAllPages(api.SearchFilter{})
127128
)
128129
if err != nil {
129-
return registries, errors.Wrap(err, "unable to get container registry integrations")
130+
return nil, errors.Wrap(err, "unable to get container registry integrations")
130131
}
131132

132-
for _, i := range regsIntegrations.Data {
133-
// avoid adding empty registries coming from the new local_scanner and avoid adding duplicate registries
134-
if i.ContainerRegistryDomain() == "" || array.ContainsStr(registries, i.ContainerRegistryDomain()) {
135-
continue
136-
}
133+
registrySet := make(map[string]struct{})
134+
for _, i := range imageSummary.Data {
135+
registrySet[i.Registry] = struct{}{}
136+
}
137137

138-
registries = append(registries, i.ContainerRegistryDomain())
138+
registries := make([]string, 0, len(registrySet))
139+
for r := range registrySet {
140+
registries = append(registries, r)
139141
}
140142

143+
sort.Strings(registries)
144+
141145
return registries, nil
142146
}

0 commit comments

Comments
 (0)