Skip to content

Commit

Permalink
Add support for vulnerability reports
Browse files Browse the repository at this point in the history
Implement generic support for vulnerability reports for images.

Implement vulnerability reports from Docker Scout.
  • Loading branch information
AlexGustafsson committed Nov 30, 2024
1 parent f3d5354 commit fe4938f
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 10 deletions.
26 changes: 26 additions & 0 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ components:
type: array
items:
$ref: '#/components/schemas/ImageLink'
vulnerabilities:
type: array
items:
$ref: '#/components/schemas/ImageVulnerability'
lastModified:
type: string
format: datetime
Expand Down Expand Up @@ -257,6 +261,28 @@ components:
- type
- url

ImageVulnerability:
type: object
properties:
id:
type: int
severity:
type: string
example: high
authority:
type: string
example: Docker Scout
description:
type: string
example: CVE-2024-9476
link:
type: string
format: url
required:
- id
- severity
- authority

Graph:
type: object
description: A graph explaining why the image is used.
Expand Down
Binary file added docs/screenshots/image-page-vulnerabilities.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 16 additions & 7 deletions internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ type PaginationMetadata struct {
}

type Image struct {
Reference string `json:"reference"`
LatestReference string `json:"latestReference,omitempty"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"`
Links []ImageLink `json:"links"`
LastModified time.Time `json:"lastModified"`
Image string `json:"image,omitempty"`
Reference string `json:"reference"`
LatestReference string `json:"latestReference,omitempty"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"`
Links []ImageLink `json:"links"`
Vulnerabilities []ImageVulnerability `json:"vulnerabilities"`
LastModified time.Time `json:"lastModified"`
Image string `json:"image,omitempty"`
}

