Skip to content

Commit e4baa13

Browse files
committed
Improved handling of deeply nested repositories
1 parent 85ab139 commit e4baa13

File tree

5 files changed

+155
-57
lines changed

5 files changed

+155
-57
lines changed

cmd/cso/main.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ func main() {
4646
}).Methods(http.MethodGet)
4747

4848
// setup caps
49-
manifest := api.NewCapabilities(adapter.NewHarborAdapter(router))
49+
dst := adapter.NewHarborAdapter()
50+
manifest := api.NewCapabilities(dst)
51+
middleware := api.NewMiddleware(dst)
5052

5153
// app paths
5254
router.Handle("/.well-known/app-capabilities", manifest).
5355
Methods(http.MethodGet)
56+
router.PathPrefix("/cso/v1/repository/").
57+
Handler(middleware).
58+
Methods(http.MethodGet)
5459

5560
serverless.NewBuilder(router).
5661
WithPort(e.Port).

internal/adapter/harbor.go

+9-53
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,22 @@ import (
2323
"fmt"
2424
"github.com/djcass44/cso-proxy/internal/adapter/harbor"
2525
"github.com/djcass44/go-utils/pkg/httputils"
26-
"github.com/gorilla/mux"
2726
"github.com/quay/container-security-operator/secscan"
2827
"github.com/quay/container-security-operator/secscan/quay"
2928
log "github.com/sirupsen/logrus"
3029
"io/ioutil"
3130
"net"
3231
"net/http"
3332
"net/url"
34-
"path/filepath"
33+
"strings"
3534
"time"
3635
)
3736

3837
type Harbor struct{}
3938

40-
func NewHarborAdapter(router *mux.Router) *Harbor {
39+
func NewHarborAdapter() *Harbor {
4140
h := new(Harbor)
4241

43-
router.HandleFunc("/cso/v1/repository/{registry}/{namespace}/{reponame}/manifest/{digest}/security", h.ManifestSecurity).
44-
Methods(http.MethodGet)
45-
router.HandleFunc("/cso/v1/repository/{namespace}/{reponame}/manifest/{digest}/security", h.ManifestSecurity).
46-
Methods(http.MethodGet)
47-
router.HandleFunc("/cso/v1/repository/{registry}/{namespace}/{reponame}/image/{imageid}/security", h.ImageSecurity).
48-
Methods(http.MethodGet)
49-
router.HandleFunc("/cso/v1/repository/{namespace}/{reponame}/image/{imageid}/security", h.ImageSecurity).
50-
Methods(http.MethodGet)
51-
5242
return h
5343
}
5444

@@ -70,56 +60,22 @@ func (*Harbor) Capabilities(uri *url.URL) *quay.AppCapabilities {
7060
ImageSecurity: struct {
7161
RestApiTemplate string `json:"rest-api-template"`
7262
}{
73-
RestApiTemplate: fmt.Sprintf("%s/cso/v1/repository/{namespace}/{reponame}/image/{imageid}/security", appURL),
63+
RestApiTemplate: "",
7464
},
7565
},
7666
}
7767
}
7868

