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

Commit 662a990

Browse files
committed
Add cve detection
Signed-off-by: Christian Dupuis <[email protected]>
1 parent 825b267 commit 662a990

File tree

4 files changed

+191
-37
lines changed

4 files changed

+191
-37
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**Note:** This repository is not an officially supported Docker project.
2+
13
# `docker index` Docker CLI plugin
24

35
Docker CLI plugin to create image SBOMs as well as analyze packages for known vulnerabilities
@@ -21,12 +23,25 @@ Alternatively, you can install manually by following these steps:
2123

2224
### `docker index sbom`
2325

24-
To detect base images for local or remote images, use the following command:
26+
To create an SBOM for a local or remote image, run the following command:
2527

2628
```shell
2729
$ docker index sbom --image <IMAGE>
2830
```
2931

30-
`<IMAGE>` can either be a local image id or fully qualified image name from a remote registry.
32+
* `--image <IMAGE>` can either be a local image id or fully qualified image name from a remote registry
33+
* `--oci-dir <DIR>` can point to a local image in OCI directory format
34+
* `--output <OUTPUT FILE>` allows to store the generated SBOM in a local file
35+
* `--include-cves` will include all detected CVEs in generated output
36+
37+
### `docker index cve`
38+
39+
To detect base images for local or remote images, use the following command:
40+
41+
```shell
42+
$ docker index cve --image <IMAGE> CVE_ID
43+
```
3144

32-
`--output <OUTPUT FILE>` allows to store the generated SBOM in a local file.
45+
* `--image <IMAGE>` can either be a local image id or fully qualified image name from a remote registry
46+
* `--oci-dir <DIR>` can point to a local image in OCI directory format
47+
* `CVE_ID` can be any known CVE id

