mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
Merge pull request #123 from tsnaik/cve
cli: add commands for fetching CVE
This commit is contained in:
commit
ebfc5958dd
12 changed files with 1464 additions and 63 deletions
2
Makefile
2
Makefile
|
@ -19,7 +19,7 @@ debug: doc
|
|||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(shell mkdir -p test/data; cd test/data; ../scripts/gen_certs.sh; cd ${TOP_LEVEL}; sudo skopeo --insecure-policy copy -q docker://centos:latest oci:${TOP_LEVEL}/test/data/zot-test:0.0.1)
|
||||
$(shell mkdir -p test/data; cd test/data; ../scripts/gen_certs.sh; cd ${TOP_LEVEL}; sudo skopeo --insecure-policy copy -q docker://centos:latest oci:${TOP_LEVEL}/test/data/zot-test:0.0.1;sudo skopeo --insecure-policy copy -q docker://centos:8 oci:${TOP_LEVEL}/test/data/zot-cve-test:0.0.1)
|
||||
go test -v -race -cover -coverpkg ./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
.PHONY: covhtml
|
||||
|
|
75
README.md
75
README.md
|
@ -7,6 +7,7 @@
|
|||
* Uses [OCI storage layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) for storage layout
|
||||
* Supports [helm charts](https://helm.sh/docs/topics/registries/)
|
||||
* Currently suitable for on-prem deployments (e.g. colocated with Kubernetes)
|
||||
* [Vulnerability scanning of images](#Scanning-images-for-known-vulnerabilities)
|
||||
* [Command-line client support](#cli)
|
||||
* TLS support
|
||||
* Authentication via:
|
||||
|
@ -117,8 +118,8 @@ remote-zot https://server-example:8080
|
|||
local http://localhost:8080
|
||||
```
|
||||
|
||||
## Fetching images
|
||||
You can fetch all images from a server by using its alias specified [in this step](#adding-a-zot-server-url):
|
||||
## Listing images
|
||||
You can list all images from a server by using its alias specified [in this step](#adding-a-zot-server-url):
|
||||
|
||||
```console
|
||||
$ zot images remote-zot
|
||||
|
@ -135,6 +136,76 @@ $ zot images remote-zot -n busybox
|
|||
IMAGE NAME TAG DIGEST SIZE
|
||||
busybox latest 414aeb86 707.8KB
|
||||
```
|
||||
## Scanning images for known vulnerabilities
|
||||
|
||||
You can fetch CVE (Common Vulnerabilities and Exposures) info for images hosted on zot
|
||||
|
||||
- Get all images affected by a CVE
|
||||
|
||||
```console
|
||||
$ zot cve remote-zot -i CVE-2017-9935
|
||||
IMAGE NAME TAG DIGEST SIZE
|
||||
c3/openjdk-dev commit-5be4d92 ac3762e2 335MB
|
||||
```
|
||||
|
||||
- Get all CVEs for an image
|
||||
|
||||
```console
|
||||
$ zot cve remote-zot -I c3/openjdk-dev:0.3.19
|
||||
ID SEVERITY TITLE
|
||||
CVE-2015-8540 LOW libpng: underflow read in png_check_keyword()
|
||||
CVE-2017-16826 LOW binutils: Invalid memory access in the coff_s...
|
||||
```
|
||||
|
||||
- Get detailed json output
|
||||
|
||||
```console
|
||||
$ zot cve remote-zot -I c3/openjdk-dev:0.3.19 -o json
|
||||
{
|
||||
"Tag": "0.3.19",
|
||||
"CVEList": [
|
||||
{
|
||||
"Id": "CVE-2019-17006",
|
||||
"Severity": "MEDIUM",
|
||||
"Title": "nss: Check length of inputs for cryptographic primitives",
|
||||
"Description": "A vulnerability was discovered in nss where input text length was not checked when using certain cryptographic primitives. This could lead to a heap-buffer overflow resulting in a crash and data leak. The highest threat is to confidentiality and integrity of data as well as system availability.",
|
||||
"PackageList": [
|
||||
{
|
||||
"Name": "nss",
|
||||
"InstalledVersion": "3.44.0-7.el7_7",
|
||||
"FixedVersion": "Not Specified"
|
||||
},
|
||||
{
|
||||
"Name": "nss-sysinit",
|
||||
"InstalledVersion": "3.44.0-7.el7_7",
|
||||
"FixedVersion": "Not Specified"
|
||||
},
|
||||
{
|
||||
"Name": "nss-tools",
|
||||
"InstalledVersion": "3.44.0-7.el7_7",
|
||||
"FixedVersion": "Not Specified"
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
- Get all images in a specific repo affected by a CVE
|
||||
|
||||
```console
|
||||
$ zot cve remote-zot -I c3/openjdk-dev -i CVE-2017-9935
|
||||
IMAGE NAME TAG DIGEST SIZE
|
||||
c3/openjdk-dev commit-2674e8a 71046748 338MB
|
||||
c3/openjdk-dev commit-bd5cc94 0ab7fc76
|
||||
```
|
||||
|
||||
- Get all images of a specific repo where a CVE is fixed
|
||||
|
||||
```console
|
||||
$ zot cve remote-zot -I c3/openjdk-dev -i CVE-2017-9935 --fixed
|
||||
IMAGE NAME TAG DIGEST SIZE
|
||||
c3/openjdk-dev commit-2674e8a-squashfs b545b8ba 321MB
|
||||
c3/openjdk-dev commit-d5024ec-squashfs cd45f8cf 321MB
|
||||
```
|
||||
|
||||
# Ecosystem
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ var (
|
|||
ErrRequireCred = errors.New("ldap: bind credentials required")
|
||||
ErrInvalidCred = errors.New("ldap: invalid credentials")
|
||||
ErrInvalidArgs = errors.New("cli: Invalid Arguments")
|
||||
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags. Add --help flag")
|
||||
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags")
|
||||
ErrInvalidURL = errors.New("cli: invalid URL format")
|
||||
ErrUnauthorizedAccess = errors.New("cli: unauthorized access. check credentials")
|
||||
ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key")
|
||||
|
@ -36,4 +36,5 @@ var (
|
|||
ErrIllegalConfigKey = errors.New("cli: given config key is not allowed")
|
||||
ErrScanNotSupported = errors.New("search: scanning of image media type not supported")
|
||||
ErrFixedTagNotFound = errors.New("search: no fixed tag found")
|
||||
ErrCLITimeout = errors.New("cli: Query timed out while waiting for results")
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ go_library(
|
|||
srcs = [
|
||||
"client.go",
|
||||
"config_cmd.go",
|
||||
"cve_cmd.go",
|
||||
"image_cmd.go",
|
||||
"root.go",
|
||||
"searcher.go",
|
||||
|
@ -31,11 +32,15 @@ go_library(
|
|||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
timeout = "moderate",
|
||||
srcs = [
|
||||
"config_cmd_test.go",
|
||||
"cve_cmd_test.go",
|
||||
"image_cmd_test.go",
|
||||
"root_test.go",
|
||||
],
|
||||
data = [
|
||||
"//:exported_testdata",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
|
@ -17,7 +18,7 @@ import (
|
|||
|
||||
var httpClient *http.Client //nolint: gochecknoglobals
|
||||
|
||||
const httpTimeout = 5 * time.Second
|
||||
const httpTimeout = 5 * time.Minute
|
||||
|
||||
func createHTTPClient(verifyTLS bool) *http.Client {
|
||||
var tr = http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
@ -40,6 +41,33 @@ func makeGETRequest(url, username, password string, verifyTLS bool, resultsPtr i
|
|||
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
return doHTTPRequest(req, verifyTLS, resultsPtr)
|
||||
}
|
||||
|
||||
func makeGraphQLRequest(url, query, username,
|
||||
password string, verifyTLS bool, resultsPtr interface{}) error {
|
||||
req, err := http.NewRequest("GET", url, bytes.NewBufferString(query))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("query", query)
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.SetBasicAuth(username, password)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
_, err = doHTTPRequest(req, verifyTLS, resultsPtr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doHTTPRequest(req *http.Request, verifyTLS bool, resultsPtr interface{}) (http.Header, error) {
|
||||
if httpClient == nil {
|
||||
httpClient = createHTTPClient(verifyTLS)
|
||||
}
|
||||
|
@ -77,7 +105,7 @@ type requestsPool struct {
|
|||
jobs chan *manifestJob
|
||||
done chan struct{}
|
||||
waitGroup *sync.WaitGroup
|
||||
outputCh chan imageListResult
|
||||
outputCh chan stringResult
|
||||
context context.Context
|
||||
}
|
||||
|
||||
|
@ -93,7 +121,7 @@ type manifestJob struct {
|
|||
|
||||
const rateLimiterBuffer = 5000
|
||||
|
||||
func newSmoothRateLimiter(ctx context.Context, wg *sync.WaitGroup, op chan imageListResult) *requestsPool {
|
||||
func newSmoothRateLimiter(ctx context.Context, wg *sync.WaitGroup, op chan stringResult) *requestsPool {
|
||||
ch := make(chan *manifestJob, rateLimiterBuffer)
|
||||
|
||||
return &requestsPool{
|
||||
|
@ -132,7 +160,7 @@ func (p *requestsPool) doJob(job *manifestJob) {
|
|||
if isContextDone(p.context) {
|
||||
return
|
||||
}
|
||||
p.outputCh <- imageListResult{"", err}
|
||||
p.outputCh <- stringResult{"", err}
|
||||
}
|
||||
|
||||
digest := header.Get("docker-content-digest")
|
||||
|
@ -159,7 +187,7 @@ func (p *requestsPool) doJob(job *manifestJob) {
|
|||
if isContextDone(p.context) {
|
||||
return
|
||||
}
|
||||
p.outputCh <- imageListResult{"", err}
|
||||
p.outputCh <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -168,7 +196,7 @@ func (p *requestsPool) doJob(job *manifestJob) {
|
|||
return
|
||||
}
|
||||
|
||||
p.outputCh <- imageListResult{str, nil}
|
||||
p.outputCh <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (p *requestsPool) submitJob(job *manifestJob) {
|
||||
|
|
135
pkg/cli/cve_cmd.go
Normal file
135
pkg/cli/cve_cmd.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCveCommand(searchService SearchService) *cobra.Command {
|
||||
searchCveParams := make(map[string]*string)
|
||||
|
||||
var servURL, user, outputFormat string
|
||||
|
||||
var isSpinner, verifyTLS, fixedFlag bool
|
||||
|
||||
var cveCmd = &cobra.Command{
|
||||
Use: "cve [config-name]",
|
||||
Short: "Lookup CVEs in images hosted on zot",
|
||||
Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on a zot instance`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configPath := path.Join(home + "/.zot")
|
||||
if servURL == "" {
|
||||
if len(args) > 0 {
|
||||
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
if urlFromConfig == "" {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
servURL = urlFromConfig
|
||||
} else {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig)
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||
spin.Prefix = fmt.Sprintf("Fetching from %s.. ", servURL)
|
||||
|
||||
searchConfig := searchConfig{
|
||||
params: searchCveParams,
|
||||
searchService: searchService,
|
||||
servURL: &servURL,
|
||||
user: &user,
|
||||
outputFormat: &outputFormat,
|
||||
fixedFlag: &fixedFlag,
|
||||
verifyTLS: &verifyTLS,
|
||||
resultWriter: cmd.OutOrStdout(),
|
||||
spinner: spinnerState{spin, isSpinner},
|
||||
}
|
||||
|
||||
err = searchCve(searchConfig)
|
||||
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
vars := cveFlagVariables{
|
||||
searchCveParams: searchCveParams,
|
||||
servURL: &servURL,
|
||||
user: &user,
|
||||
outputFormat: &outputFormat,
|
||||
fixedFlag: &fixedFlag,
|
||||
}
|
||||
|
||||
setupCveFlags(cveCmd, vars)
|
||||
|
||||
return cveCmd
|
||||
}
|
||||
|
||||
func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) {
|
||||
variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]")
|
||||
variables.searchCveParams["cveID"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE")
|
||||
|
||||
cveCmd.Flags().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
|
||||
cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+
|
||||
`zot server in USERNAME:PASSWORD format`)
|
||||
cveCmd.Flags().StringVarP(variables.outputFormat, "output", "o", "", "Specify output format [text/json/yaml]."+
|
||||
" JSON and YAML format return all info for CVEs")
|
||||
|
||||
cveCmd.Flags().BoolVar(variables.fixedFlag, "fixed", false, "List tags which have fixed a CVE")
|
||||
}
|
||||
|
||||
type cveFlagVariables struct {
|
||||
searchCveParams map[string]*string
|
||||
servURL *string
|
||||
user *string
|
||||
outputFormat *string
|
||||
fixedFlag *bool
|
||||
}
|
||||
|
||||
func searchCve(searchConfig searchConfig) error {
|
||||
for _, searcher := range getCveSearchers() {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidFlagsCombination
|
||||
}
|
507
pkg/cli/cve_cmd_test.go
Normal file
507
pkg/cli/cve_cmd_test.go
Normal file
|
@ -0,0 +1,507 @@
|
|||
package cli //nolint:testpackage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
"gopkg.in/resty.v1"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSearchCVECmd(t *testing.T) {
|
||||
Convey("Test CVE help", t, func() {
|
||||
args := []string{"--help"}
|
||||
configPath := makeConfigFile("")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("with the shorthand", func() {
|
||||
args[0] = "-h"
|
||||
configPath := makeConfigFile("")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
Convey("Test CVE no url", t, func() {
|
||||
args := []string{"cvetest", "-i", "cveIdRandom"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrNoURLProvided)
|
||||
})
|
||||
|
||||
Convey("Test CVE no params", t, func() {
|
||||
args := []string{"cvetest", "--url", "someUrl"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidFlagsCombination)
|
||||
})
|
||||
|
||||
Convey("Test CVE invalid url", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "invalidUrl"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid URL format")
|
||||
})
|
||||
|
||||
Convey("Test CVE invalid url port", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "http://localhost:99999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid port")
|
||||
|
||||
Convey("without flags", func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "http://localhost:99999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid port")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE unreachable", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "http://localhost:9999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test CVE url from config", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test CVE by name and CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("using shorthand", func() {
|
||||
args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"}
|
||||
buff := bytes.NewBufferString("")
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE by image name", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("in json format", func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "json"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `{ "Tag": "dummyImageName:tag", "CVEList": `+
|
||||
`[ { "Id": "dummyCVEID", "Severity": "HIGH", "Title": "Title of that CVE", `+
|
||||
`"Description": "Description of the CVE", "PackageList": [ { "Name": "packagename",`+
|
||||
` "InstalledVersion": "installedver", "FixedVersion": "fixedver" } ] } ] }`)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("in yaml format", func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "yaml"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `tag: dummyImageName:tag cvelist: - id: dummyCVEID`+
|
||||
` severity: HIGH title: Title of that CVE description: Description of the CVE packagelist: `+
|
||||
`- name: packagename installedversion: installedver fixedversion: fixedver`)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
Convey("invalid format", func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "random"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "Error: invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test images by CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test fixed tags by and image name CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "aCVEID", "--image", "fixedImage", "--url", "someURL", "--fixed"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE fixedImage tag DigestsA 123kB")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerCVEResponse(t *testing.T) {
|
||||
port := "8080"
|
||||
url := "http://127.0.0.1:8080"
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = port
|
||||
c := api.NewController(config)
|
||||
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = copyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
cveConfig := &api.CVEConfig{
|
||||
UpdateInterval: 2,
|
||||
}
|
||||
searchConfig := &api.SearchConfig{
|
||||
CVE: cveConfig,
|
||||
}
|
||||
c.Config.Extensions = &api.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}(c)
|
||||
// wait till ready
|
||||
for {
|
||||
res, err := resty.R().Get(url + "/query")
|
||||
if err == nil && res.StatusCode() == 200 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(25 * time.Second)
|
||||
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(c)
|
||||
|
||||
Convey("Test CVE by image name", t, func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
|
||||
So(str, ShouldContainSubstring, "CVE")
|
||||
Convey("invalid image", func() {
|
||||
args := []string{"cvetest", "--image", "invalid:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test images by CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-20807"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 da0186c7 75MB")
|
||||
Convey("invalid CVE ID", func() {
|
||||
args := []string{"cvetest", "--cve-id", "invalid"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fixed tags by and image name CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "")
|
||||
Convey("random cve", func() {
|
||||
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE by name and CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-20807"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 da0186c7 75MB")
|
||||
Convey("invalidname and CVE ID", func() {
|
||||
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(ioutil.Discard)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func copyFiles(sourceDir string, destDir string) error {
|
||||
sourceMeta, err := os.Stat(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
sourceFilePath := path.Join(sourceDir, file.Name())
|
||||
destFilePath := path.Join(destDir, file.Name())
|
||||
|
||||
if file.IsDir() {
|
||||
if err = copyFiles(sourceFilePath, destFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
sourceFile, err := os.Open(sourceFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(destFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewImageCommand(searchService ImageSearchService) *cobra.Command {
|
||||
func NewImageCommand(searchService SearchService) *cobra.Command {
|
||||
searchImageParams := make(map[string]*string)
|
||||
|
||||
var servURL, user, outputFormat string
|
||||
|
@ -84,7 +84,7 @@ func NewImageCommand(searchService ImageSearchService) *cobra.Command {
|
|||
},
|
||||
}
|
||||
|
||||
setupCmdFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat)
|
||||
setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat)
|
||||
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
|
||||
|
||||
return imageCmd
|
||||
|
@ -104,7 +104,8 @@ func parseBooleanConfig(configPath, configName, configParam string) (bool, error
|
|||
return val, nil
|
||||
}
|
||||
|
||||
func setupCmdFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, servURL, user, outputFormat *string) {
|
||||
func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*string,
|
||||
servURL, user, outputFormat *string) {
|
||||
searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name")
|
||||
|
||||
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
|
||||
|
@ -113,7 +114,7 @@ func setupCmdFlags(imageCmd *cobra.Command, searchImageParams map[string]*string
|
|||
}
|
||||
|
||||
func searchImage(searchConfig searchConfig) error {
|
||||
for _, searcher := range getSearchers() {
|
||||
for _, searcher := range getImageSearchers() {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
|
|
|
@ -63,6 +63,7 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrNoURLProvided)
|
||||
})
|
||||
|
||||
Convey("Test image no params", t, func() {
|
||||
|
@ -440,8 +441,9 @@ func uploadManifest(url string) {
|
|||
type mockService struct{}
|
||||
|
||||
func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||
channel chan imageListResult, wg *sync.WaitGroup) {
|
||||
channel chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(channel)
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = "randomimageName"
|
||||
|
@ -455,15 +457,16 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
|
|||
|
||||
str, err := image.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
channel <- imageListResult{"", err}
|
||||
channel <- stringResult{"", err}
|
||||
return
|
||||
}
|
||||
channel <- imageListResult{str, nil}
|
||||
channel <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (service mockService) getImageByName(ctx context.Context, config searchConfig,
|
||||
username, password, imageName string, channel chan imageListResult, wg *sync.WaitGroup) {
|
||||
username, password, imageName string, channel chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(channel)
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = imageName
|
||||
|
@ -477,10 +480,60 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
|
|||
|
||||
str, err := image.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
channel <- imageListResult{"", err}
|
||||
channel <- stringResult{"", err}
|
||||
return
|
||||
}
|
||||
channel <- imageListResult{str, nil}
|
||||
channel <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (service mockService) getCveByImage(ctx context.Context, config searchConfig, username, password,
|
||||
imageName string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
cveRes := &cveResult{}
|
||||
cveRes.Data = cveData{
|
||||
CVEListForImage: cveListForImage{
|
||||
Tag: imageName,
|
||||
CVEList: []cve{
|
||||
{
|
||||
ID: "dummyCVEID",
|
||||
Description: "Description of the CVE",
|
||||
Title: "Title of that CVE",
|
||||
Severity: "HIGH",
|
||||
PackageList: []packageList{
|
||||
{
|
||||
Name: "packagename",
|
||||
FixedVersion: "fixedver",
|
||||
InstalledVersion: "installedver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
str, err := cveRes.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
c <- stringResult{"", err}
|
||||
return
|
||||
}
|
||||
c <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveID string,
|
||||
c chan stringResult, wg *sync.WaitGroup) {
|
||||
service.getImageByName(ctx, config, username, password, "anImage", c, wg)
|
||||
}
|
||||
|
||||
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
|
||||
password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, c, wg)
|
||||
}
|
||||
|
||||
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, c, wg)
|
||||
}
|
||||
|
||||
func makeConfigFile(content string) string {
|
||||
|
|
|
@ -98,7 +98,8 @@ func NewRootCmd() *cobra.Command {
|
|||
rootCmd.AddCommand(gcCmd)
|
||||
|
||||
rootCmd.AddCommand(NewConfigCommand())
|
||||
rootCmd.AddCommand(NewImageCommand(NewImageSearchService()))
|
||||
rootCmd.AddCommand(NewImageCommand(NewSearchService()))
|
||||
rootCmd.AddCommand(NewCveCommand(NewSearchService()))
|
||||
|
||||
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
|
||||
|
||||
|
|
|
@ -9,10 +9,11 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
"github.com/briandowns/spinner"
|
||||
)
|
||||
|
||||
func getSearchers() []searcher {
|
||||
func getImageSearchers() []searcher {
|
||||
searchers := []searcher{
|
||||
new(allImagesSearcher),
|
||||
new(imageByNameSearcher),
|
||||
|
@ -21,6 +22,17 @@ func getSearchers() []searcher {
|
|||
return searchers
|
||||
}
|
||||
|
||||
func getCveSearchers() []searcher {
|
||||
searchers := []searcher{
|
||||
new(cveByImageSearcher),
|
||||
new(imagesByCVEIDSearcher),
|
||||
new(tagsByImageNameAndCVEIDSearcher),
|
||||
new(fixedTagsSearcher),
|
||||
}
|
||||
|
||||
return searchers
|
||||
}
|
||||
|
||||
type searcher interface {
|
||||
search(searchConfig searchConfig) (bool, error)
|
||||
}
|
||||
|
@ -39,11 +51,12 @@ func canSearch(params map[string]*string, requiredParams *set) bool {
|
|||
|
||||
type searchConfig struct {
|
||||
params map[string]*string
|
||||
searchService ImageSearchService
|
||||
searchService SearchService
|
||||
servURL *string
|
||||
user *string
|
||||
outputFormat *string
|
||||
verifyTLS *bool
|
||||
fixedFlag *bool
|
||||
resultWriter io.Writer
|
||||
spinner spinnerState
|
||||
}
|
||||
|
@ -56,7 +69,7 @@ func (search allImagesSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
imageErr := make(chan imageListResult)
|
||||
imageErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
@ -68,7 +81,7 @@ func (search allImagesSearcher) search(config searchConfig) (bool, error) {
|
|||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
|
||||
go collectImages(config, &wg, imageErr, cancel, errCh)
|
||||
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
|
||||
wg.Wait()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
|
@ -86,18 +99,19 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
imageErr := make(chan imageListResult)
|
||||
imageErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getImageByName(ctx, config, username, password, *config.params["imageName"], imageErr, &wg)
|
||||
go config.searchService.getImageByName(ctx, config, username, password,
|
||||
*config.params["imageName"], imageErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectImages(config, &wg, imageErr, cancel, errCh)
|
||||
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
|
@ -109,8 +123,146 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func collectImages(config searchConfig, wg *sync.WaitGroup, imageErr chan imageListResult,
|
||||
cancel context.CancelFunc, errCh chan error) {
|
||||
type cveByImageSearcher struct{}
|
||||
|
||||
func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("imageName")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !validateImageNameTag(*config.params["imageName"]) {
|
||||
return true, errInvalidImageNameAndTag
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
strErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getCveByImage(ctx, config, username, password, *config.params["imageName"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectResults(config, &wg, strErr, cancel, printCVETableHeader, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type imagesByCVEIDSearcher struct{}
|
||||
|
||||
func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
strErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type tagsByImageNameAndCVEIDSearcher struct{}
|
||||
|
||||
func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if strings.Contains(*config.params["imageName"], ":") {
|
||||
return true, errInvalidImageName
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
strErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"],
|
||||
*config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type fixedTagsSearcher struct{}
|
||||
|
||||
func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if strings.Contains(*config.params["imageName"], ":") {
|
||||
return true, errInvalidImageName
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
strErr := make(chan stringResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"],
|
||||
*config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
|
||||
cancel context.CancelFunc, printHeader printHeader, errCh chan error) {
|
||||
var foundResult bool
|
||||
|
||||
defer wg.Done()
|
||||
|
@ -133,10 +285,10 @@ func collectImages(config searchConfig, wg *sync.WaitGroup, imageErr chan imageL
|
|||
return
|
||||
}
|
||||
|
||||
if !foundResult && (*config.outputFormat == "text" || *config.outputFormat == "") {
|
||||
if !foundResult && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") {
|
||||
var builder strings.Builder
|
||||
|
||||
printImageTableHeader(&builder)
|
||||
printHeader(&builder)
|
||||
fmt.Fprint(config.resultWriter, builder.String())
|
||||
}
|
||||
|
||||
|
@ -144,8 +296,10 @@ func collectImages(config searchConfig, wg *sync.WaitGroup, imageErr chan imageL
|
|||
|
||||
fmt.Fprint(config.resultWriter, result.StrValue)
|
||||
case <-time.After(waitTimeout):
|
||||
cancel()
|
||||
config.spinner.stopSpinner()
|
||||
cancel()
|
||||
|
||||
errCh <- zotErrors.ErrCLITimeout
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -161,6 +315,22 @@ func getUsernameAndPassword(user string) (string, string) {
|
|||
return "", ""
|
||||
}
|
||||
|
||||
func validateImageNameTag(input string) bool {
|
||||
if !strings.Contains(input, ":") {
|
||||
return false
|
||||
}
|
||||
|
||||
split := strings.Split(input, ":")
|
||||
name := strings.TrimSpace(split[0])
|
||||
tag := strings.TrimSpace(split[1])
|
||||
|
||||
if name == "" || tag == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type set struct {
|
||||
m map[string]struct{}
|
||||
}
|
||||
|
@ -190,7 +360,7 @@ var (
|
|||
ErrInvalidOutputFormat = errors.New("invalid output format")
|
||||
)
|
||||
|
||||
type imageListResult struct {
|
||||
type stringResult struct {
|
||||
StrValue string
|
||||
Err error
|
||||
}
|
||||
|
@ -212,17 +382,37 @@ func (spinner *spinnerState) stopSpinner() {
|
|||
}
|
||||
}
|
||||
|
||||
type printHeader func(writer io.Writer)
|
||||
|
||||
func printImageTableHeader(writer io.Writer) {
|
||||
table := getNoBorderTableWriter(writer)
|
||||
row := []string{"IMAGE NAME",
|
||||
"TAG",
|
||||
"DIGEST",
|
||||
"SIZE",
|
||||
}
|
||||
table := getImageTableWriter(writer)
|
||||
row := make([]string, 4)
|
||||
|
||||
row[colImageNameIndex] = "IMAGE NAME"
|
||||
row[colTagIndex] = "TAG"
|
||||
row[colDigestIndex] = "DIGEST"
|
||||
row[colSizeIndex] = "SIZE"
|
||||
|
||||
table.Append(row)
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func printCVETableHeader(writer io.Writer) {
|
||||
table := getCVETableWriter(writer)
|
||||
row := make([]string, 3)
|
||||
row[colCVEIDIndex] = "ID"
|
||||
row[colCVESeverityIndex] = "SEVERITY"
|
||||
row[colCVETitleIndex] = "TITLE"
|
||||
|
||||
table.Append(row)
|
||||
table.Render()
|
||||
}
|
||||
|
||||
const (
|
||||
waitTimeout = 6 * time.Second
|
||||
waitTimeout = httpTimeout + 5*time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")
|
||||
errInvalidImageName = errors.New("cli: Invalid input format. Expected IMAGENAME without :TAG")
|
||||
)
|
||||
|
|
|
@ -2,11 +2,13 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
@ -16,20 +18,29 @@ import (
|
|||
zotErrors "github.com/anuvu/zot/errors"
|
||||
)
|
||||
|
||||
type ImageSearchService interface {
|
||||
type SearchService interface {
|
||||
getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||
channel chan imageListResult, wg *sync.WaitGroup)
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
getImageByName(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
channel chan imageListResult, wg *sync.WaitGroup)
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
getCveByImage(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveID string,
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string,
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveID string,
|
||||
channel chan stringResult, wg *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type searchService struct{}
|
||||
|
||||
func NewImageSearchService() ImageSearchService {
|
||||
func NewSearchService() SearchService {
|
||||
return searchService{}
|
||||
}
|
||||
|
||||
func (service searchService) getImageByName(ctx context.Context, config searchConfig,
|
||||
username, password, imageName string, c chan imageListResult, wg *sync.WaitGroup) {
|
||||
username, password, imageName string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
|
@ -47,7 +58,7 @@ func (service searchService) getImageByName(ctx context.Context, config searchCo
|
|||
}
|
||||
|
||||
func (service searchService) getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||
c chan imageListResult, wg *sync.WaitGroup) {
|
||||
c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
|
@ -58,7 +69,7 @@ func (service searchService) getAllImages(ctx context.Context, config searchConf
|
|||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -68,7 +79,7 @@ func (service searchService) getAllImages(ctx context.Context, config searchConf
|
|||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -91,7 +102,7 @@ func (service searchService) getAllImages(ctx context.Context, config searchConf
|
|||
}
|
||||
|
||||
func getImage(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
c chan imageListResult, wg *sync.WaitGroup, pool *requestsPool) {
|
||||
c chan stringResult, wg *sync.WaitGroup, pool *requestsPool) {
|
||||
defer wg.Done()
|
||||
|
||||
tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", imageName))
|
||||
|
@ -99,7 +110,7 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
|
|||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -111,7 +122,7 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
|
|||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -123,6 +134,199 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
|
|||
}
|
||||
}
|
||||
|
||||
func (service searchService) getImagesByCveID(ctx context.Context, config searchConfig, username,
|
||||
password, cveID string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
Name Tags }
|
||||
}`,
|
||||
cveID)
|
||||
result := &imagesForCve{}
|
||||
|
||||
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var localWg sync.WaitGroup
|
||||
|
||||
p := newSmoothRateLimiter(ctx, &localWg, c)
|
||||
localWg.Add(1)
|
||||
|
||||
go p.startRateLimiter()
|
||||
|
||||
for _, image := range result.Data.ImageListForCVE {
|
||||
for _, tag := range image.Tags {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, p, username, password, image.Name, tag, c, &localWg)
|
||||
}
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
}
|
||||
|
||||
func (service searchService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
|
||||
password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
Name Tags }
|
||||
}`,
|
||||
cveID)
|
||||
result := &imagesForCve{}
|
||||
|
||||
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var localWg sync.WaitGroup
|
||||
|
||||
p := newSmoothRateLimiter(ctx, &localWg, c)
|
||||
localWg.Add(1)
|
||||
|
||||
go p.startRateLimiter()
|
||||
|
||||
for _, image := range result.Data.ImageListForCVE {
|
||||
if !strings.EqualFold(imageName, image.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tag := range image.Tags {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, p, username, password, image.Name, tag, c, &localWg)
|
||||
}
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
}
|
||||
|
||||
func (service searchService) getCveByImage(ctx context.Context, config searchConfig, username, password,
|
||||
imageName string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
query := fmt.Sprintf(`{ CVEListForImage (image:"%s")`+
|
||||
` { Tag CVEList { Id Title Severity Description `+
|
||||
`PackageList {Name InstalledVersion FixedVersion}} } }`, imageName)
|
||||
result := &cveResult{}
|
||||
|
||||
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
str, err := result.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func isContextDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -132,8 +336,77 @@ func isContextDone(ctx context.Context) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer close(c)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") {`+`
|
||||
Tags {Name Timestamp} }
|
||||
}`,
|
||||
cveID, imageName)
|
||||
result := &fixedTags{}
|
||||
|
||||
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
if err.Message == zotErrors.ErrFixedTagNotFound.Error() {
|
||||
// this if block and goto should be removed when the server API is fixed.
|
||||
// currently, the API returns an error if the data is empty and we are ignoring that error here
|
||||
goto Outside
|
||||
}
|
||||
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Outside:
|
||||
var localWg sync.WaitGroup
|
||||
|
||||
p := newSmoothRateLimiter(ctx, &localWg, c)
|
||||
localWg.Add(1)
|
||||
|
||||
go p.startRateLimiter()
|
||||
|
||||
for _, imgTag := range result.Data.ImageListWithCVEFixed.Tags {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, p, username, password, imageName, imgTag.Name, c, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
}
|
||||
|
||||
func addManifestCallToPool(ctx context.Context, config searchConfig, p *requestsPool, username, password, imageName,
|
||||
tagName string, c chan imageListResult, wg *sync.WaitGroup) {
|
||||
tagName string, c chan stringResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
resultManifest := manifestResponse{}
|
||||
|
@ -144,7 +417,7 @@ func addManifestCallToPool(ctx context.Context, config searchConfig, p *requests
|
|||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
c <- stringResult{"", err}
|
||||
}
|
||||
|
||||
job := manifestJob{
|
||||
|
@ -161,6 +434,109 @@ func addManifestCallToPool(ctx context.Context, config searchConfig, p *requests
|
|||
p.submitJob(&job)
|
||||
}
|
||||
|
||||
type cveResult struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data cveData `json:"data"`
|
||||
}
|
||||
type errorGraphQL struct {
|
||||
Message string `json:"message"`
|
||||
Path []string `json:"path"`
|
||||
}
|
||||
type packageList struct {
|
||||
Name string `json:"Name"`
|
||||
InstalledVersion string `json:"InstalledVersion"`
|
||||
FixedVersion string `json:"FixedVersion"`
|
||||
}
|
||||
type cve struct {
|
||||
ID string `json:"Id"`
|
||||
Severity string `json:"Severity"`
|
||||
Title string `json:"Title"`
|
||||
Description string `json:"Description"`
|
||||
PackageList []packageList `json:"PackageList"`
|
||||
}
|
||||
type cveListForImage struct {
|
||||
Tag string `json:"Tag"`
|
||||
CVEList []cve `json:"CVEList"`
|
||||
}
|
||||
type cveData struct {
|
||||
CVEListForImage cveListForImage `json:"CVEListForImage"`
|
||||
}
|
||||
|
||||
func (cve cveResult) string(format string) (string, error) {
|
||||
switch strings.ToLower(format) {
|
||||
case "", defaultOutoutFormat:
|
||||
return cve.stringPlainText()
|
||||
case "json":
|
||||
return cve.stringJSON()
|
||||
case "yml", "yaml":
|
||||
return cve.stringYAML()
|
||||
default:
|
||||
return "", ErrInvalidOutputFormat
|
||||
}
|
||||
}
|
||||
|
||||
func (cve cveResult) stringPlainText() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
table := getCVETableWriter(&builder)
|
||||
|
||||
for _, c := range cve.Data.CVEListForImage.CVEList {
|
||||
id := ellipsize(c.ID, cveIDWidth, ellipsis)
|
||||
title := ellipsize(c.Title, cveTitleWidth, ellipsis)
|
||||
severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis)
|
||||
row := make([]string, 3)
|
||||
row[colCVEIDIndex] = id
|
||||
row[colCVESeverityIndex] = severity
|
||||
row[colCVETitleIndex] = title
|
||||
|
||||
table.Append(row)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (cve cveResult) stringJSON() (string, error) {
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
body, err := json.MarshalIndent(cve.Data.CVEListForImage, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (cve cveResult) stringYAML() (string, error) {
|
||||
body, err := yaml.Marshal(&cve.Data.CVEListForImage)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
type fixedTags struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageListWithCVEFixed struct {
|
||||
Tags []struct {
|
||||
Name string `json:"Name"`
|
||||
Timestamp time.Time `json:"Timestamp"`
|
||||
} `json:"Tags"`
|
||||
} `json:"ImageListWithCVEFixed"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imagesForCve struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageListForCVE []tagListResp `json:"ImageListForCVE"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type tagListResp struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
|
@ -178,7 +554,7 @@ type tags struct {
|
|||
|
||||
func (img imageStruct) string(format string) (string, error) {
|
||||
switch strings.ToLower(format) {
|
||||
case "", "text":
|
||||
case "", defaultOutoutFormat:
|
||||
return img.stringPlainText()
|
||||
case "json":
|
||||
return img.stringJSON()
|
||||
|
@ -192,18 +568,19 @@ func (img imageStruct) string(format string) (string, error) {
|
|||
func (img imageStruct) stringPlainText() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
table := getNoBorderTableWriter(&builder)
|
||||
table := getImageTableWriter(&builder)
|
||||
|
||||
for _, tag := range img.Tags {
|
||||
imageName := ellipsize(img.Name, imageNameWidth, ellipsis)
|
||||
tagName := ellipsize(tag.Name, tagWidth, ellipsis)
|
||||
digest := ellipsize(tag.Digest, digestWidth, "")
|
||||
size := ellipsize(strings.ReplaceAll(humanize.Bytes(tag.Size), " ", ""), sizeWidth, ellipsis)
|
||||
row := []string{imageName,
|
||||
tagName,
|
||||
digest,
|
||||
size,
|
||||
}
|
||||
row := make([]string, 4)
|
||||
|
||||
row[colImageNameIndex] = imageName
|
||||
row[colTagIndex] = tagName
|
||||
row[colDigestIndex] = digest
|
||||
row[colSizeIndex] = size
|
||||
|
||||
table.Append(row)
|
||||
}
|
||||
|
@ -273,6 +650,7 @@ func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
|
|||
}
|
||||
|
||||
func ellipsize(text string, max int, trailing string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) <= max {
|
||||
return text
|
||||
}
|
||||
|
@ -282,7 +660,7 @@ func ellipsize(text string, max int, trailing string) string {
|
|||
return text[:max-chopLength] + trailing
|
||||
}
|
||||
|
||||
func getNoBorderTableWriter(writer io.Writer) *tablewriter.Table {
|
||||
func getImageTableWriter(writer io.Writer) *tablewriter.Table {
|
||||
table := tablewriter.NewWriter(writer)
|
||||
|
||||
table.SetAutoWrapText(false)
|
||||
|
@ -304,6 +682,27 @@ func getNoBorderTableWriter(writer io.Writer) *tablewriter.Table {
|
|||
return table
|
||||
}
|
||||
|
||||
func getCVETableWriter(writer io.Writer) *tablewriter.Table {
|
||||
table := tablewriter.NewWriter(writer)
|
||||
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetTablePadding(" ")
|
||||
table.SetNoWhiteSpace(true)
|
||||
table.SetColMinWidth(colCVEIDIndex, cveIDWidth)
|
||||
table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth)
|
||||
table.SetColMinWidth(colCVETitleIndex, cveTitleWidth)
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
const (
|
||||
imageNameWidth = 32
|
||||
tagWidth = 24
|
||||
|
@ -315,4 +714,14 @@ const (
|
|||
colTagIndex = 1
|
||||
colDigestIndex = 2
|
||||
colSizeIndex = 3
|
||||
|
||||
cveIDWidth = 16
|
||||
cveSeverityWidth = 8
|
||||
cveTitleWidth = 48
|
||||
|
||||
colCVEIDIndex = 0
|
||||
colCVESeverityIndex = 1
|
||||
colCVETitleIndex = 2
|
||||
|
||||
defaultOutoutFormat = "text"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue