Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit e1dd637

Browse files
committed
Add diff command
Signed-off-by: Christian Dupuis <[email protected]>
1 parent 237b0b1 commit e1dd637

File tree

15 files changed

+1083
-572
lines changed

15 files changed

+1083
-572
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist/
33
.idea/
44
index-cli-plugin
55
/sbom.json
6+
/docker-index

commands/cmd.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/*
2+
* Copyright © 2022 Docker, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package commands
18+
19+
import (
20+
"bufio"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"os"
25+
"strings"
26+
27+
"github.com/atomist-skills/go-skill"
28+
"github.com/docker/cli/cli"
29+
"github.com/docker/cli/cli-plugins/plugin"
30+
"github.com/docker/cli/cli/command"
31+
"github.com/docker/index-cli-plugin/query"
32+
"github.com/docker/index-cli-plugin/sbom"
33+
"github.com/docker/index-cli-plugin/types"
34+
v1 "github.com/google/go-containerregistry/pkg/v1"
35+
"github.com/moby/term"
36+
"github.com/pkg/errors"
37+
"github.com/spf13/cobra"
38+
)
39+
40+
func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Command {
41+
cmd := &cobra.Command{
42+
Short: "Docker Index",
43+
Long: `Index Docker images, create SBOMs and detect CVEs`,
44+
Use: name,
45+
}
46+
if isPlugin {
47+
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
48+
return plugin.PersistentPreRunE(cmd, args)
49+
}
50+
} else {
51+
cmd.SilenceUsage = true
52+
cmd.SilenceErrors = true
53+
cmd.TraverseChildren = true
54+
cmd.DisableFlagsInUseLine = true
55+
cli.DisableFlagsInUseLine(cmd)
56+
}
57+
58+
config := dockerCli.ConfigFile()
59+
60+
var (
61+
output, ociDir, image, workspace string
62+
apiKeyStdin, includeCves bool
63+
)
64+
65+
logoutCommand := &cobra.Command{
66+
Use: "logout",
67+
Short: "Remove Atomist workspace authentication",
68+
RunE: func(cmd *cobra.Command, _ []string) error {
69+
config.SetPluginConfig("index", "workspace", "")
70+
config.SetPluginConfig("index", "api-key", "")
71+
return config.Save()
72+
},
73+
}
74+
75+
loginCommand := &cobra.Command{
76+
Use: "login WORKSPACE",
77+
Short: "Authenticate with Atomist workspace",
78+
RunE: func(cmd *cobra.Command, args []string) error {
79+
workspace, err := readWorkspace(args, dockerCli)
80+
if err != nil {
81+
return err
82+
}
83+
apiKey, err := readApiKey(apiKeyStdin, dockerCli)
84+
if err != nil {
85+
return err
86+
}
87+
if valid, err := query.CheckAuth(workspace, apiKey); err == nil && valid {
88+
skill.Log.Info("Login successful")
89+
config.SetPluginConfig("index", "workspace", workspace)
90+
config.SetPluginConfig("index", "api-key", apiKey)
91+
return config.Save()
92+
} else {
93+
return errors.New("Login failed")
94+
}
95+
},
96+
}
97+
loginCommandFlags := loginCommand.Flags()
98+
loginCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
99+
100+
sbomCommand := &cobra.Command{
101+
Use: "sbom [OPTIONS]",
102+
Short: "Write SBOM file",
103+
RunE: func(cmd *cobra.Command, args []string) error {
104+
var err error
105+
var sb *types.Sbom
106+
107+
if ociDir == "" {
108+
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
109+
} else {
110+
sb, _, err = sbom.IndexPath(ociDir, image)
111+
}
112+
if err != nil {
113+
return err
114+
}
115+
if includeCves {
116+
workspace, _ := config.PluginConfig("index", "workspace")
117+
apiKey, _ := config.PluginConfig("index", "api-key")
118+
cves, err := query.QueryCves(sb, "", workspace, apiKey)
119+
if err != nil {
120+
return err
121+
}
122+
sb.Vulnerabilities = *cves
123+
}
124+
125+
js, err := json.MarshalIndent(sb, "", " ")
126+
if err != nil {
127+
return err
128+
}
129+
if output != "" {
130+
_ = os.WriteFile(output, js, 0644)
131+
skill.Log.Infof("SBOM written to %s", output)
132+
} else {
133+
os.Stdout.WriteString(string(js) + "\n")
134+
}
135+
return nil
136+
},
137+
}
138+
sbomCommandFlags := sbomCommand.Flags()
139+
sbomCommandFlags.StringVarP(&output, "output", "o", "", "Location path to write SBOM to")
140+
sbomCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
141+
sbomCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
142+
sbomCommandFlags.BoolVarP(&includeCves, "include-cves", "c", false, "Include package CVEs")
143+
144+
uploadCommand := &cobra.Command{
145+
Use: "upload [OPTIONS]",
146+
Short: "Upload SBOM",
147+
RunE: func(cmd *cobra.Command, args []string) error {
148+
var err error
149+
150+
if workspace == "" {
151+
workspace, _ = config.PluginConfig("index", "workspace")
152+
if workspace == "" {
153+
workspace, err = readWorkspace(args, dockerCli)
154+
if err != nil {
155+
return err
156+
}
157+
}
158+
}
159+
160+
apiKey, _ := config.PluginConfig("index", "api-key")
161+
if apiKey == "" {
162+
apiKey, err = readApiKey(apiKeyStdin, dockerCli)
163+
if err != nil {
164+
return err
165+
}
166+
}
167+
168+
var sb *types.Sbom
169+
var img *v1.Image
170+
if ociDir == "" {
171+
sb, img, err = sbom.IndexImage(image, dockerCli.Client())
172+
} else {
173+
sb, img, err = sbom.IndexPath(ociDir, image)
174+
}
175+
if err != nil {
176+
return err
177+
}
178+
err = sbom.UploadSbom(sb, img, workspace, apiKey)
179+
180+
return nil
181+
},
182+
}
183+
uploadCommandFlags := uploadCommand.Flags()
184+
uploadCommandFlags.StringVar(&image, "image", "", "Image reference to index")
185+
uploadCommandFlags.StringVar(&ociDir, "oci-dir", "", "Path to image in OCI format")
186+
uploadCommandFlags.StringVar(&workspace, "workspace", "", "Atomist workspace")
187+
uploadCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
188+
189+
cveCommand := &cobra.Command{
190+
Use: "cve [OPTIONS] CVE_ID",
191+
Short: "Check if image is vulnerable to given CVE",
192+
RunE: func(cmd *cobra.Command, args []string) error {
193+
if len(args) != 1 {
194+
return fmt.Errorf(`"docker index cve" requires exactly 1 argument`)
195+
}
196+
cve := args[0]
197+
var err error
198+
var sb *types.Sbom
199+
200+
if ociDir == "" {
201+
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
202+
} else {
203+
sb, _, err = sbom.IndexPath(ociDir, image)
204+
}
205+
if err != nil {
206+
return err
207+
}
208+
workspace, _ := config.PluginConfig("index", "workspace")
209+
apiKey, _ := config.PluginConfig("index", "api-key")
210+
cves, err := query.QueryCves(sb, cve, workspace, apiKey)
211+
if err != nil {
212+
return err
213+
}
214+
215+
if len(*cves) > 0 {
216+
for _, c := range *cves {
217+
skill.Log.Warnf("Detected %s at", cve)
218+
skill.Log.Warnf("")
219+
purl := c.Purl
220+
for _, p := range sb.Artifacts {
221+
if p.Purl == purl {
222+
skill.Log.Warnf(" %s", p.Purl)
223+
loc := p.Locations[0]
224+
for i, l := range sb.Source.Image.Config.RootFS.DiffIDs {
225+
if l.String() == loc.DiffId {
226+
h := sb.Source.Image.Config.History[i]
227+
skill.Log.Warnf(" ")
228+
skill.Log.Warnf(" Instruction: %s", h.CreatedBy)
229+
skill.Log.Warnf(" Layer %d: %s", i, loc.Digest)
230+
}
231+
}
232+
}
233+
}
234+
}
235+
os.Exit(1)
236+
} else {
237+
skill.Log.Infof("%s not detected", cve)
238+
os.Exit(0)
239+
}
240+
return nil
241+
},
242+
}
243+
cveCommandFlags := cveCommand.Flags()
244+
cveCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
245+
cveCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
246+
247+
diffCommand := &cobra.Command{
248+
Use: "diff [OPTIONS]",
249+
Short: "Diff images",
250+
RunE: func(cmd *cobra.Command, args []string) error {
251+
return sbom.DiffImages(args[0], args[1], dockerCli.Client(), "", "")
252+
},
253+
}
254+
255+
cmd.AddCommand(loginCommand, logoutCommand, sbomCommand, cveCommand, uploadCommand, diffCommand)
256+
return cmd
257+
}
258+
259+
func readWorkspace(args []string, cli command.Cli) (string, error) {
260+
var workspace string
261+
if len(args) == 1 {
262+
workspace = args[0]
263+
} else if v, ok := os.LookupEnv("ATOMIST_WORKSPACE"); v != "" && ok {
264+
workspace = v
265+
} else {
266+
fmt.Fprintf(cli.Out(), "Workspace: ")
267+
268+
workspace = readInput(cli.In(), cli.Out())
269+
if workspace == "" {
270+
return "", errors.Errorf("Error: Workspace required")
271+
}
272+
}
273+
return workspace, nil
274+
}
275+
276+
func readApiKey(apiKeyStdin bool, cli command.Cli) (string, error) {
277+
var apiKey string
278+
279+
if apiKeyStdin {
280+
contents, err := io.ReadAll(cli.In())
281+
if err != nil {
282+
return "", err
283+
}
284+
285+
apiKey = strings.TrimSuffix(string(contents), "\n")
286+
apiKey = strings.TrimSuffix(apiKey, "\r")
287+
} else if v, ok := os.LookupEnv("ATOMIST_API_KEY"); v != "" && ok {
288+
apiKey = v
289+
} else {
290+
oldState, err := term.SaveState(cli.In().FD())
291+
if err != nil {
292+
return "", err
293+
}
294+
fmt.Fprintf(cli.Out(), "API key: ")
295+
term.DisableEcho(cli.In().FD(), oldState)
296+
297+
apiKey = readInput(cli.In(), cli.Out())
298+
fmt.Fprint(cli.Out(), "\n")
299+
term.RestoreTerminal(cli.In().FD(), oldState)
300+
if apiKey == "" {
301+
return "", errors.Errorf("Error: API key required")
302+
}
303+
}
304+
return apiKey, nil
305+
}
306+
307+
func readInput(in io.Reader, out io.Writer) string {
308+
reader := bufio.NewReader(in)
309+
line, _, err := reader.ReadLine()
310+
if err != nil {
311+
fmt.Fprintln(out, err.Error())
312+
os.Exit(1)
313+
}
314+
return string(line)
315+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/docker/docker v20.10.17+incompatible
1313
github.com/google/go-containerregistry v0.11.0
1414
github.com/google/uuid v1.3.0
15+
github.com/jedib0t/go-pretty/v6 v6.4.0
1516
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae
1617
github.com/opencontainers/go-digest v1.0.0
1718
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1
@@ -91,6 +92,7 @@ require (
9192
github.com/golang/snappy v0.0.4 // indirect
9293
github.com/google/go-cmp v0.5.9 // indirect
9394
github.com/google/licenseclassifier/v2 v2.0.0-pre5 // indirect
95+
github.com/gookit/color v1.5.2 // indirect
9496
github.com/gorilla/mux v1.8.0 // indirect
9597
github.com/hashicorp/errwrap v1.1.0 // indirect
9698
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -110,7 +112,6 @@ require (
110112
github.com/klauspost/pgzip v1.2.5 // indirect
111113
github.com/knqyf263/go-rpmdb v0.0.0-20220629110411-9a3bd2ebb923 // indirect
112114
github.com/knqyf263/nested v0.0.1 // indirect
113-
github.com/mattn/go-colorable v0.1.12 // indirect
114115
github.com/mattn/go-isatty v0.0.14 // indirect
115116
github.com/mattn/go-runewidth v0.0.13 // indirect
116117
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
@@ -123,7 +124,6 @@ require (
123124
github.com/mitchellh/mapstructure v1.5.0 // indirect
124125
github.com/mitchellh/reflectwalk v1.0.2 // indirect
125126
github.com/moby/locker v1.0.1 // indirect
126-
github.com/moby/sys/mount v0.3.3 // indirect
127127
github.com/moby/sys/mountinfo v0.6.2 // indirect
128128
github.com/moby/sys/signal v0.7.0 // indirect
129129
github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb // indirect
@@ -169,6 +169,7 @@ require (
169169
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 // indirect
170170
github.com/xanzy/ssh-agent v0.3.0 // indirect
171171
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
172+
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
172173
go.etcd.io/bbolt v1.3.6 // indirect
173174
go.opencensus.io v0.23.0 // indirect
174175
go.uber.org/atomic v1.10.0 // indirect

0 commit comments

Comments
 (0)