main.go

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func main() {
4444

4545
var (
4646
output, ociDir, image, workspace string
47-
apiKeyStdin, includeVulns bool
47+
apiKeyStdin, includeCves bool
4848
)
4949

5050
logoutCommand := &cobra.Command{
@@ -97,10 +97,10 @@ func main() {
9797
if err != nil {
9898
return err
9999
}
100-
if includeVulns {
100+
if includeCves {
101101
workspace, _ := config.PluginConfig("index", "workspace")
102102
apiKey, _ := config.PluginConfig("index", "api-key")
103-
cves, err := query.QueryCves(sb, workspace, apiKey)
103+
cves, err := query.QueryCves(sb, "", workspace, apiKey)
104104
if err != nil {
105105
return err
106106
}
@@ -121,10 +121,10 @@ func main() {
121121
},
122122
}
123123
sbomCommandFlags := sbomCommand.Flags()
124-
sbomCommandFlags.StringVar(&output, "output", "", "Location path to write SBOM to")
125-
sbomCommandFlags.StringVar(&image, "image", "", "Image reference to index")
126-
sbomCommandFlags.StringVar(&ociDir, "oci-dir", "", "Path to image in OCI format")
127-
sbomCommandFlags.BoolVar(&includeVulns, "include-vulns", false, "Include package CVEs")
124+
sbomCommandFlags.StringVarP(&output, "output", "o", "", "Location path to write SBOM to")
125+
sbomCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
126+
sbomCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
127+
sbomCommandFlags.BoolVarP(&includeCves, "include-cves", "c", false, "Include package CVEs")
128128

129129
uploadCommand := &cobra.Command{
130130
Use: "upload [OPTIONS]",
@@ -171,12 +171,70 @@ func main() {
171171
uploadCommandFlags.StringVar(&workspace, "workspace", "", "Atomist workspace")
172172
uploadCommandFlags.BoolVar(&apiKeyStdin, "api-key-stdin", false, "Atomist API key")
173173

174+
cveCommand := &cobra.Command{
175+
Use: "cve [OPTIONS] CVE_ID",
176+
Short: "Check if image is vulnerable to given CVE",
177+
RunE: func(cmd *cobra.Command, args []string) error {
178+
if len(args) != 1 {
179+
return fmt.Errorf(`"docker index cve" requires exactly 1 argument`)
180+
}
181+
cve := args[0]
182+
var err error
183+
var sb *sbom.Sbom
184+
185+
if ociDir == "" {
186+
sb, _, err = sbom.IndexImage(image, dockerCli.Client())
187+
} else {
188+
sb, _, err = sbom.IndexPath(ociDir, image)
189+
}
190+
if err != nil {
191+
return err
192+
}
193+
workspace, _ := config.PluginConfig("index", "workspace")
194+
apiKey, _ := config.PluginConfig("index", "api-key")
195+
cves, err := query.QueryCves(sb, cve, workspace, apiKey)
196+
if err != nil {
197+
return err
198+
}
199+
200+
if len(*cves) > 0 {
201+
for _, c := range *cves {
202+
skill.Log.Warnf("Detected %s at", cve)
203+
skill.Log.Warnf("")
204+
purl := c.Purl
205+
for _, p := range sb.Artifacts {
206+
if p.Purl == purl {
207+
skill.Log.Warnf(" %s", p.Purl)
208+
loc := p.Locations[0]
209+
for i, l := range sb.Source.Image.Config.RootFS.DiffIDs {
210+
if l.String() == loc.DiffId {
211+
h := sb.Source.Image.Config.History[i]
212+
skill.Log.Warnf(" ")
213+
skill.Log.Warnf(" Instruction: %s", h.CreatedBy)
214+
skill.Log.Warnf(" Layer %d: %s", i, loc.Digest)
215+
}
216+
}
217+
}
218+
}
219+
}
220+
os.Exit(1)
221+
} else {
222+
skill.Log.Infof("%s not detected", cve)
223+
os.Exit(0)
224+
}
225+
return nil
226+
},
227+
}
228+
cveCommandFlags := cveCommand.Flags()
229+
cveCommandFlags.StringVarP(&image, "image", "i", "", "Image reference to index")
230+
cveCommandFlags.StringVarP(&ociDir, "oci-dir", "d", "", "Path to image in OCI format")
231+
174232
cmd := &cobra.Command{
175233
Use: "index",
176234
Short: "Docker Index",
177235
}
178236

179-
cmd.AddCommand(loginCommand, logoutCommand, sbomCommand, uploadCommand)
237+
cmd.AddCommand(loginCommand, logoutCommand, sbomCommand, cveCommand, uploadCommand)
180238
return cmd
181239
},
182240
manager.Metadata{

query/package_cve.edn

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
[:find
2+
?cves
3+
:keys cves
4+
:in $ $before-db %% ?ctx
5+
:where
6+
7+
[(ground "%s") ?source-id]
8+
[(ground [%s]) ?packages]
9+
10+
[(adb/query (quote [:find
11+
?purl ?source-id ?source ?range ?url ?fixed-by
12+
(pull ?v [:vulnerability/source-id
13+
:vulnerability/source
14+
{:vulnerability/urls [:vulnerability.url/value :vulnerability.url/name]}
15+
{:vulnerability/references [:vulnerability.reference/source {:vulnerability.reference/scores [:vulnerability.reference.score/type :vulnerability.reference.score/value]}]}])
16+
(pull ?cve [:vulnerability/source-id
17+
:vulnerability/source
18+
:vulnerability/description
19+
{:vulnerability/urls [:vulnerability.url/value :vulnerability.url/name]}
20+
{:vulnerability/cwes [:vulnerability.cwe/source-id :vulnerability.cwe/name]}
21+
{:vulnerability/references [:vulnerability.reference/source {:vulnerability.reference/scores [:vulnerability.reference.score/type :vulnerability.reference.score/value]}]}])
22+
:keys purl source-id source vulnerable-range url fixed-by v cve
23+
:in $ $b %% ?ctx [?packages ?source-id]
24+
:where
25+
[?v :vulnerability/source-id ?source-id]
26+
[?v :vulnerability/source ?source]
27+
[?v :vulnerability/advisories ?adv]
28+
29+
[(untuple ?packages) [?package ...]]
30+
[(untuple ?package) [?purl ?type ?version ?url]]
31+
[?adv :vulnerability.advisory/url ?url]
32+
33+
[(missing? $ ?v :vulnerability/withdrawn-at)]
34+
[?adv :vulnerability.advisory/versions ?versions]
35+
[?versions :vulnerability.advisory.version/vulnerable-range ?range]
36+
(range-satisfied? ?type ?version ?source ?range)
37+
38+
(or-join [?v ?source-id]
39+
[?v :vulnerability/cve-id ?source-id]
40+
(and
41+
[(missing? $ ?v :vulnerability/cve-id)]
42+
[?v :vulnerability/source-id ?source-id]))
43+
44+
(or-join [?versions ?fixed-by]
45+
[?versions :vulnerability.advisory.version/fixed-by ?fixed-by]
46+
(and
47+
[(missing? $ ?versions :vulnerability.advisory.version/fixed-by)]
48+
[(ground "not fixed") ?fixed-by]))
49+
50+
(or-join [?v ?cve]
51+
(and
52+
[?v :vulnerability/cve-id ?cveId]
53+
[?cve :vulnerability/source-id ?cveId]
54+
[?cve :vulnerability/source "nist"])
55+
(and
56+
[?v :vulnerability/source-id ?cveId]
57+
[?cve :vulnerability/source-id ?cveId]
58+
[?cve :vulnerability/source "nist"])
59+
(and
60+
(not-join [?v]
61+
[?v :vulnerability/cve-id ?cveId]
62+
[?cve :vulnerability/source-id ?cveId]
63+
[?cve :vulnerability/source "nist"]
64+
)
65+
(not-join [?v]
66+
[?v :vulnerability/source-id ?cveId]
67+
[?cve :vulnerability/source-id ?cveId]
68+
[?cve :vulnerability/source "nist"]
69+
)
70+
([ground "n/a"] ?cve))
71+
)
72+
])
73+
?packages ?source-id)
74+
?cves]
75+
]

query/query.go

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package query
1919
import (
2020
_ "embed"
2121
"fmt"
22+
"github.com/docker/index-cli-plugin/internal"
2223
"net/http"
2324
"strings"
2425

@@ -44,8 +45,11 @@ var enabledSkillsQuery string
4445
//go:embed package_cves.edn
4546
var packageCvesQuery string
4647

48+
//go:embed package_cve.edn
49+
var packageCveQuery string
50+
4751
func CheckAuth(workspace string, apiKey string) (bool, error) {
48-
resp, err := query(enabledSkillsQuery, workspace, apiKey)
52+
resp, err := query(enabledSkillsQuery, "auth_check", workspace, apiKey)
4953
if err != nil {
5054
return false, errors.Wrap(err, "failed to check auth")
5155
}
@@ -55,45 +59,45 @@ func CheckAuth(workspace string, apiKey string) (bool, error) {
5559
return true, nil
5660
}
5761

58-
func QueryCves(sb *sbom.Sbom, workspace string, apiKey string) (*[]sbom.Cve, error) {
62+
func QueryCves(sb *sbom.Sbom, cve string, workspace string, apiKey string) (*[]sbom.Cve, error) {
5963
pkgs := make([]string, 0)
6064
for _, p := range sb.Artifacts {
6165
pkgs = append(pkgs, fmt.Sprintf(`["%s" "%s" "%s" "%s"]`, p.Purl, p.Type, p.Version, sbom.ToAdvisoryUrl(p)))
6266
}
6367

64-
resp, err := query(fmt.Sprintf(packageCvesQuery, strings.Join(pkgs, " ")), workspace, apiKey)
65-
if workspace == "" || apiKey == "" {
66-
var result QueryResult
67-
err = edn.NewDecoder(resp.Body).Decode(&result)
68-
if err != nil {
69-
return nil, errors.Wrapf(err, "failed to unmarshal response")
70-
}
71-
if len(result.Query.Data) > 0 {
72-
skill.Log.Infof("Detected %d vulnerabilities", len(result.Query.Data[0].Cves))
73-
return &result.Query.Data[0].Cves, nil
68+
var q, name string
69+
if cve == "" {
70+
q = fmt.Sprintf(packageCvesQuery, strings.Join(pkgs, " "))
71+
name = "cves_query"
72+
} else {
73+
q = fmt.Sprintf(packageCveQuery, cve, strings.Join(pkgs, " "))
74+
name = "cve_query"
75+
}
76+
resp, err := query(q, name, workspace, apiKey)
77+
var result QueryResult
78+
err = edn.NewDecoder(resp.Body).Decode(&result)
79+
if err != nil {
80+
return nil, errors.Wrapf(err, "failed to unmarshal response")
81+
}
82+
if len(result.Query.Data) > 0 {
83+
if len(result.Query.Data) == 1 {
84+
skill.Log.Infof("Detected %d vulnerability", len(result.Query.Data[0].Cves))
7485
} else {
75-
return nil, nil
86+
skill.Log.Infof("Detected %d vulnerabilities", len(result.Query.Data[0].Cves))
7687
}
88+
return &result.Query.Data[0].Cves, nil
7789
} else {
78-
var cves []CveResult
79-
err = edn.NewDecoder(resp.Body).Decode(&cves)
80-
if err != nil {
81-
return nil, errors.Wrapf(err, "failed to unmarshal response")
82-
}
83-
skill.Log.Infof("Detected %d vulnerabilities", len(cves[0].Cves))
84-
return &cves[0].Cves, nil
90+
return nil, nil
8591
}
8692
}
8793

88-
func query(query string, workspace string, apiKey string) (*http.Response, error) {
89-
url := "https://api.dso.docker.com/datalog/team/" + workspace
94+
func query(query string, name string, workspace string, apiKey string) (*http.Response, error) {
95+
url := fmt.Sprintf("https://api.dso.docker.com/datalog/team/%s/queries", workspace)
9096
if workspace == "" || apiKey == "" {
9197
url = "https://api.dso.docker.com/datalog/shared-vulnerability/queries"
92-
query = fmt.Sprintf(`{:queries [{:name "query" :query %s}]}`, query)
93-
} else {
94-
query = fmt.Sprintf(`{:query %s}`, query)
95-
}
9698

99+
}
100+
query = fmt.Sprintf(`{:queries [{:name "query" :query %s}]}`, query)
97101
client := &http.Client{}
98102
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(query))
99103
if err != nil {
@@ -103,6 +107,8 @@ func query(query string, workspace string, apiKey string) (*http.Response, error
103107
req.Header.Set("Authorization", "Bearer "+apiKey)
104108
}
105109
req.Header.Set("Content-Type", "application/edn")
110+
req.Header.Set("X-Docker-Client", fmt.Sprintf("index-cli-plugin/%s", internal.FromBuild().Version))
111+
req.Header.Set("X-Docker-Query", name)
106112
if err != nil {
107113
return nil, errors.Wrapf(err, "failed to create http client")
108114
}

0 commit comments

Comments
 (0)