diff --git a/src/internal/error/errors.go b/src/internal/error/errors.go index d37623f08..324f00788 100644 --- a/src/internal/error/errors.go +++ b/src/internal/error/errors.go @@ -92,6 +92,8 @@ const ( GeneralCode = "UNKNOWN" // DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied. DENIED = "DENIED" + // PROJECTPOLICYVIOLATION ... + PROJECTPOLICYVIOLATION = "PROJECTPOLICYVIOLATION" // ViolateForeignKeyConstraintCode is the error code for violating foreign key constraint error ViolateForeignKeyConstraintCode = "VIOLATE_FOREIGN_KEY_CONSTRAINT" ) diff --git a/src/server/middleware/contenttrust/contenttrust.go b/src/server/middleware/contenttrust/contenttrust.go new file mode 100644 index 000000000..4183c4ac5 --- /dev/null +++ b/src/server/middleware/contenttrust/contenttrust.go @@ -0,0 +1,99 @@ +package contenttrust + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/common/utils/notary" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/middlewares/util" + internal_errors "github.com/goharbor/harbor/src/internal/error" + "github.com/goharbor/harbor/src/server/middleware" + "net/http" + "net/http/httptest" +) + +// NotaryEndpoint ... +var NotaryEndpoint = "" + +// Middleware handle docker pull content trust check +func Middleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + doContentTrustCheck, mf := validate(req) + if !doContentTrustCheck { + next.ServeHTTP(rw, req) + return + } + rec := httptest.NewRecorder() + next.ServeHTTP(rec, req) + if rec.Result().StatusCode == http.StatusOK { + match, err := matchNotaryDigest(mf) + if err != nil { + pkgE := internal_errors.New(nil).WithCode(internal_errors.PROJECTPOLICYVIOLATION).WithMessage("Failed in communication with Notary please check the log") + msg := internal_errors.NewErrs(pkgE).Error() + http.Error(rw, msg, http.StatusInternalServerError) + return + } + if !match { + log.Debugf("digest mismatch, failing the response.") + pkgE := internal_errors.New(nil).WithCode(internal_errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed in Notary.") + msg := internal_errors.NewErrs(pkgE).Error() + http.Error(rw, msg, http.StatusPreconditionFailed) + return + } + } + middleware.CopyResp(rec, rw) + }) + } +} + +func validate(req *http.Request) (bool, *middleware.ManifestInfo) { + mf, ok := middleware.ManifestInfoFromContext(req.Context()) + if !ok { + return false, nil + } + _, err := mf.ManifestExists(req.Context()) + if err != nil { + return false, mf + } + if scannerPull, ok := middleware.ScannerPullFromContext(req.Context()); ok && scannerPull { + return false, mf + } + if !middleware.GetPolicyChecker().ContentTrustEnabled(mf.ProjectName) { + return false, mf + } + return true, mf +} + +func matchNotaryDigest(mf *middleware.ManifestInfo) (bool, error) { + if NotaryEndpoint == "" { + NotaryEndpoint = config.InternalNotaryEndpoint() + } + targets, err := notary.GetInternalTargets(NotaryEndpoint, util.TokenUsername, mf.Repository) + if err != nil { + return false, err + } + for _, t := range targets { + if mf.Digest != "" { + d, err := notary.DigestFromTarget(t) + if err != nil { + return false, err + } + if mf.Digest == d { + return true, nil + } + } else { + if t.Tag == mf.Tag { + log.Debugf("found reference: %s in notary, try to match digest.", mf.Tag) + d, err := notary.DigestFromTarget(t) + if err != nil { + return false, err + } + if mf.Digest == d { + return true, nil + } + } + } + } + log.Debugf("image: %#v, not found in notary", mf) + return false, nil +} diff --git a/src/server/middleware/contenttrust/contenttrust_test.go b/src/server/middleware/contenttrust/contenttrust_test.go new file mode 100644 index 000000000..c1f720fcc --- /dev/null +++ b/src/server/middleware/contenttrust/contenttrust_test.go @@ -0,0 +1 @@ +package contenttrust diff --git a/src/server/middleware/manifestinfo/manifest_info.go b/src/server/middleware/manifestinfo/manifest_info.go index 3bfa6d753..c522cde38 100644 --- a/src/server/middleware/manifestinfo/manifest_info.go +++ b/src/server/middleware/manifestinfo/manifest_info.go @@ -49,8 +49,9 @@ func parseManifestInfoFromPath(req *http.Request) (*middleware.ManifestInfo, err } info := &middleware.ManifestInfo{ - ProjectID: project.ProjectID, - Repository: repository, + ProjectID: project.ProjectID, + ProjectName: projectName, + Repository: repository, } dgt, err := digest.Parse(reference) diff --git a/src/server/middleware/util.go b/src/server/middleware/util.go index 4007eba18..76e8d9a3a 100644 --- a/src/server/middleware/util.go +++ b/src/server/middleware/util.go @@ -4,8 +4,22 @@ import ( "context" "fmt" "github.com/docker/distribution/reference" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/promgr" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/goharbor/harbor/src/pkg/repository" + "github.com/goharbor/harbor/src/pkg/scan/vuln" + "github.com/goharbor/harbor/src/pkg/scan/whitelist" + "github.com/goharbor/harbor/src/pkg/tag" "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + "net/http" + "net/http/httptest" "regexp" + "sync" ) type contextKey string @@ -42,10 +56,67 @@ var ( // ManifestInfo ... type ManifestInfo struct { - ProjectID int64 - Repository string - Tag string - Digest string + ProjectID int64 + ProjectName string + Repository string + Tag string + Digest string + + manifestExist bool + manifestExistErr error + manifestExistOnce sync.Once +} + +// ManifestExists ... +func (info *ManifestInfo) ManifestExists(ctx context.Context) (bool, error) { + info.manifestExistOnce.Do(func() { + + // ToDo: use the artifact controller method + total, repos, err := repository.Mgr.List(ctx, &q.Query{ + Keywords: map[string]interface{}{ + "Name": info.Repository, + }, + }) + if err != nil { + info.manifestExistErr = err + return + } + if total == 0 { + return + } + + total, tags, err := tag.Mgr.List(ctx, &q.Query{ + Keywords: map[string]interface{}{ + "Name": info.Tag, + "RepositoryID": repos[0].RepositoryID, + }, + }) + if err != nil { + info.manifestExistErr = err + return + } + if total == 0 { + return + } + + total, afs, err := artifact.Mgr.List(ctx, &q.Query{ + Keywords: map[string]interface{}{ + "ID": tags[0].ArtifactID, + }, + }) + if err != nil { + info.manifestExistErr = err + return + } + if total == 0 { + return + } + + info.Digest = afs[0].Digest + info.manifestExist = total > 0 + }) + + return info.manifestExist, info.manifestExistErr } // ArtifactInfo ... @@ -86,3 +157,89 @@ func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) { func NewScannerPullContext(ctx context.Context, scannerPull bool) context.Context { return context.WithValue(ctx, ScannerPullCtxKey, scannerPull) } + +// ScannerPullFromContext returns whether to bypass policy check +func ScannerPullFromContext(ctx context.Context) (bool, bool) { + info, ok := ctx.Value(ScannerPullCtxKey).(bool) + return info, ok +} + +// CopyResp ... +func CopyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) { + for k, v := range rec.Header() { + rw.Header()[k] = v + } + rw.WriteHeader(rec.Result().StatusCode) + rw.Write(rec.Body.Bytes()) +} + +// PolicyChecker checks the policy of a project by project name, to determine if it's needed to check the image's status under this project. +type PolicyChecker interface { + // contentTrustEnabled returns whether a project has enabled content trust. + ContentTrustEnabled(name string) bool + // vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity. + VulnerablePolicy(name string) (bool, vuln.Severity, models.CVEWhitelist) +} + +// PmsPolicyChecker ... +type PmsPolicyChecker struct { + pm promgr.ProjectManager +} + +// ContentTrustEnabled ... +func (pc PmsPolicyChecker) ContentTrustEnabled(name string) bool { + project, err := pc.pm.Get(name) + if err != nil { + log.Errorf("Unexpected error when getting the project, error: %v", err) + return true + } + if project == nil { + log.Debugf("project %s not found", name) + return false + } + return project.ContentTrustEnabled() +} + +// VulnerablePolicy ... +func (pc PmsPolicyChecker) VulnerablePolicy(name string) (bool, vuln.Severity, models.CVEWhitelist) { + project, err := pc.pm.Get(name) + wl := models.CVEWhitelist{} + if err != nil { + log.Errorf("Unexpected error when getting the project, error: %v", err) + return true, vuln.Unknown, wl + } + + mgr := whitelist.NewDefaultManager() + if project.ReuseSysCVEWhitelist() { + w, err := mgr.GetSys() + if err != nil { + log.Error(errors.Wrap(err, "policy checker: vulnerable policy")) + } else { + wl = *w + + // Use the real project ID + wl.ProjectID = project.ProjectID + } + } else { + w, err := mgr.Get(project.ProjectID) + if err != nil { + log.Error(errors.Wrap(err, "policy checker: vulnerable policy")) + } else { + wl = *w + } + } + + return project.VulPrevented(), vuln.ParseSeverityVersion3(project.Severity()), wl +} + +// NewPMSPolicyChecker returns an instance of an pmsPolicyChecker +func NewPMSPolicyChecker(pm promgr.ProjectManager) PolicyChecker { + return &PmsPolicyChecker{ + pm: pm, + } +} + +// GetPolicyChecker ... +func GetPolicyChecker() PolicyChecker { + return NewPMSPolicyChecker(config.GlobalProjectMgr) +} diff --git a/src/server/middleware/vulnerable/vulnerable.go b/src/server/middleware/vulnerable/vulnerable.go new file mode 100644 index 000000000..c92d866d5 --- /dev/null +++ b/src/server/middleware/vulnerable/vulnerable.go @@ -0,0 +1,119 @@ +package vulnerable + +import ( + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + internal_errors "github.com/goharbor/harbor/src/internal/error" + sc "github.com/goharbor/harbor/src/pkg/scan/api/scan" + "github.com/goharbor/harbor/src/pkg/scan/report" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/scan/vuln" + "github.com/goharbor/harbor/src/server/middleware" + "github.com/pkg/errors" + "net/http" + "net/http/httptest" +) + +// Middleware handle docker pull vulnerable check +func Middleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + doVulCheck, img, projectVulnerableSeverity, wl := validate(req) + if !doVulCheck { + next.ServeHTTP(rw, req) + return + } + rec := httptest.NewRecorder() + next.ServeHTTP(rec, req) + // only enable vul policy check the response 200 + if rec.Result().StatusCode == http.StatusOK { + // Invalid project ID + if wl.ProjectID == 0 { + err := errors.Errorf("project verification error: project %s", img.ProjectName) + sendError(err, rw) + return + } + + // Get the vulnerability summary + artifact := &v1.Artifact{ + NamespaceID: wl.ProjectID, + Repository: img.Repository, + Tag: img.Tag, + Digest: img.Digest, + MimeType: v1.MimeTypeDockerArtifact, + } + + cve := report.CVESet(wl.CVESet()) + summaries, err := sc.DefaultController.GetSummary( + artifact, + []string{v1.MimeTypeNativeReport}, + report.WithCVEWhitelist(&cve), + ) + + if err != nil { + err = errors.Wrap(err, "middleware: vulnerable handler") + sendError(err, rw) + return + } + + rawSummary, ok := summaries[v1.MimeTypeNativeReport] + // No report yet? + if !ok { + err = errors.Errorf("no scan report existing for the artifact: %s:%s@%s", img.Repository, img.Tag, img.Digest) + sendError(err, rw) + return + } + + summary := rawSummary.(*vuln.NativeReportSummary) + + // Do judgement + if summary.Severity.Code() >= projectVulnerableSeverity.Code() { + err = errors.Errorf("current image with '%q vulnerable' cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of %q from running.' "+ + "Please contact your project administrator for help'", summary.Severity, projectVulnerableSeverity) + sendError(err, rw) + return + } + + // Print scannerPull CVE list + if len(summary.CVEBypassed) > 0 { + for _, cve := range summary.CVEBypassed { + log.Infof("Vulnerable policy check: scannerPull CVE %s", cve) + } + } + } + middleware.CopyResp(rec, rw) + }) + } +} + +func validate(req *http.Request) (bool, *middleware.ManifestInfo, vuln.Severity, models.CVEWhitelist) { + var vs vuln.Severity + var wl models.CVEWhitelist + var mf *middleware.ManifestInfo + mf, ok := middleware.ManifestInfoFromContext(req.Context()) + if !ok { + return false, nil, vs, wl + } + + exist, err := mf.ManifestExists(req.Context()) + if err != nil || !exist { + return false, nil, vs, wl + } + + if scannerPull, ok := middleware.ScannerPullFromContext(req.Context()); ok && scannerPull { + return false, mf, vs, wl + } + // Is vulnerable policy set? + projectVulnerableEnabled, projectVulnerableSeverity, wl := middleware.GetPolicyChecker().VulnerablePolicy(mf.ProjectName) + if !projectVulnerableEnabled { + return false, mf, vs, wl + } + return true, mf, projectVulnerableSeverity, wl +} + +func sendError(err error, rw http.ResponseWriter) { + log.Error(err) + pkgE := internal_errors.New(err).WithCode(internal_errors.PROJECTPOLICYVIOLATION) + msg := internal_errors.NewErrs(pkgE).Error() + http.Error(rw, msg, http.StatusPreconditionFailed) +} diff --git a/src/server/middleware/vulnerable/vulnerable_test.go b/src/server/middleware/vulnerable/vulnerable_test.go new file mode 100644 index 000000000..50ef35100 --- /dev/null +++ b/src/server/middleware/vulnerable/vulnerable_test.go @@ -0,0 +1 @@ +package vulnerable diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index c0395ce8c..c5f875e3e 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -15,6 +15,9 @@ package registry import ( + "github.com/goharbor/harbor/src/server/middleware/contenttrust" + "github.com/goharbor/harbor/src/server/middleware/vulnerable" + "github.com/goharbor/harbor/src/core/config" pkg_repo "github.com/goharbor/harbor/src/pkg/repository" pkg_tag "github.com/goharbor/harbor/src/pkg/tag" @@ -52,7 +55,7 @@ func New(url *url.URL) http.Handler { // handle manifest // TODO maybe we should split it into several sub routers based on the method manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter() - manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), manifestinfo.Middleware(), regtoken.Middleware())) + manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), manifestinfo.Middleware(), regtoken.Middleware(), contenttrust.Middleware(), vulnerable.Middleware())) manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(proxy)) manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete()))