0
Fork 0
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:
Ramkumar Chinchani 2020-08-21 10:02:49 -07:00 committed by GitHub
commit ebfc5958dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1464 additions and 63 deletions

View file

@ -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

View file

@ -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

View file

@ -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")
)

View file

@ -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",

View file

@ -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
View 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
View 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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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")

View file

@ -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")
)

View file

@ -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"
)