79-
func (h *Harbor) ManifestSecurity(w http.ResponseWriter, r *http.Request) {
80-
features, vulnerabilities := r.URL.Query().Get("features") == "true", r.URL.Query().Get("vulnerabilities") == "true"
81-
vars := mux.Vars(r)
82-
registry, namespace, reponame, digest := vars["registry"], vars["namespace"], vars["reponame"], vars["digest"]
83-
log.WithContext(r.Context()).WithFields(log.Fields{
84-
"registry": registry,
69+
func (h *Harbor) ManifestSecurity(ctx context.Context, path, digest string, opts Opts) (*secscan.Response, int, error) {
70+
bits := strings.SplitN(path, "/", 2)
71+
namespace, reponame := bits[0], bits[1]
72+
log.WithContext(ctx).WithFields(log.Fields{
8573
"namespace": namespace,
8674
"reponame": reponame,
8775
"digest": digest,
8876
}).Infof("fetching manifest information")
89-
uri := fmt.Sprintf("https://%s", r.Host)
90-
if registry != "" {
91-
reponame = url.PathEscape(filepath.Join(namespace, reponame))
92-
namespace = registry
93-
}
94-
report, code, err := h.vulnerabilityInfo(r.Context(), uri, namespace, reponame, digest, features, vulnerabilities)
95-
if err != nil {
96-
http.Error(w, err.Error(), code)
97-
return
98-
}
99-
httputils.ReturnJSON(w, code, report)
100-
}
101-
102-
func (h *Harbor) ImageSecurity(w http.ResponseWriter, r *http.Request) {
103-
features, vulnerabilities := r.URL.Query().Get("features") == "true", r.URL.Query().Get("vulnerabilities") == "true"
104-
vars := mux.Vars(r)
105-
registry, namespace, reponame, imageid := vars["registry"], vars["namespace"], vars["reponame"], vars["imageid"]
106-
log.WithContext(r.Context()).WithFields(log.Fields{
107-
"registry": registry,
108-
"namespace": namespace,
109-
"reponame": reponame,
110-
"imageid": imageid,
111-
}).Infof("fetching image information")
112-
if registry != "" {
113-
reponame = url.PathEscape(filepath.Join(namespace, reponame))
114-
namespace = registry
115-
}
116-
uri := fmt.Sprintf("https://%s", r.Host)
117-
report, code, err := h.vulnerabilityInfo(r.Context(), uri, namespace, reponame, imageid, features, vulnerabilities)
118-
if err != nil {
119-
http.Error(w, err.Error(), code)
120-
return
121-
}
122-
httputils.ReturnJSON(w, code, report)
77+
reponame = url.PathEscape(reponame)
78+
return h.vulnerabilityInfo(ctx, opts.URI, namespace, reponame, digest, opts.Features, opts.Vulnerabilities)
12379
}
12480

12581
func (*Harbor) vulnerabilityInfo(ctx context.Context, uri, namespace, repository, reference string, showFeatures, showVulnerabilities bool) (*secscan.Response, int, error) {

internal/adapter/types.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@
1818
package adapter
1919

2020
import (
21+
"context"
2122
"github.com/djcass44/cso-proxy/internal/adapter/harbor"
23+
"github.com/quay/container-security-operator/secscan"
2224
"github.com/quay/container-security-operator/secscan/quay"
23-
"net/http"
2425
"net/url"
2526
)
2627

2728
type Adapter interface {
2829
Capabilities(uri *url.URL) *quay.AppCapabilities
29-
ManifestSecurity(w http.ResponseWriter, r *http.Request)
30-
ImageSecurity(w http.ResponseWriter, r *http.Request)
30+
ManifestSecurity(ctx context.Context, path, digest string, opts Opts) (*secscan.Response, int, error)
31+
}
32+
33+
type Opts struct {
34+
URI string
35+
Features bool
36+
Vulnerabilities bool
3137
}
3238

3339
type HarborScan map[string]harbor.Report

internal/api/middleware.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2022 Django Cass
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package api
19+
20+
import (
21+
"fmt"
22+
"github.com/djcass44/cso-proxy/internal/adapter"
23+
"github.com/djcass44/go-utils/pkg/httputils"
24+
"net/http"
25+
"regexp"
26+
"strings"
27+
)
28+
29+
var ManifestRegex = regexp.MustCompile(`^/cso/v1/repository/([^/]+/){2,}manifest/([^/]+)/security$`)
30+
31+
type Middleware struct {
32+
dst adapter.Adapter
33+
}
34+
35+
func NewMiddleware(dst adapter.Adapter) *Middleware {
36+
return &Middleware{
37+
dst: dst,
38+
}
39+
}
40+
41+
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
42+
if !ManifestRegex.MatchString(r.URL.Path) {
43+
http.NotFound(w, r)
44+
return
45+
}
46+
features, vulnerabilities := r.URL.Query().Get("features") == "true", r.URL.Query().Get("vulnerabilities") == "true"
47+
path := strings.SplitN(strings.TrimPrefix(r.URL.Path, "/cso/v1/repository/"), "/manifest/", 2)[0]
48+
digest := ManifestRegex.FindStringSubmatch(r.URL.Path)[2]
49+
resp, code, err := m.dst.ManifestSecurity(r.Context(), path, digest, adapter.Opts{
50+
URI: fmt.Sprintf("https://%s", r.Host),
51+
Features: features,
52+
Vulnerabilities: vulnerabilities,
53+
})
54+
if err != nil {
55+
http.Error(w, err.Error(), code)
56+
return
57+
}
58+
httputils.ReturnJSON(w, code, resp)
59+
}

internal/api/middleware_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2022 Django Cass
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package api
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"github.com/djcass44/cso-proxy/internal/adapter"
24+
"github.com/quay/container-security-operator/secscan"
25+
"github.com/quay/container-security-operator/secscan/quay"
26+
"github.com/stretchr/testify/assert"
27+
"net/http"
28+
"net/http/httptest"
29+
"net/url"
30+
"testing"
31+
)
32+
33+
type testAdapter struct{}
34+
35+
func (t testAdapter) Capabilities(*url.URL) *quay.AppCapabilities {
36+
return &quay.AppCapabilities{}
37+
}
38+
39+
func (t testAdapter) ManifestSecurity(context.Context, string, string, adapter.Opts) (*secscan.Response, int, error) {
40+
return &secscan.Response{
41+
Status: "",
42+
Data: secscan.Data{},
43+
}, http.StatusOK, nil
44+
}
45+
46+
func TestMiddleware_ServeHTTP(t *testing.T) {
47+
var cases = []struct {
48+
path string
49+
code int
50+
}{
51+
{
52+
"/cso/v1/repository/registry.gitlab.com/av1o/base-images/alpine/manifest/sha256:f42e1d4f05bfd2911c7a205588348d06c7af7ec9bb46e2cb4846e733fb0399da/security",
53+
http.StatusOK,
54+
},
55+
{
56+
"/cso/v1/repository/bitnami/postgresql/manifest/foobar/security",
57+
http.StatusOK,
58+
},
59+
{
60+
"/cso/v1/repository/bitnami/postgresql/image/foobar/security",
61+
http.StatusNotFound,
62+
},
63+
}
64+
m := NewMiddleware(&testAdapter{})
65+
for _, tt := range cases {
66+
t.Run(tt.path, func(t *testing.T) {
67+
w := httptest.NewRecorder()
68+
m.ServeHTTP(w, httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://example.org%s", tt.path), nil))
69+
assert.EqualValues(t, tt.code, w.Code)
70+
})
71+
}
72+
}

0 commit comments

Comments
 (0)