mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
vul interceptor
update fix http no found fix test fix test update fix typo fix travis update per comment update typo
This commit is contained in:
parent
476494d122
commit
5a26ab1a53
@ -3,6 +3,7 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/adminserver/client"
|
||||||
"github.com/vmware/harbor/src/common"
|
"github.com/vmware/harbor/src/common"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
|
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
var endpoint = "10.117.4.142"
|
var endpoint = "10.117.4.142"
|
||||||
var notaryServer *httptest.Server
|
var notaryServer *httptest.Server
|
||||||
var adminServer *httptest.Server
|
var adminServer *httptest.Server
|
||||||
|
var adminserverClient client.Client
|
||||||
|
|
||||||
var admiralEndpoint = "http://127.0.0.1:8282"
|
var admiralEndpoint = "http://127.0.0.1:8282"
|
||||||
var token = ""
|
var token = ""
|
||||||
@ -43,6 +45,7 @@ func TestMain(m *testing.M) {
|
|||||||
if err := config.Init(); err != nil {
|
if err := config.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
adminserverClient = client.NewClient(adminServer.URL, nil)
|
||||||
result := m.Run()
|
result := m.Run()
|
||||||
if result != 0 {
|
if result != 0 {
|
||||||
os.Exit(result)
|
os.Exit(result)
|
||||||
@ -95,18 +98,46 @@ func TestEnvPolicyChecker(t *testing.T) {
|
|||||||
if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil {
|
if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil {
|
||||||
t.Fatalf("Failed to set env variable: %v", err)
|
t.Fatalf("Failed to set env variable: %v", err)
|
||||||
}
|
}
|
||||||
|
if err2 := os.Setenv("PROJECT_VULNERABLE", "1"); err2 != nil {
|
||||||
|
t.Fatalf("Failed to set env variable: %v", err2)
|
||||||
|
}
|
||||||
|
if err3 := os.Setenv("PROJECT_SEVERITY", "negligible"); err3 != nil {
|
||||||
|
t.Fatalf("Failed to set env variable: %v", err3)
|
||||||
|
}
|
||||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("whatever")
|
contentTrustFlag := getPolicyChecker().contentTrustEnabled("whatever")
|
||||||
vulFlag := getPolicyChecker().vulnerableEnabled("whatever")
|
vulFlag, sev := getPolicyChecker().vulnerablePolicy("whatever")
|
||||||
assert.True(contentTrustFlag)
|
assert.True(contentTrustFlag)
|
||||||
assert.False(vulFlag)
|
assert.True(vulFlag)
|
||||||
|
assert.Equal(sev, models.SevNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPMSPolicyChecker(t *testing.T) {
|
func TestPMSPolicyChecker(t *testing.T) {
|
||||||
|
|
||||||
|
var defaultConfigAdmiral = map[string]interface{}{
|
||||||
|
common.ExtEndpoint: "https://" + endpoint,
|
||||||
|
common.WithNotary: true,
|
||||||
|
common.CfgExpiration: 5,
|
||||||
|
common.AdmiralEndpoint: admiralEndpoint,
|
||||||
|
}
|
||||||
|
adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer adminServer.Close()
|
||||||
|
if err := os.Setenv("ADMIN_SERVER_URL", adminServer.URL); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := config.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
pm := pms.NewProjectManager(admiralEndpoint, token)
|
pm := pms.NewProjectManager(admiralEndpoint, token)
|
||||||
name := "project_for_test_get_true"
|
name := "project_for_test_get_sev_low"
|
||||||
id, err := pm.Create(&models.Project{
|
id, err := pm.Create(&models.Project{
|
||||||
Name: name,
|
Name: name,
|
||||||
EnableContentTrust: true,
|
EnableContentTrust: true,
|
||||||
|
PreventVulnerableImagesFromRunning: false,
|
||||||
|
PreventVulnerableImagesFromRunningSeverity: "low",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer func(id int64) {
|
defer func(id int64) {
|
||||||
@ -117,29 +148,27 @@ func TestPMSPolicyChecker(t *testing.T) {
|
|||||||
project, err := pm.Get(id)
|
project, err := pm.Get(id)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, id, project.ProjectID)
|
assert.Equal(t, id, project.ProjectID)
|
||||||
server, err2 := utilstest.NewAdminserver(nil)
|
|
||||||
if err2 != nil {
|
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
|
||||||
t.Fatalf("failed to create a mock admin server: %v", err2)
|
|
||||||
}
|
|
||||||
defer server.Close()
|
|
||||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_true")
|
|
||||||
assert.True(t, contentTrustFlag)
|
assert.True(t, contentTrustFlag)
|
||||||
|
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||||
|
assert.False(t, projectVulnerableEnabled)
|
||||||
|
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchNotaryDigest(t *testing.T) {
|
func TestMatchNotaryDigest(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
//The data from common/utils/notary/helper_test.go
|
//The data from common/utils/notary/helper_test.go
|
||||||
img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo"}
|
img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo", "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"}
|
||||||
img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo"}
|
img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo", "sha256:12345678"}
|
||||||
res1, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")
|
|
||||||
|
res1, err := matchNotaryDigest(img1)
|
||||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
||||||
assert.True(res1)
|
assert.True(res1)
|
||||||
res2, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a8")
|
|
||||||
assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img1)
|
res2, err := matchNotaryDigest(img2)
|
||||||
|
assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img2)
|
||||||
assert.False(res2)
|
assert.False(res2)
|
||||||
res3, err := matchNotaryDigest(img2, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")
|
|
||||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img2)
|
|
||||||
assert.False(res3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCopyResp(t *testing.T) {
|
func TestCopyResp(t *testing.T) {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "github.com/vmware/harbor/src/ui/api"
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/clair"
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
"github.com/vmware/harbor/src/common/utils/notary"
|
"github.com/vmware/harbor/src/common/utils/notary"
|
||||||
|
// "github.com/vmware/harbor/src/ui/api"
|
||||||
"github.com/vmware/harbor/src/ui/config"
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
"github.com/vmware/harbor/src/ui/projectmanager"
|
"github.com/vmware/harbor/src/ui/projectmanager"
|
||||||
"github.com/vmware/harbor/src/ui/projectmanager/pms"
|
"github.com/vmware/harbor/src/ui/projectmanager/pms"
|
||||||
@ -26,6 +29,9 @@ const (
|
|||||||
tokenUsername = "admin"
|
tokenUsername = "admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Record the docker deamon raw response.
|
||||||
|
var rec *httptest.ResponseRecorder
|
||||||
|
|
||||||
// NotaryEndpoint , exported for testing.
|
// NotaryEndpoint , exported for testing.
|
||||||
var NotaryEndpoint = config.InternalNotaryEndpoint()
|
var NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||||
|
|
||||||
@ -51,8 +57,8 @@ func MatchPullManifest(req *http.Request) (bool, string, string) {
|
|||||||
type policyChecker interface {
|
type policyChecker interface {
|
||||||
// contentTrustEnabled returns whether a project has enabled content trust.
|
// contentTrustEnabled returns whether a project has enabled content trust.
|
||||||
contentTrustEnabled(name string) bool
|
contentTrustEnabled(name string) bool
|
||||||
// vulnerableEnabled returns whether a project has enabled content trust.
|
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
|
||||||
vulnerableEnabled(name string) bool
|
vulnerablePolicy(name string) (bool, models.Severity)
|
||||||
}
|
}
|
||||||
|
|
||||||
//For testing
|
//For testing
|
||||||
@ -61,9 +67,8 @@ type envPolicyChecker struct{}
|
|||||||
func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
|
func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
|
||||||
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
|
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
|
||||||
}
|
}
|
||||||
func (ec envPolicyChecker) vulnerableEnabled(name string) bool {
|
func (ec envPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||||
// TODO: May need get more information in vulnerable policies.
|
return os.Getenv("PROJECT_VULNERABLE") == "1", clair.ParseClairSev(os.Getenv("PROJECT_SEVERITY"))
|
||||||
return os.Getenv("PROJECT_VULNERABBLE") == "1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pmsPolicyChecker struct {
|
type pmsPolicyChecker struct {
|
||||||
@ -78,8 +83,13 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
|
|||||||
}
|
}
|
||||||
return project.EnableContentTrust
|
return project.EnableContentTrust
|
||||||
}
|
}
|
||||||
func (pc pmsPolicyChecker) vulnerableEnabled(name string) bool {
|
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||||
return true
|
project, err := pc.pm.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||||
|
return true, models.SevUnknown
|
||||||
|
}
|
||||||
|
return project.PreventVulnerableImagesFromRunning, clair.ParseClairSev(project.PreventVulnerableImagesFromRunningSeverity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
||||||
@ -101,7 +111,7 @@ type imageInfo struct {
|
|||||||
repository string
|
repository string
|
||||||
tag string
|
tag string
|
||||||
projectName string
|
projectName string
|
||||||
// digest string
|
digest string
|
||||||
}
|
}
|
||||||
|
|
||||||
type urlHandler struct {
|
type urlHandler struct {
|
||||||
@ -120,32 +130,18 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
http.Error(rw, fmt.Sprintf("Bad repository name: %s", repository), http.StatusBadRequest)
|
http.Error(rw, fmt.Sprintf("Bad repository name: %s", repository), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/*
|
rec = httptest.NewRecorder()
|
||||||
//Need to get digest of the image.
|
uh.next.ServeHTTP(rec, req)
|
||||||
endpoint, err := config.RegistryURL()
|
if rec.Result().StatusCode != http.StatusOK {
|
||||||
if err != nil {
|
copyResp(rec, rw)
|
||||||
log.Errorf("Error getting Registry URL: %v", err)
|
|
||||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rc, err := api.NewRepositoryClient(endpoint, false, username, repository, "repository", repository, "pull")
|
digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error creating repository Client: %v", err)
|
|
||||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
digest, exist, err := rc.ManifestExist(tag)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get digest for tag: %s, error: %v", tag, err)
|
|
||||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
img := imageInfo{
|
img := imageInfo{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
projectName: components[0],
|
projectName: components[0],
|
||||||
|
digest: digest,
|
||||||
}
|
}
|
||||||
log.Debugf("image info of the request: %#v", img)
|
log.Debugf("image info of the request: %#v", img)
|
||||||
|
|
||||||
@ -171,31 +167,70 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
|||||||
cth.next.ServeHTTP(rw, req)
|
cth.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//May need to update status code, let's use recorder
|
match, err := matchNotaryDigest(img)
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
cth.next.ServeHTTP(rec, req)
|
|
||||||
if rec.Result().StatusCode != http.StatusOK {
|
|
||||||
copyResp(rec, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debugf("showing digest")
|
|
||||||
digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
|
||||||
log.Debugf("digest: %s", digest)
|
|
||||||
match, err := matchNotaryDigest(img, digest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, "Failed in communication with Notary please check the log", http.StatusInternalServerError)
|
http.Error(rw, "Failed in communication with Notary please check the log", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if match {
|
if !match {
|
||||||
log.Debugf("Passing the response to outter responseWriter")
|
|
||||||
copyResp(rec, rw)
|
|
||||||
} else {
|
|
||||||
log.Debugf("digest mismatch, failing the response.")
|
log.Debugf("digest mismatch, failing the response.")
|
||||||
http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
|
http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
cth.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchNotaryDigest(img imageInfo, digest string) (bool, error) {
|
type vulnerableHandler struct {
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
imgRaw := req.Context().Value(imageInfoCtxKey)
|
||||||
|
if imgRaw == nil || !config.WithClair() {
|
||||||
|
vh.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo)
|
||||||
|
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||||
|
if !projectVulnerableEnabled {
|
||||||
|
vh.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overview, err := dao.GetImgScanOverview(img.digest)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get ImgScanOverview with repo: %s, tag: %s, digest: %s. Error: %v", img.repository, img.tag, img.digest, err)
|
||||||
|
http.Error(rw, "Failed to get ImgScanOverview.", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if overview == nil {
|
||||||
|
log.Debugf("cannot get the image scan overview info, failing the response.")
|
||||||
|
http.Error(rw, "Cannot get the image scan overview info.", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageSev := overview.Sev
|
||||||
|
if imageSev > int(projectVulnerableSeverity) {
|
||||||
|
log.Debugf("the image severity is higher then project setting, failing the response.")
|
||||||
|
http.Error(rw, "The image scan result doesn't pass the project setting.", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vh.next.ServeHTTP(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type funnelHandler struct {
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fu funnelHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
imgRaw := req.Context().Value(imageInfoCtxKey)
|
||||||
|
if imgRaw != nil {
|
||||||
|
log.Debugf("Return the original response as no the interceptor takes action.")
|
||||||
|
copyResp(rec, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fu.next.ServeHTTP(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchNotaryDigest(img imageInfo) (bool, error) {
|
||||||
targets, err := notary.GetInternalTargets(NotaryEndpoint, tokenUsername, img.repository)
|
targets, err := notary.GetInternalTargets(NotaryEndpoint, tokenUsername, img.repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -207,7 +242,7 @@ func matchNotaryDigest(img imageInfo, digest string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return digest == d, nil
|
return img.digest == d, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Debugf("image: %#v, not found in notary", img)
|
log.Debugf("image: %#v, not found in notary", img)
|
||||||
|
@ -42,7 +42,7 @@ func Init(urls ...string) error {
|
|||||||
}
|
}
|
||||||
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||||
//TODO: add vulnerable interceptor.
|
//TODO: add vulnerable interceptor.
|
||||||
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}}
|
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: vulnerableHandler{next: funnelHandler{next: Proxy}}}}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user