Skip to content

Commit 88c7ac0

Browse files
authored
Merge pull request #52 from lacework/afiune/ALLY-54/vulnerabilities-scan
Vulnerabilities for Pipelines
2 parents 717fb26 + 6403029 commit 88c7ac0

File tree

88 files changed

+4722
-30
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+4722
-30
lines changed

api/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const (
2626
apiIntegrationByGUID = "external/integrations/%s"
2727
apiIntegrationSchema = "external/integrations/schema/%s"
2828
apiTokens = "access/tokens"
29+
30+
apiVulnerabilitiesScan = "external/vulnerabilities/container/repository/images/scan"
31+
apiVulnerabilitiesScanStatus = "external/vulnerabilities/container/reqId/%s"
32+
apiVulnerabilitiesReportFromID = "external/vulnerabilities/container/imageId/%s"
33+
apiVulnerabilitiesReportFromDigest = "external/vulnerabilities/container/imageDigest/%s"
2934
)
3035

3136
// WithApiV2 configures the client to use the API version 2 (/api/v2)

api/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ type Client struct {
4242
c *http.Client
4343
log *zap.Logger
4444

45-
Integrations *IntegrationsService
45+
Integrations *IntegrationsService
46+
Vulnerabilities *VulnerabilitiesService
4647
}
4748

