From a1a08ebed0407a2188e73575772efe3bd68e0b81 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Tue, 20 Jun 2017 11:03:13 +0800 Subject: [PATCH] provide API to get vulnerability details --- src/common/models/scan_job.go | 10 +++++ src/ui/api/repository.go | 83 +++++++++++++++++++++++++++++++---- src/ui/api/utils.go | 32 ++++++++++++++ src/ui/config/config.go | 5 +++ src/ui/router.go | 1 + 5 files changed, 122 insertions(+), 9 deletions(-) diff --git a/src/common/models/scan_job.go b/src/common/models/scan_job.go index a0ddaefa1..f821a6dc6 100644 --- a/src/common/models/scan_job.go +++ b/src/common/models/scan_job.go @@ -86,3 +86,13 @@ type ImageScanReq struct { Repo string `json:"repository"` Tag string `json:"tag"` } + +// VulnerabilityItem is an item in the vulnerability result returned by vulnerability details API. +type VulnerabilityItem struct { + ID string `json:"id"` + Severity Severity `json:"severity"` + Pkg string `json:"package"` + Version string `json:"version"` + Description string `json:"description"` + Fixed string `json:"fixedVersion,omitempty"` +} diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index bbf7f0795..e21dd2da1 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -27,6 +27,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" + "github.com/vmware/harbor/src/common/utils/clair" registry_error "github.com/vmware/harbor/src/common/utils/error" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/notary" @@ -295,20 +296,17 @@ func (ra *RepositoryAPI) Delete() { // GetTag returns the tag of a repository func (ra *RepositoryAPI) GetTag() { repository := ra.GetString(":splat") - - project, _ := utils.ParseRepository(repository) - exist, err := ra.ProjectMgr.Exist(project) + tag := ra.GetString(":tag") + exist, _, err := ra.checkExistence(repository, tag) if err != nil { - ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v", - project, err)) + ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of resource, error: %v", err)) return } - if !exist { - ra.HandleNotFound(fmt.Sprintf("project %s not found", project)) + ra.HandleNotFound(fmt.Sprintf("resource: %s:%s not found", repository, tag)) return } - + project, _ := utils.ParseRepository(repository) if !ra.SecurityCtx.HasReadPerm(project) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() @@ -325,7 +323,6 @@ func (ra *RepositoryAPI) GetTag() { return } - tag := ra.GetString(":tag") _, exist, err = client.ManifestExist(tag) if err != nil { ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of %s:%s: %v", repository, tag, err)) @@ -749,6 +746,49 @@ func (ra *RepositoryAPI) ScanImage() { } } +// VulnerabilityDetails fetch vulnerability info from clair, transform to Harbor's format and return to client. +func (ra *RepositoryAPI) VulnerabilityDetails() { + if !config.WithClair() { + log.Warningf("Harbor is not deployed with Clair, it's not impossible to get vulnerability details.") + ra.RenderError(http.StatusServiceUnavailable, "") + return + } + repository := ra.GetString(":splat") + tag := ra.GetString(":tag") + exist, digest, err := ra.checkExistence(repository, tag) + if err != nil { + ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of resource, error: %v", err)) + return + } + if !exist { + ra.HandleNotFound(fmt.Sprintf("resource: %s:%s not found", repository, tag)) + return + } + project, _ := utils.ParseRepository(repository) + if !ra.SecurityCtx.HasReadPerm(project) { + if !ra.SecurityCtx.IsAuthenticated() { + ra.HandleUnauthorized() + return + } + ra.HandleForbidden(ra.SecurityCtx.GetUsername()) + return + } + overview, err := dao.GetImgScanOverview(digest) + if err != nil { + ra.HandleInternalServerError(fmt.Sprintf("failed to get the scan overview, error: %v", err)) + return + } + clairClient := clair.NewClient(config.ClairEndpoint(), nil) + log.Debugf("The key for getting details: %s", overview.DetailsKey) + details, err := clairClient.GetResult(overview.DetailsKey) + if err != nil { + ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err)) + return + } + ra.Data["json"] = transformVulnerabilities(details) + ra.ServeJSON() +} + func getSignatures(repository, username string) (map[string]*notary.Target, error) { targets, err := notary.GetInternalTargets(config.InternalNotaryEndpoint(), username, repository) @@ -768,6 +808,31 @@ func getSignatures(repository, username string) (map[string]*notary.Target, erro return signatures, nil } +func (ra *RepositoryAPI) checkExistence(repository, tag string) (bool, string, error) { + project, _ := utils.ParseRepository(repository) + exist, err := ra.ProjectMgr.Exist(project) + if err != nil { + return false, "", fmt.Errorf("failed to check the existence of project %s: %v", project, err) + } + if !exist { + log.Errorf("project %s not found", project) + return false, "", nil + } + client, err := ra.initRepositoryClient(repository) + if err != nil { + return false, "", fmt.Errorf("failed to initialize the client for %s: %v", repository, err) + } + digest, exist, err := client.ManifestExist(tag) + if err != nil { + return false, "", fmt.Errorf("failed to check the existence of %s:%s: %v", repository, tag, err) + } + if !exist { + log.Errorf("%s not found", tag) + return false, "", nil + } + return true, digest, nil +} + //will return nil when it failed to get data. The parm "tag" is for logging only. func getScanOverview(digest string, tag string) *models.ImgScanOverview { data, err := dao.GetImgScanOverview(digest) diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index 2197a9c99..9dc47f639 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -27,6 +27,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" + "github.com/vmware/harbor/src/common/utils/clair" registry_error "github.com/vmware/harbor/src/common/utils/error" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/registry" @@ -557,3 +558,34 @@ func requestAsUI(method, url string, body io.Reader, expectSC int) error { } return nil } + +// transformVulnerabilities transforms the returned value of Clair API to a list of VulnerabilityItem +func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*models.VulnerabilityItem { + res := []*models.VulnerabilityItem{} + l := layerWithVuln.Layer + if l == nil { + return res + } + features := l.Features + if features == nil { + return res + } + for _, f := range features { + vulnerabilities := f.Vulnerabilities + if vulnerabilities == nil { + continue + } + for _, v := range vulnerabilities { + vItem := &models.VulnerabilityItem{ + ID: v.Name, + Pkg: f.Name, + Version: f.Version, + Severity: clair.ParseClairSev(v.Severity), + Fixed: v.FixedBy, + Description: v.Description, + } + res = append(res, vItem) + } + } + return res +} diff --git a/src/ui/config/config.go b/src/ui/config/config.go index b8ebce6c7..8a5f2745e 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -323,6 +323,11 @@ func WithClair() bool { return cfg[common.WithClair].(bool) } +// ClairEndpoint returns the end point of clair instance, by default it's the one deployed within Harbor. +func ClairEndpoint() string { + return "http://clair:6060" +} + // AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string. func AdmiralEndpoint() string { cfg, err := mg.Get() diff --git a/src/ui/router.go b/src/ui/router.go index 398d52940..d4a0e7b6b 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -77,6 +77,7 @@ func initRouters() { beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag") beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage") + beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails") beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests") beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures") beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")