зеркало из https://github.com/docker/hub-tool.git
Add scanning summaries column in the tag ls command
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
This commit is contained in:
Родитель
4b740d34be
Коммит
80da07b590
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Загрузка…
Ссылка в новой задаче