Add scanning summaries column in the tag ls command

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
This commit is contained in:
Silvin Lubecki 2020-12-16 15:30:54 +01:00
Родитель 4b740d34be
Коммит 80da07b590
3 изменённых файлов: 206 добавлений и 11 удалений

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

@ -35,6 +35,8 @@ var (
Warn = utils.Yellow
// Error color should be used when something bad happened
Error = utils.Red
// Success color shold be used when something happened successfully
Success = utils.Green
// Emphasise color should be used with important content
Emphasise = utils.Green
// NoColor doesn't add any colors to the output

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

@ -38,40 +38,45 @@ const (
lsName = "ls"
)
type cliTag struct {
hub.Tag
Vulnerabilities *hub.ScanReportSummary
}
var (
defaultColumns = []column{
{"TAG", func(t hub.Tag) (string, int) { return t.Name, len(t.Name) }},
{"DIGEST", func(t hub.Tag) (string, int) {
{"TAG", func(t cliTag) (string, int) { return t.Name, len(t.Name) }},
{"DIGEST", func(t cliTag) (string, int) {
if len(t.Images) > 0 {
return t.Images[0].Digest, len(t.Images[0].Digest)
}
return "", 0
}},
{"STATUS", func(t hub.Tag) (string, int) {
{"STATUS", func(t cliTag) (string, int) {
return t.Status, len(t.Status)
}},
{"LAST UPDATE", func(t hub.Tag) (string, int) {
{"LAST UPDATE", func(t cliTag) (string, int) {
if t.LastUpdated.Nanosecond() == 0 {
return "", 0
}
s := fmt.Sprintf("%s ago", units.HumanDuration(time.Since(t.LastUpdated)))
return s, len(s)
}},
{"LAST PUSHED", func(t hub.Tag) (string, int) {
{"LAST PUSHED", func(t cliTag) (string, int) {
if t.LastPushed.Nanosecond() == 0 {
return "", 0
}
s := units.HumanDuration(time.Since(t.LastPushed))
return s, len(s)
}},
{"LAST PULLED", func(t hub.Tag) (string, int) {
{"LAST PULLED", func(t cliTag) (string, int) {
if t.LastPulled.Nanosecond() == 0 {
return "", 0
}
s := units.HumanDuration(time.Since(t.LastPulled))
return s, len(s)
}},
{"SIZE", func(t hub.Tag) (string, int) {
{"SIZE", func(t cliTag) (string, int) {
size := t.FullSize
if len(t.Images) > 0 {
size = 0
@ -83,9 +88,31 @@ var (
return s, len(s)
}},
}
scanColumn = column{
"VULNERABILITIES (H/M/L)",
func(t cliTag) (string, int) {
if t.Vulnerabilities != nil {
high := ansi.Success("0")
if t.Vulnerabilities.High > 0 {
high = ansi.Error(fmt.Sprintf("%d", t.Vulnerabilities.High))
}
medium := ansi.Success("0")
if t.Vulnerabilities.Medium > 0 {
medium = ansi.Warn(fmt.Sprintf("%d", t.Vulnerabilities.Medium))
}
low := ansi.Success("0")
if t.Vulnerabilities.Low > 0 {
low = ansi.Info(fmt.Sprintf("%d", t.Vulnerabilities.Low))
}
s := fmt.Sprintf("%s/%s/%s", high, medium, low)
return s, len(s)
}
return "", 0
},
}
platformColumn = column{
"OS/ARCH",
func(t hub.Tag) (string, int) {
func(t cliTag) (string, int) {
var platforms []string
for _, image := range t.Images {
platform := fmt.Sprintf("%s/%s", image.Os, image.Architecture)
@ -102,7 +129,7 @@ var (
type column struct {
header string
value func(t hub.Tag) (string, int)
value func(t cliTag) (string, int)
}
type listOptions struct {
@ -149,21 +176,43 @@ func runList(streams command.Streams, hubClient *hub.Client, opts listOptions, r
if ordering != "" {
reqOps = append(reqOps, hub.WithSortingOrder(ordering))
}
now := time.Now()
tags, total, err := hubClient.GetTags(repository, reqOps...)
if err != nil {
return err
}
tagList := makeTagList(tags)
now = time.Now()
// TODO: test if scanning enabled or not via entitlement API
scanSummaries, err := hubClient.GetScanSummaries(repository, tagList...)
if err != nil {
return err
}
if len(scanSummaries) > 0 {
defaultColumns = append(defaultColumns, scanColumn)
}
var cliTags []cliTag
for _, t := range tags {
var vulns *hub.ScanReportSummary
if summary, ok := scanSummaries[t.Images[0].Digest]; ok {
vulns = &summary
}
cliTags = append(cliTags, cliTag{
Tag: t,
Vulnerabilities: vulns,
})
}
if opts.platforms {
defaultColumns = append(defaultColumns, platformColumn)
}
return opts.Print(streams.Out(), tags, printTags(total))
return opts.Print(streams.Out(), cliTags, printTags(total))
}
func printTags(total int) format.PrettyPrinter {
return func(out io.Writer, values interface{}) error {
tags := values.([]hub.Tag)
tags := values.([]cliTag)
tw := tabwriter.New(out, " ")
for _, column := range defaultColumns {
tw.Column(ansi.Header(column.header), len(column.header))
@ -221,3 +270,11 @@ func mapOrdering(order string) (string, error) {
return "", fmt.Errorf(`unknown sorting column %q: should be either "name" or "updated"`, fields[0])
}
}
func makeTagList(tags []hub.Tag) []string {
var tagsList []string
for _, t := range tags {
tagsList = append(tagsList, t.Images[0].Digest)
}
return tagsList
}

136
internal/hub/scans.go Normal file
Просмотреть файл

@ -0,0 +1,136 @@
/*
Copyright 2020 Docker Hub Tool authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hub
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
const (
// ScansURL path to the Hub API listing the scans
// POST /reports/{scanner}/{registry}/{account}/{repo}
ScansURL = "/api/scan/v1/reports/snyk/docker.io/%s/"
// ScanURL path to the Hub API describing a scan
// GET /reports/{scanner}/{registry}/{account}/{repo}/{digest}/{scanned_at}
ScanURL = "/api/scan/v1/reports/snyk/docker.io/%s/%s/%s/newest"
)
//ScanReportSummary summaries how many vulnerabilities were found
type ScanReportSummary struct {
High int
Medium int
Low int
Total int
ScannedAt time.Time
}
//GetScanSummaries calls the hub repo API and returns all the scan summaries
func (c *Client) GetScanSummaries(repository string, digests ...string) (map[string]ScanReportSummary, error) {
digests = deduplicateDigests(digests)
repoPath, err := getRepoPath(repository)
if err != nil {
return nil, err
}
u, err := url.Parse(c.domain + fmt.Sprintf(ScansURL, repoPath))
if err != nil {
return nil, err
}
input := hubScanReportSummaryBulkInput{Digests: digests}
data, err := json.Marshal(input)
if err != nil {
return nil, err
}
body := bytes.NewBuffer(data)
req, err := http.NewRequest("POST", u.String(), body)
if err != nil {
return nil, err
}
response, err := c.doRequest(req, withHubToken(c.token))
if err != nil {
return nil, err
}
var hubResponse hubScanReportSummaryBulkOutput
if err := json.Unmarshal(response, &hubResponse); err != nil {
return nil, err
}
return toScanSummary(hubResponse), nil
}
type hubScanReportSummaryBulkInput struct {
Digests []string `json:"digests"`
}
type hubScanReportSummaryBulkOutput struct {
Reports map[string]hubScanReportSummary `json:"Reports"`
}
type hubScanReportSummary struct {
Registry string `json:"registry"`
Account string `json:"account"`
Repo string `json:"repo"`
Digest string `json:"digest"`
ScannedAt string `json:"scannedAt"`
Scanner string `json:"scanner"`
Vulnerabilities *hubScanVulnSummary `json:"vulnerabilities,omitempty"`
Error *string `json:"error,omitempty"`
}
type hubScanVulnSummary struct {
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
Total int `json:"total"`
}
func toScanSummary(reports hubScanReportSummaryBulkOutput) map[string]ScanReportSummary {
summaries := map[string]ScanReportSummary{}
for sha, summary := range reports.Reports {
if summary.Error == nil && summary.Vulnerabilities != nil {
scannedAt, err := strconv.Atoi(summary.ScannedAt)
if err != nil {
continue
}
summaries[sha] = ScanReportSummary{
High: summary.Vulnerabilities.High,
Medium: summary.Vulnerabilities.Medium,
Low: summary.Vulnerabilities.Low,
Total: summary.Vulnerabilities.Total,
ScannedAt: time.Unix(int64(scannedAt), 0),
}
}
}
return summaries
}
func deduplicateDigests(digests []string) []string {
dedup := map[string]bool{}
for _, d := range digests {
dedup[d] = true
}
var result []string
for digest := range dedup {
result = append(result, digest)
}
return result
}