type RawImage struct {
Expand All @@ -56,6 +57,14 @@ type ImageLink struct {
URL string `json:"url"`
}

type ImageVulnerability struct {
ID int `json:"id"`
Severity string `json:"severity"`
Authority string `json:"authority"`
Description string `json:"description,omitempty"`
Link string `json:"link,omitempty"`
}

type Graph struct {
Edges map[string]map[string]bool `json:"edges"`
Nodes map[string]GraphNode `json:"nodes"`
Expand Down
65 changes: 65 additions & 0 deletions internal/registry/docker/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package docker

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -209,6 +210,61 @@ func (c *Client) GetOrganizationOrUser(ctx context.Context, organizationOrUser s
return &result, nil
}

func (c *Client) GetVulnerabilityReport(ctx context.Context, repo string, digest string) (*VulnerabilityReport, error) {
body, err := json.Marshal(map[string]any{
"query": "query imageSummariesByDigest($v1:Context!,$v2:[String!]!,$v3:ScRepositoryInput){imageSummariesByDigest(context:$v1,digests:$v2,repository:$v3){digest,sbomState,vulnerabilityReport{critical,high,medium,low,unspecified,total}}}",
"variables": map[string]any{
"v1": map[string]any{},
"v2": []string{
digest,
},
"v3": map[string]any{
"hostName": "hub.docker.com",
"repoName": repo,
},
},
"operationName": "imageSummariesByDigest",
})
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.dso.docker.com/v1/graphql", bytes.NewReader(body))
if err != nil {
return nil, err
}

res, err := c.Client.DoCached(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, nil
} else if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %s", res.Status)
}

var result struct {
Data struct {
ImageSummariesByDigest []struct {
Digest string `json:"digest"`
SBOMStatae string `json:"sbomState"`
VulnerabilityReport *VulnerabilityReport `json:"vulnerabilityReport"`
} `json:"imageSummariesByDigest"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, err
}

if len(result.Data.ImageSummariesByDigest) != 1 {
return nil, nil
}

return result.Data.ImageSummariesByDigest[0].VulnerabilityReport, nil
}

type Page[T any] struct {
Count int `json:"count"`
Next *string `json:"next"`
Expand Down Expand Up @@ -298,3 +354,12 @@ type Entity struct {
Type string `json:"type"`
Badge string `json:"badge,omitempty"`
}

type VulnerabilityReport struct {
Criticial int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
Unspecified int `json:"unspecified"`
Total int `json:"total"`
}
15 changes: 15 additions & 0 deletions internal/registry/docker/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ func TestClientGetRepository(t *testing.T) {

json.NewEncoder(os.Stdout).Encode(repository)
}

func TestGetVulnerabilityReport(t *testing.T) {
if testing.Short() {
t.Skip()
}

client := &Client{
Client: httputil.NewClient(cachetest.NewCache(t), 24*time.Hour),
}

report, err := client.GetVulnerabilityReport(context.TODO(), "traefik", "sha256:bdeec8d8ac650ff774393581757a7fbd4bcdef555acd22b265c4641b3cf2256a")
require.NoError(t, err)

json.NewEncoder(os.Stdout).Encode(report)
}
5 changes: 5 additions & 0 deletions internal/registry/docker/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ func RepositoryUIPath(image oci.Reference) string {

return "https://hub.docker.com/r/" + url.PathEscape(owner) + "/" + url.PathEscape(name)
}

func TagUIPath(image oci.Reference, digest string) string {
owner, name, _ := strings.Cut(image.Path, "/")
return "https://hub.docker.com/layers/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/" + url.PathEscape(image.Tag) + "/images/" + url.PathEscape(strings.ReplaceAll(digest, ":", "-"))
}
2 changes: 2 additions & 0 deletions internal/registry/oci/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func (c *Client) GetManifests(ctx context.Context, image Reference) ([]Manifest,
SchemaVersion: manifest.SchemaVersion,
MediaType: manifest.MediaType,
Annotations: make(map[string]string),
Digest: manifest.Digest,
})
}

Expand All @@ -122,6 +123,7 @@ func (c *Client) GetManifests(ctx context.Context, image Reference) ([]Manifest,
SchemaVersion: manifest.SchemaVersion,
MediaType: manifest.MediaType,
Annotations: make(map[string]string),
Digest: manifest.Digest,
},
}, nil
}
Expand Down
1 change: 1 addition & 0 deletions internal/registry/oci/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Annotations map[string]string `json:"annotations"`
Digest string `json:"digest"`
}

func (m Manifest) SourceAnnotation() string {
Expand Down
10 changes: 10 additions & 0 deletions internal/store/createTablesIfNotExist.sql
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ CREATE TABLE IF NOT EXISTS images_graphs (
PRIMARY KEY (reference),
FOREIGN KEY(reference) REFERENCES images(reference) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS images_vulnerabilities (
id INTEGER PRIMARY KEY,
reference TEXT NOT NULL,
severity TEXT NOT NULL,
authority TEXT NOT NULL,
description TEXT NOT NULL,
link TEXT NOT NULL,
FOREIGN KEY(reference) REFERENCES images(reference) ON DELETE CASCADE
)
54 changes: 54 additions & 0 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ func (s *Store) InsertImage(ctx context.Context, image *models.Image) error {
}
}

// TODO: Removed vulnerabilities are not removed from db
for _, vulnerability := range image.Vulnerabilities {
statement, err := tx.PrepareContext(ctx, `INSERT INTO images_vulnerabilities
(reference, severity, authority, description, link)
VALUES
(?, ?, ?, ?, ?);`)
if err != nil {
tx.Rollback()
return err
}

_, err = statement.ExecContext(ctx, image.Reference, vulnerability.Severity, vulnerability.Authority, vulnerability.Description, vulnerability.Link)
statement.Close()
if err != nil {
tx.Rollback()
return err
}
}

return tx.Commit()
}

Expand Down Expand Up @@ -276,6 +295,11 @@ func (s *Store) GetImage(ctx context.Context, reference string) (*models.Image,
return nil, err
}

image.Vulnerabilities, err = s.GetImageVulnerabilities(ctx, reference)
if err != nil {
return nil, err
}

return &image, nil
}

Expand Down Expand Up @@ -339,6 +363,36 @@ func (s *Store) GetImagesLinks(ctx context.Context, reference string) ([]models.
return links, nil
}

func (s *Store) GetImageVulnerabilities(ctx context.Context, reference string) ([]models.ImageVulnerability, error) {
statement, err := s.db.PrepareContext(ctx, `SELECT id, severity, authority, description, link FROM images_vulnerabilities WHERE reference = ?;`)
if err != nil {
return nil, err
}
defer statement.Close()

res, err := statement.QueryContext(ctx, reference)
if err != nil {
return nil, err
}

vulnerabilities := make([]models.ImageVulnerability, 0)
for res.Next() {
var vulnerability models.ImageVulnerability
err := res.Scan(&vulnerability.ID, &vulnerability.Severity, &vulnerability.Authority, &vulnerability.Description, &vulnerability.Link)
if err != nil {
res.Close()
return nil, err
}
vulnerabilities = append(vulnerabilities, vulnerability)
}
res.Close()
if err := res.Err(); err != nil {
return nil, err
}

return vulnerabilities, nil
}

func (s *Store) GetTags(ctx context.Context) ([]string, error) {
statement, err := s.db.PrepareContext(ctx, `SELECT DISTINCT tag FROM images_tags;`)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func (w *Worker) ProcessRawImage(ctx context.Context, image models.RawImage) err
FullDescription: nil,
ReleaseNotes: nil,
Links: make([]models.ImageLink, 0),
Vulnerabilities: make([]models.ImageVulnerability, 0),
Graph: image.Graph,
}

Expand Down Expand Up @@ -111,6 +112,7 @@ func (w *Worker) ProcessRawImage(ctx context.Context, image models.RawImage) err
Tags: data.Tags,
Image: data.Image,
Links: data.Links,
Vulnerabilities: data.Vulnerabilities,
LastModified: time.Now(),
}
if data.LatestReference != nil {
Expand Down
12 changes: 12 additions & 0 deletions internal/workflow/imageworkflow/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Data struct {
FullDescription *models.ImageDescription
ReleaseNotes *models.ImageReleaseNotes
Links []models.ImageLink
Vulnerabilities []models.ImageVulnerability
Graph models.Graph
}

Expand Down Expand Up @@ -52,3 +53,14 @@ func (d *Data) InsertLinks(links []models.ImageLink) {
func (d *Data) InsertLink(link models.ImageLink) {
d.InsertLinks([]models.ImageLink{link})
}

func (d *Data) InsertVulnerabilities(vulnerabilities []models.ImageVulnerability) {
d.Lock()
defer d.Unlock()

d.Vulnerabilities = append(d.Vulnerabilities, vulnerabilities...)
}

func (d *Data) InsertVulnerability(vulnerability models.ImageVulnerability) {
d.InsertVulnerabilities([]models.ImageVulnerability{vulnerability})
}
Loading

0 comments on commit fe4938f

Please sign in to comment.