Signed-off-by: Christian Dupuis <cd@atomist.com>
This commit is contained in:
Christian Dupuis 2022-10-27 22:41:00 +02:00
Родитель 662a990268
Коммит 237b0b1705
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E32B019A8B65E57A
7 изменённых файлов: 261 добавлений и 207 удалений

Просмотреть файл

@ -1,3 +1,5 @@
project_name: docker-index
release:
prerelease: auto
draft: true

Просмотреть файл

@ -7,26 +7,19 @@ using the Atomist data plane.
## Installation
To install, run the following command in your terminal:
You can install manually by following these steps:
```shell
$ curl -sSfL https://raw.githubusercontent.com/docker/index-cli-plugin/main/install.sh | sh -s --
```
Alternatively, you can install manually by following these steps:
* Download the plugin binary from the [release page](https://github.com/docker/index-cli-plugin/releases/latest)
* Download the binary from the [release page](https://github.com/docker/index-cli-plugin/releases/latest)
* Unzip the archive
* Copy/move the binary into `$HOME/.docker/cli-plugins`
## Usage
### `docker index sbom`
### `docker-index sbom`
To create an SBOM for a local or remote image, run the following command:
```shell
$ docker index sbom --image <IMAGE>
$ docker-index sbom --image <IMAGE>
```
* `--image <IMAGE>` can either be a local image id or fully qualified image name from a remote registry
@ -34,12 +27,12 @@ $ docker index sbom --image <IMAGE>
* `--output <OUTPUT FILE>` allows to store the generated SBOM in a local file
* `--include-cves` will include all detected CVEs in generated output
### `docker index cve`
### `docker-index cve`
To detect base images for local or remote images, use the following command:
```shell
$ docker index cve --image <IMAGE> CVE_ID
$ docker-index cve --image <IMAGE> CVE_ID
```
* `--image <IMAGE>` can either be a local image id or fully qualified image name from a remote registry

Просмотреть файл

@ -6,7 +6,7 @@ vars:
tasks:
go:build:
cmds:
- go build -ldflags="-w -s -X 'github.com/docker/index-cli-plugin/internal.version={{.GIT_COMMIT}}'"
- go build -o docker-index -ldflags="-w -s -X 'github.com/docker/index-cli-plugin/internal.version={{.GIT_COMMIT}}'"
env:
CGO_ENABLED: 0
vars:
@ -17,7 +17,7 @@ tasks:
deps: [go:build]
cmds:
- mkdir -p ~/.docker/cli-plugins
- install index-cli-plugin ~/.docker/cli-plugins/docker-index
- install docker-index ~/.docker/cli-plugins/docker-index
go:fmt:
cmds:
@ -29,6 +29,10 @@ tasks:
cmds:
- goreleaser release --rm-dist
go:snapshot:
cmds:
- goreleaser release --snapshot --rm-dist
docker:build:
cmds:
- docker buildx build . -f Dockerfile -t {{.IMAGE_NAME}} --load

9
go.mod
Просмотреть файл

@ -8,7 +8,8 @@ require (
github.com/anchore/syft v0.59.0
github.com/aquasecurity/trivy v0.30.4
github.com/atomist-skills/go-skill v0.0.6-0.20221003172518-c3d268e1f3f1
github.com/docker/cli v20.10.17+incompatible
github.com/docker/cli v20.10.21+incompatible
github.com/docker/docker v20.10.17+incompatible
github.com/google/go-containerregistry v0.11.0
github.com/google/uuid v1.3.0
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae
@ -63,7 +64,6 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.17+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.4.0 // indirect
@ -205,3 +205,8 @@ require (
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
)
replace (
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20220803220330-418ca3b4d46f+incompatible // master (v22.06-dev)
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220720171342-a60b458179aa+incompatible // 22.06 branch (v22.06-dev)
)

6
go.sum
Просмотреть файл

@ -507,10 +507,14 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.3-0.20220803220330-418ca3b4d46f+incompatible h1:iKanFYBu6Cum7d9j8JGTw2s/d7hUAcXRkEcp2m8b6Qc=
github.com/docker/cli v20.10.3-0.20220803220330-418ca3b4d46f+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.10+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU=
github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@ -518,6 +522,8 @@ github.com/docker/distribution v2.8.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.3-0.20220720171342-a60b458179aa+incompatible h1:gaceOC7utpmWqA7OF95kxXO2lGNTkbHIvgxpE+0hPi8=
github.com/docker/docker v20.10.3-0.20220720171342-a60b458179aa+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=

Просмотреть файл

@ -18,11 +18,11 @@
set -u
PROJECT_NAME="index-cli-plugin"
PROJECT_NAME="docker-index"
OWNER=docker
REPO="${PROJECT_NAME}"
REPO="index-cli-plugin"
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${REPO}
BINARY="docker-index"
DOCKER_HOME=${DOCKER_HOME:-~/.docker}
DEFAULT_INSTALL_DIR=${DOCKER_HOME}/cli-plugins

418
main.go
Просмотреть файл

@ -25,9 +25,11 @@ import (
"strings"
"github.com/atomist-skills/go-skill"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/index-cli-plugin/internal"
"github.com/docker/index-cli-plugin/query"
"github.com/docker/index-cli-plugin/sbom"
@ -37,211 +39,253 @@ import (
"github.com/spf13/cobra"
)
func main() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
skill.Log.SetOutput(os.Stderr)
config := dockerCli.ConfigFile()
func runStandalone(cmd *command.DockerCli) error {
if err := cmd.Initialize(cliflags.NewClientOptions()); err != nil {
return err
}
rootCmd := newRootCmd(os.Args[0], false, cmd)
return rootCmd.Execute()
}
var (
output, ociDir, image, workspace string
apiKeyStdin, includeCves bool
)
func runPlugin(cmd *command.DockerCli) error {
rootCmd := newRootCmd("index", true, cmd)
return plugin.RunPlugin(cmd, rootCmd, manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.FromBuild().Version,
})
}
logoutCommand := &cobra.Command{
Use: "logout",
Short: "Remove Atomist workspace authentication",
RunE: func(cmd *cobra.Command, _ []string) error {
config.SetPluginConfig("index", "workspace", "")
config.SetPluginConfig("index", "api-key", "")
func newRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Short: "Docker Index",
Long: `Index Docker images, create SBOMs and detect CVEs`,
Use: name,
}
if isPlugin {
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
return plugin.PersistentPreRunE(cmd, args)
}
} else {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.TraverseChildren = true
cmd.DisableFlagsInUseLine = true
cli.DisableFlagsInUseLine(cmd)
}
config := dockerCli.ConfigFile()
var (
output, ociDir, image, workspace string
apiKeyStdin, includeCves bool
)
logoutCommand := &cobra.Command{
Use: "logout",
Short: "Remove Atomist workspace authentication",
RunE: func(cmd *cobra.Command, _ []string) error {
config.SetPluginConfig("index", "workspace", "")
config.SetPluginConfig("index", "api-key", "")
return config.Save()
},
}
loginCommand := &cobra.Command{
Use: "login WORKSPACE",
Short: "Authenticate with Atomist workspace",
RunE: func(cmd *cobra.Command, args []string) error {
workspace, err := readWorkspace(args, dockerCli)
if err != nil {
return err
}
apiKey, err := readApiKey(apiKeyStdin, dockerCli)
if err != nil {
return err
}
if valid, err := query.CheckAuth(workspace, apiKey); err == nil && valid {
skill.Log.Info("Login successful")
config.SetPluginConfig("index", "workspace", workspace)
config.SetPluginConfig("index", "api-key", apiKey)
return config.Save()
},
}
} else {
return errors.New("Login failed")
}
},
}
loginCommandFlags := loginCommand.Flags()
loginCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
loginCommand := &cobra.Command{
Use: "login WORKSPACE",
Short: "Authenticate with Atomist workspace",
RunE: func(cmd *cobra.Command, args []string) error {
workspace, err := readWorkspace(args, dockerCli)
if err != nil {
return err
}
apiKey, err := readApiKey(apiKeyStdin, dockerCli)
if err != nil {
return err
}
if valid, err := query.CheckAuth(workspace, apiKey); err == nil && valid {
skill.Log.Info("Login successful")
config.SetPluginConfig("index", "workspace", workspace)
config.SetPluginConfig("index", "api-key", apiKey)
return config.Save()
} else {
return errors.New("Login failed")
}
},
}
loginCommandFlags := loginCommand.Flags()
loginCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
sbomCommand := &cobra.Command{
Use: "sbom [OPTIONS]",
Short: "Write SBOM file",
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var sb *sbom.Sbom
sbomCommand := &cobra.Command{
Use: "sbom [OPTIONS]",
Short: "Write SBOM file",
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var sb *sbom.Sbom
if ociDir == "" {
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, _, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
if includeCves {
workspace, _ := config.PluginConfig("index", "workspace")
apiKey, _ := config.PluginConfig("index", "api-key")
cves, err := query.QueryCves(sb, "", workspace, apiKey)
if err != nil {
return err
}
sb.Vulnerabilities = *cves
}
js, err := json.MarshalIndent(sb, "", " ")
if err != nil {
return err
}
if output != "" {
_ = os.WriteFile(output, js, 0644)
skill.Log.Infof("SBOM written to %s", output)
} else {
os.Stdout.WriteString(string(js) + "\n")
}
return nil
},
}
sbomCommandFlags := sbomCommand.Flags()
sbomCommandFlags.StringVarP(&output, "output", "o", "", "Location path to write SBOM to")
sbomCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
sbomCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
sbomCommandFlags.BoolVarP(&includeCves, "include-cves", "c", false, "Include package CVEs")
uploadCommand := &cobra.Command{
Use: "upload [OPTIONS]",
Short: "Upload SBOM",
RunE: func(cmd *cobra.Command, args []string) error {
var err error
if workspace == "" {
workspace, _ = config.PluginConfig("index", "workspace")
if workspace == "" {
workspace, err = readWorkspace(args, dockerCli)
if err != nil {
return err
}
}
}
apiKey, _ := config.PluginConfig("index", "api-key")
if apiKey == "" {
apiKey, err = readApiKey(apiKeyStdin, dockerCli)
if err != nil {
return err
}
}
var sb *sbom.Sbom
var img *v1.Image
if ociDir == "" {
sb, img, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, img, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
err = sbom.UploadSbom(sb, img, workspace, apiKey)
return nil
},
}
uploadCommandFlags := uploadCommand.Flags()
uploadCommandFlags.StringVar(&image, "image", "", "Image reference to index")
uploadCommandFlags.StringVar(&ociDir, "oci-dir", "", "Path to image in OCI format")
uploadCommandFlags.StringVar(&workspace, "workspace", "", "Atomist workspace")
uploadCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
cveCommand := &cobra.Command{
Use: "cve [OPTIONS] CVE_ID",
Short: "Check if image is vulnerable to given CVE",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf(`"docker index cve" requires exactly 1 argument`)
}
cve := args[0]
var err error
var sb *sbom.Sbom
if ociDir == "" {
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, _, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
if ociDir == "" {
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, _, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
if includeCves {
workspace, _ := config.PluginConfig("index", "workspace")
apiKey, _ := config.PluginConfig("index", "api-key")
cves, err := query.QueryCves(sb, cve, workspace, apiKey)
cves, err := query.QueryCves(sb, "", workspace, apiKey)
if err != nil {
return err
}
sb.Vulnerabilities = *cves
}
if len(*cves) > 0 {
for _, c := range *cves {
skill.Log.Warnf("Detected %s at", cve)
skill.Log.Warnf("")
purl := c.Purl
for _, p := range sb.Artifacts {
if p.Purl == purl {
skill.Log.Warnf(" %s", p.Purl)
loc := p.Locations[0]
for i, l := range sb.Source.Image.Config.RootFS.DiffIDs {
if l.String() == loc.DiffId {
h := sb.Source.Image.Config.History[i]
skill.Log.Warnf(" ")
skill.Log.Warnf(" Instruction: %s", h.CreatedBy)
skill.Log.Warnf(" Layer %d: %s", i, loc.Digest)
}
js, err := json.MarshalIndent(sb, "", " ")
if err != nil {
return err
}
if output != "" {
_ = os.WriteFile(output, js, 0644)
skill.Log.Infof("SBOM written to %s", output)
} else {
os.Stdout.WriteString(string(js) + "\n")
}
return nil
},
}
sbomCommandFlags := sbomCommand.Flags()
sbomCommandFlags.StringVarP(&output, "output", "o", "", "Location path to write SBOM to")
sbomCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
sbomCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
sbomCommandFlags.BoolVarP(&includeCves, "include-cves", "c", false, "Include package CVEs")
uploadCommand := &cobra.Command{
Use: "upload [OPTIONS]",
Short: "Upload SBOM",
RunE: func(cmd *cobra.Command, args []string) error {
var err error
if workspace == "" {
workspace, _ = config.PluginConfig("index", "workspace")
if workspace == "" {
workspace, err = readWorkspace(args, dockerCli)
if err != nil {
return err
}
}
}
apiKey, _ := config.PluginConfig("index", "api-key")
if apiKey == "" {
apiKey, err = readApiKey(apiKeyStdin, dockerCli)
if err != nil {
return err
}
}
var sb *sbom.Sbom
var img *v1.Image
if ociDir == "" {
sb, img, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, img, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
err = sbom.UploadSbom(sb, img, workspace, apiKey)
return nil
},
}
uploadCommandFlags := uploadCommand.Flags()
uploadCommandFlags.StringVar(&image, "image", "", "Image reference to index")
uploadCommandFlags.StringVar(&ociDir, "oci-dir", "", "Path to image in OCI format")
uploadCommandFlags.StringVar(&workspace, "workspace", "", "Atomist workspace")
uploadCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
cveCommand := &cobra.Command{
Use: "cve [OPTIONS] CVE_ID",
Short: "Check if image is vulnerable to given CVE",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf(`"docker index cve" requires exactly 1 argument`)
}
cve := args[0]
var err error
var sb *sbom.Sbom
if ociDir == "" {
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
} else {
sb, _, err = sbom.IndexPath(ociDir, image)
}
if err != nil {
return err
}
workspace, _ := config.PluginConfig("index", "workspace")
apiKey, _ := config.PluginConfig("index", "api-key")
cves, err := query.QueryCves(sb, cve, workspace, apiKey)
if err != nil {
return err
}
if len(*cves) > 0 {
for _, c := range *cves {
skill.Log.Warnf("Detected %s at", cve)
skill.Log.Warnf("")
purl := c.Purl
for _, p := range sb.Artifacts {
if p.Purl == purl {
skill.Log.Warnf(" %s", p.Purl)
loc := p.Locations[0]
for i, l := range sb.Source.Image.Config.RootFS.DiffIDs {
if l.String() == loc.DiffId {
h := sb.Source.Image.Config.History[i]
skill.Log.Warnf(" ")
skill.Log.Warnf(" Instruction: %s", h.CreatedBy)
skill.Log.Warnf(" Layer %d: %s", i, loc.Digest)
}
}
}
}
os.Exit(1)
} else {
skill.Log.Infof("%s not detected", cve)
os.Exit(0)
}
return nil
},
}
cveCommandFlags := cveCommand.Flags()
cveCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
cveCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
os.Exit(1)
} else {
skill.Log.Infof("%s not detected", cve)
os.Exit(0)
}
return nil
},
}
cveCommandFlags := cveCommand.Flags()
cveCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
cveCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
cmd := &cobra.Command{
Use: "index",
Short: "Docker Index",
}
cmd.AddCommand(loginCommand, logoutCommand, sbomCommand, cveCommand, uploadCommand)
return cmd
}
cmd.AddCommand(loginCommand, logoutCommand, sbomCommand, cveCommand, uploadCommand)
return cmd
},
manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.FromBuild().Version,
})
func main() {
cmd, err := command.NewDockerCli()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if plugin.RunningStandalone() {
err = runStandalone(cmd)
} else {
err = runPlugin(cmd)
}
if err == nil {
return
}
skill.Log.Errorf("%s", err)
os.Exit(1)
}
func readWorkspace(args []string, cli command.Cli) (string, error) {