4849
type Option interface {
@@ -84,6 +85,7 @@ func NewClient(account string, opts ...Option) (*Client, error) {
8485
c: &http.Client{Timeout: defaultTimeout},
8586
}
8687
c.Integrations = &IntegrationsService{c}
88+
c.Vulnerabilities = &VulnerabilitiesService{c}
8789

8890
// init logger, this could change if a user calls api.WithLogLevel()
8991
c.initLogger()

api/http.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ func (c *Client) RequestDecoder(method, path string, body io.Reader, v interface
134134
return err
135135
}
136136

137+
// RequestEncoderDecoder leverages RequestDecoder and performs an http request that first
138+
// encodes the provider 'data' as a JSON Reader and passes it as the body to the request
139+
func (c *Client) RequestEncoderDecoder(method, path string, data, v interface{}) error {
140+
body, err := jsonReader(data)
141+
if err != nil {
142+
return err
143+
}
144+
return c.RequestDecoder(method, path, body, v)
145+
}
146+
137147
// Do calls request.Do() directly
138148
func (c *Client) Do(req *http.Request) (*http.Response, error) {
139149
response, err := c.c.Do(req)

api/integrations.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package api
2020

2121
import (
2222
"fmt"
23-
"strings"
2423
)
2524

2625
// IntegrationsService is a service that interacts with the integrations
@@ -173,24 +172,45 @@ type commonIntegrationData struct {
173172
TypeName string `json:"TYPE_NAME,omitempty"`
174173
}
175174

175+
func (iData commonIntegrationData) Status() string {
176+
if iData.Enabled == 1 {
177+
return "Enabled"
178+
}
179+
return "Disabled"
180+
}
181+
176182
type state struct {
177183
Ok bool `json:"ok"`
178184
LastUpdatedTime string `json:"lastUpdatedTime"`
179185
LastSuccessfulTime string `json:"lastSuccessfulTime"`
180186
}
181187

188+
func (s state) String() string {
189+
if s.Ok {
190+
return "Ok"
191+
}
192+
return "Check"
193+
194+
}
195+
182196
type IntegrationsResponse struct {
183197
Data []commonIntegrationData `json:"data"`
184198
Ok bool `json:"ok"`
185199
Message string `json:"message"`
186200
}
187201

188-
func (integrations *IntegrationsResponse) String() string {
189-
out := []string{}
190-
for _, integration := range integrations.Data {
191-
out = append(out, fmt.Sprintf("%s %s", integration.IntgGuid, integration.Type))
202+
func (integrations *IntegrationsResponse) Table() [][]string {
203+
out := [][]string{}
204+
for _, idata := range integrations.Data {
205+
out = append(out, []string{
206+
idata.IntgGuid,
207+
idata.Name,
208+
idata.Type,
209+
idata.Status(),
210+
idata.State.String(),
211+
})
192212
}
193-
return strings.Join(out, "\n")
213+
return out
194214
}
195215

196216
type RawIntegration struct {

api/logging.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,5 @@ func (c *Client) initLoggerWithWriter(w io.Writer) {
133133

134134
// debugMode returns true if the client is configured to display debug level logs
135135
func (c *Client) debugMode() bool {
136-
return c.logLevel == "debug"
136+
return c.logLevel == "DEBUG"
137137
}

api/vulnerabilities.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
//
2+
// Author:: Salim Afiune Maya (<[email protected]>)
3+
// Copyright:: Copyright 2020, Lacework Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package api
20+
21+
import (
22+
"fmt"
23+
"strings"
24+
)
25+
26+
// VulnerabilitiesService is a service that interacts with the vulnerabilities
27+
// endpoints from the Lacework Server
28+
type VulnerabilitiesService struct {
29+
client *Client
30+
}
31+
32+
// Scan triggers a vulnerability scan to the provider registry, repository, and
33+
// tag provided. This function calls the underlaying API endpoint that assumes
34+
// that the container repository has been already integrated with the platform.
35+
func (svc *VulnerabilitiesService) Scan(registry, repository, tagOrHash string) (
36+
response vulScanResponse,
37+
err error,
38+
) {
39+
err = svc.client.RequestEncoderDecoder("POST",
40+
apiVulnerabilitiesScan,
41+
vulScanRequest{registry, repository, tagOrHash},
42+
&response,
43+
)
44+
return
45+
}
46+
47+
type vulScanRequest struct {
48+
Registry string `json:"registry"`
49+
Repository string `json:"repository"`
50+
Tag string `json:"tag"`
51+
}
52+
53+
type vulScanResponse struct {
54+
Data struct {
55+
Status string `json:"status"`
56+
RequestID string `json:"requestId"`
57+
} `json:"data"`
58+
Ok bool `json:"ok"`
59+
Message string `json:"message"`
60+
}
61+
62+
func (svc *VulnerabilitiesService) ScanStatus(requestID string) (
63+
response vulScanStatusResponse,
64+
err error,
65+
) {
66+
apiPath := fmt.Sprintf(apiVulnerabilitiesScanStatus, requestID)
67+
err = svc.client.RequestDecoder("GET", apiPath, nil, &response)
68+
return
69+
}
70+
71+
type vulScanStatusResponse struct {
72+
Data VulContainerReport `json:"data"`
73+
Ok bool `json:"ok"`
74+
Message string `json:"message"`
75+
}
76+
77+
func (scan *vulScanStatusResponse) CheckStatus() string {
78+
if !scan.Ok {
79+
return fmt.Sprintf("there is a problem with the vulnerability scan: %s", scan.Message)
80+
}
81+
82+
if scan.Data.Status != "" {
83+
// @afiune as far as I can see, the three status we could have are:
84+
// * Scanning
85+
// * Failed
86+
// * NotFound
87+
//
88+
// Where is "Success"? Not here. Why?
89+
return scan.Data.Status
90+
}
91+
92+
// If the scan is not running, that means, it completed running, now the
93+
// status of the scan changes to be stored in "ScanStatus" :sadpanda:
94+
if scan.Data.ScanStatus != "" {
95+
return scan.Data.ScanStatus
96+
}
97+
98+
return "Unknown"
99+
}
100+
101+
func (svc *VulnerabilitiesService) ReportFromID(imageID string) (
102+
response VulContainerReportResponse,
103+
err error,
104+
) {
105+
apiPath := fmt.Sprintf(apiVulnerabilitiesReportFromID, imageID)
106+
err = svc.client.RequestDecoder("GET", apiPath, nil, &response)
107+
return
108+
}
109+
110+
func (svc *VulnerabilitiesService) ReportFromDigest(imageDigest string) (
111+
response VulContainerReportResponse,
112+
err error,
113+
) {
114+
apiPath := fmt.Sprintf(apiVulnerabilitiesReportFromDigest, imageDigest)
115+
err = svc.client.RequestDecoder("GET", apiPath, nil, &response)
116+
return
117+
}
118+
119+
type VulContainerReportResponse struct {
120+
Data VulContainerReport `json:"data"`
121+
Ok bool `json:"ok"`
122+
Message string `json:"message"`
123+
}
124+
125+
func (res *VulContainerReportResponse) CheckStatus() string {
126+
if !res.Ok {
127+
return fmt.Sprintf("there is a problem with the vulnerability report: %s", res.Message)
128+
}
129+
130+
if res.Data.ScanStatus != "" {
131+
return res.Data.ScanStatus
132+
}
133+
134+
if res.Data.Status != "" {
135+
return res.Data.Status
136+
}
137+
138+
return "Unknown"
139+
}
140+
141+
type VulContainerReport struct {
142+
TotalVulnerabilities int32 `json:"total_vulnerabilities"`
143+
CriticalVulnerabilities int32 `json:"critical_vulnerabilities"`
144+
HighVulnerabilities int32 `json:"high_vulnerabilities"`
145+
MediumVulnerabilities int32 `json:"medium_vulnerabilities"`
146+
LowVulnerabilities int32 `json:"low_vulnerabilities"`
147+
InfoVulnerabilities int32 `json:"info_vulnerabilities"`
148+
FixableVulnerabilities int32 `json:"fixable_vulnerabilities"`
149+
LastEvaluationTime string `json:"last_evaluation_time"`
150+
Image vulContainerImage `json:"image"`
151+
152+
// @afiune these two parameters, Status and Message will appear when
153+
// the vulnerability scan is still running. ugh. why?
154+
Status string `json:"status,omitempty"`
155+
Message string `json:"message,omitempty"`
156+
157+
// ScanStatus is a property that will appear when the vulnerability scan finished
158+
// running, this status indicates whether the scan finished successfully or not
159+
ScanStatus string `json:"scan_status"`
160+
161+
// @afiune why we can't parse the time?
162+
//LastEvaluationTime time.Time `json:"last_evaluation_time"`
163+
}
164+
165+
func (report *VulContainerReport) VulCountsTable() [][]string {
166+
return [][]string{
167+
[]string{"Critical", fmt.Sprint(report.CriticalVulnerabilities),
168+
fmt.Sprint(report.VulFixableCount("critical"))},
169+
[]string{"High", fmt.Sprint(report.HighVulnerabilities),
170+
fmt.Sprint(report.VulFixableCount("high"))},
171+
[]string{"Medium", fmt.Sprint(report.MediumVulnerabilities),
172+
fmt.Sprint(report.VulFixableCount("medium"))},
173+
[]string{"Low", fmt.Sprint(report.LowVulnerabilities),
174+
fmt.Sprint(report.VulFixableCount("low"))},
175+
[]string{"Info", fmt.Sprint(report.InfoVulnerabilities),
176+
fmt.Sprint(report.VulFixableCount("info"))},
177+
}
178+
}
179+
180+
func (report *VulContainerReport) VulFixableCount(severity string) int32 {
181+
// @afiune check valid severity
182+
severity = strings.ToLower(severity)
183+
184+
if len(report.Image.ImageLayers) == 0 {
185+
return 0
186+
}
187+
188+
var fixable int32 = 0
189+
for _, layer := range report.Image.ImageLayers {
190+
for _, pkg := range layer.Packages {
191+
for _, vul := range pkg.Vulnerabilities {
192+
if vul.Severity == severity && vul.FixVersion != "" {
193+
fixable++
194+
}
195+
}
196+
}
197+
}
198+
return fixable
199+
}
200+
201+
type vulContainerImage struct {
202+
ImageInfo vulContainerImageInfo `json:"image_info"`
203+
ImageLayers []vulContainerImageLayer `json:"image_layers"`
204+
}
205+
206+
func (image *vulContainerImage) Table() [][]string {
207+
info := image.ImageInfo
208+
return [][]string{
209+
[]string{"ID", info.ImageID},
210+
[]string{"Digest", info.ImageDigest},
211+
[]string{"Registry", info.Registry},
212+
[]string{"Repository", info.Repository},
213+
[]string{"Size", ByteCountBinary(info.Size)},
214+
[]string{"Created At", info.CreatedTime},
215+
[]string{"Tags", strings.Join(info.Tags, ",")},
216+
}
217+
}
218+
219+
func ByteCountBinary(b int64) string {
220+
const unit = 1024
221+
if b < unit {
222+
return fmt.Sprintf("%d B", b)
223+
}
224+
div, exp := int64(unit), 0
225+
for n := b / unit; n >= unit; n /= unit {
226+
div *= unit
227+
exp++
228+
}
229+
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
230+
}
231+
232+
type vulContainerImageInfo struct {
233+
ImageDigest string `json:"image_digest"`
234+
ImageID string `json:"image_id"`
235+
Registry string `json:"registry"`
236+
Repository string `json:"repository"`
237+
CreatedTime string `json:"created_time"`
238+
Size int64 `json:"size"`
239+
Tags []string `json:"tags"`
240+
}
241+
242+
type vulContainerImageLayer struct {
243+
Hash string `json:"hash"`
244+
CreatedBy string `json:"created_by"`
245+
Packages []vulContainerPackage `json:"packages"`
246+
}
247+
248+
type vulContainerPackage struct {
249+
Name string `json:"name"`
250+
Namespace string `json:"namescape"`
251+
Version string `json:"version"`
252+
Vulnerabilities []containerVulnerability `json:"vulnerabilities"`
253+
254+
// @afiune maybe these fields are host related information and not container
255+
FixAvailable string `json:"fix_available"`
256+
FixedVersion string `json:"fixed_version"`
257+
HostCount string `json:"host_count"`
258+
Severity string `json:"severity"`
259+
Status string `json:"status"`
260+
CveLink string `json:"cve_link"`
261+
CveScore string `json:"cve_score"`
262+
CvssV3Score string `json:"cvss_v3_score"`
263+
CvssV2Score string `json:"cvss_v2_score"`
264+
FirstSeenTime string `json:"first_seen_time"`
265+
}
266+
267+
type containerVulnerability struct {
268+
Name string `json:"name"`
269+
Description string `json:"description"`
270+
Severity string `json:"severity"`
271+
Link string `json:"link"`
272+
FixVersion string `json:"fix_version"`
273+
Metadata map[string]interface{} `json:"metadata"`
274+
}

0 commit comments

Comments
 (0)