From f310110b6290559d24b9085d956bac9d24217fc4 Mon Sep 17 00:00:00 2001 From: Yousef Alowayed Date: Mon, 3 Feb 2025 15:02:31 -0800 Subject: [PATCH] Add internal enricher interface. PiperOrigin-RevId: 722812226 --- internal/datasource/cache.go | 122 ------- internal/datasource/cache_test.go | 97 ------ internal/datasource/http_auth.go | 317 ----------------- internal/datasource/http_auth_test.go | 327 ------------------ internal/datasource/insights.go | 114 ------ internal/datasource/insights_cache.go | 126 ------- internal/datasource/maven_registry.go | 272 --------------- internal/datasource/maven_registry_cache.go | 63 ---- internal/datasource/maven_registry_test.go | 194 ----------- internal/datasource/maven_settings.go | 141 -------- internal/datasource/maven_settings_test.go | 129 ------- .../testdata/maven_settings/settings.xml | 21 -- internal/mavenutil/maven.go | 197 ----------- internal/mavenutil/maven_test.go | 84 ----- internal/mavenutil/testdata/my-app/pom.xml | 8 - internal/mavenutil/testdata/parent/pom.xml | 8 - internal/mavenutil/testdata/pom.xml | 8 - internal/resolution/client/client.go | 34 -- internal/resolution/client/depsdev_client.go | 66 ---- .../client/maven_registry_client.go | 181 ---------- internal/resolution/client/override_client.go | 93 ----- internal/resolution/clienttest/mock_http.go | 97 ------ .../clienttest/mock_resolution_client.go | 76 ---- 23 files changed, 2775 deletions(-) delete mode 100644 internal/datasource/cache.go delete mode 100644 internal/datasource/cache_test.go delete mode 100644 internal/datasource/http_auth.go delete mode 100644 internal/datasource/http_auth_test.go delete mode 100644 internal/datasource/insights.go delete mode 100644 internal/datasource/insights_cache.go delete mode 100644 internal/datasource/maven_registry.go delete mode 100644 internal/datasource/maven_registry_cache.go delete mode 100644 internal/datasource/maven_registry_test.go delete mode 100644 internal/datasource/maven_settings.go delete mode 100644 internal/datasource/maven_settings_test.go delete mode 100644 internal/datasource/testdata/maven_settings/settings.xml delete mode 100644 internal/mavenutil/maven.go delete mode 100644 internal/mavenutil/maven_test.go delete mode 100644 internal/mavenutil/testdata/my-app/pom.xml delete mode 100644 internal/mavenutil/testdata/parent/pom.xml delete mode 100644 internal/mavenutil/testdata/pom.xml delete mode 100644 internal/resolution/client/client.go delete mode 100644 internal/resolution/client/depsdev_client.go delete mode 100644 internal/resolution/client/maven_registry_client.go delete mode 100644 internal/resolution/client/override_client.go delete mode 100644 internal/resolution/clienttest/mock_http.go delete mode 100644 internal/resolution/clienttest/mock_resolution_client.go diff --git a/internal/datasource/cache.go b/internal/datasource/cache.go deleted file mode 100644 index c39d8eb6..00000000 --- a/internal/datasource/cache.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "bytes" - "encoding/gob" - "maps" - "sync" - "time" -) - -const cacheExpiry = 6 * time.Hour - -func gobMarshal(v any) ([]byte, error) { - var b bytes.Buffer - enc := gob.NewEncoder(&b) - - err := enc.Encode(v) - if err != nil { - return nil, err - } - - return b.Bytes(), nil -} - -func gobUnmarshal(b []byte, v any) error { - dec := gob.NewDecoder(bytes.NewReader(b)) - return dec.Decode(v) -} - -type requestCacheCall[V any] struct { - wg sync.WaitGroup - val V - err error -} - -// RequestCache is a map to cache the results of expensive functions that are called concurrently. -type RequestCache[K comparable, V any] struct { - cache map[K]V - calls map[K]*requestCacheCall[V] - mu sync.Mutex -} - -// NewRequestCache creates a new RequestCache. -func NewRequestCache[K comparable, V any]() *RequestCache[K, V] { - return &RequestCache[K, V]{ - cache: make(map[K]V), - calls: make(map[K]*requestCacheCall[V]), - } -} - -// Get gets the value from the cache map if it's cached, otherwise it will call fn to get the value and cache it. -// fn will only ever be called once for a key, even if there are multiple simultaneous calls to Get before the first call is finished. -func (rq *RequestCache[K, V]) Get(key K, fn func() (V, error)) (V, error) { - // Try get it from regular cache. - rq.mu.Lock() - if v, ok := rq.cache[key]; ok { - rq.mu.Unlock() - return v, nil - } - - // See if there is already a pending request for this key. - if c, ok := rq.calls[key]; ok { - rq.mu.Unlock() - c.wg.Wait() - - return c.val, c.err - } - - // Cache miss - create the call. - c := new(requestCacheCall[V]) - c.wg.Add(1) - rq.calls[key] = c - rq.mu.Unlock() - - c.val, c.err = fn() - rq.mu.Lock() - defer rq.mu.Unlock() - - // Allow other waiting goroutines to return - c.wg.Done() - - // Store value in regular cache. - if c.err == nil { - rq.cache[key] = c.val - } - - // Remove the completed call now that it's cached. - if rq.calls[key] == c { - delete(rq.calls, key) - } - - return c.val, c.err -} - -// GetMap gets a shallow clone of the stored cache map. -func (rq *RequestCache[K, V]) GetMap() map[K]V { - rq.mu.Lock() - defer rq.mu.Unlock() - - return maps.Clone(rq.cache) -} - -// SetMap loads (a shallow clone of) the provided map into the cache map. -func (rq *RequestCache[K, V]) SetMap(m map[K]V) { - rq.mu.Lock() - defer rq.mu.Unlock() - rq.cache = maps.Clone(m) -} diff --git a/internal/datasource/cache_test.go b/internal/datasource/cache_test.go deleted file mode 100644 index fbec8638..00000000 --- a/internal/datasource/cache_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource_test - -import ( - "maps" - "sync" - "sync/atomic" - "testing" - - "github.com/google/osv-scalibr/internal/datasource" -) - -func TestRequestCache(t *testing.T) { - // Test that RequestCache calls each function exactly once per key. - requestCache := datasource.NewRequestCache[int, int]() - - const numKeys = 20 - const requestsPerKey = 50 - - var wg sync.WaitGroup - var fnCalls [numKeys]int32 - - for i := range numKeys { - for range requestsPerKey { - wg.Add(1) - go func() { - t.Helper() - requestCache.Get(i, func() (int, error) { - // Count how many times this function gets called for this key, - // then return the key as the value. - atomic.AddInt32(&fnCalls[i], 1) - return i, nil - }) - wg.Done() - }() - } - } - - wg.Wait() // Make sure all the goroutines are finished - - for i, c := range fnCalls { - if c != 1 { - t.Errorf("RequestCache Get(%d) function called %d times", i, c) - } - } - - cacheMap := requestCache.GetMap() - if len(cacheMap) != numKeys { - t.Errorf("RequestCache GetMap length was %d, expected %d", len(cacheMap), numKeys) - } - - for k, v := range cacheMap { - if k != v { - t.Errorf("RequestCache GetMap key %d has unexpected value %d", k, v) - } - } -} - -func TestRequestCacheSetMap(t *testing.T) { - requestCache := datasource.NewRequestCache[string, string]() - requestCache.SetMap(map[string]string{"foo": "foo1", "bar": "bar2"}) - fn := func() (string, error) { return "CACHE MISS", nil } - - want := map[string]string{ - "foo": "foo1", - "bar": "bar2", - "baz": "CACHE MISS", - "FOO": "CACHE MISS", - } - - for k, v := range want { - got, err := requestCache.Get(k, fn) - if err != nil { - t.Errorf("Get(%v) returned an error: %v", v, err) - } else if got != v { - t.Errorf("Get(%v) got: %v, want %v", k, got, v) - } - } - - gotMap := requestCache.GetMap() - if !maps.Equal(want, gotMap) { - t.Errorf("GetMap() got %v, want %v", gotMap, want) - } -} diff --git a/internal/datasource/http_auth.go b/internal/datasource/http_auth.go deleted file mode 100644 index b3b159b8..00000000 --- a/internal/datasource/http_auth.go +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/hex" - "net/http" - "slices" - "strings" - "sync/atomic" -) - -// HTTPAuthMethod definesthe type of HTTP authentication method. -type HTTPAuthMethod int - -// HTTP authentication method. -const ( - AuthBasic HTTPAuthMethod = iota - AuthBearer - AuthDigest -) - -// HTTPAuthentication holds the information needed for general HTTP Authentication support. -// Requests made through this will automatically populate the relevant info in the Authorization headers. -// This is a general implementation and should be suitable for use with any ecosystem. -type HTTPAuthentication struct { - SupportedMethods []HTTPAuthMethod // In order of preference, only one method will be attempted. - - // AlwaysAuth determines whether to always send auth headers. - // If false, the server must respond with a WWW-Authenticate header which will be checked for supported methods. - // Must be set to false to use Digest authentication. - AlwaysAuth bool - - // Shared - Username string // Basic & Digest, plain text. - Password string // Basic & Digest, plain text. - // Basic - BasicAuth string // Base64-encoded username:password. Overrides Username & Password fields if set. - // Bearer - BearerToken string - // Digest - CnonceFunc func() string // Function used to generate cnonce string for Digest. OK to leave unassigned. Mostly for use in tests. - - lastUsed atomic.Value // The last-used authentication method - used when AlwaysAuth is false to automatically send Basic auth. -} - -// Get makes an http GET request with the given http.Client. -// The Authorization Header will automatically be populated according from the fields in the HTTPAuthentication. -func (auth *HTTPAuthentication) Get(ctx context.Context, httpClient *http.Client, url string) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - - // For convenience, have the nil HTTPAuthentication just make an unauthenticated request. - if auth == nil { - return httpClient.Do(req) - } - - if auth.AlwaysAuth { - for _, method := range auth.SupportedMethods { - ok := false - switch method { - case AuthBasic: - ok = auth.addBasic(req) - case AuthBearer: - ok = auth.addBearer(req) - case AuthDigest: - // AuthDigest needs a challenge from WWW-Authenticate, so we cannot always add the auth. - } - if ok { - break - } - } - - return httpClient.Do(req) - } - - // If the last request we made to this server used Basic or Bearer auth, send the header with this request - if lastUsed, ok := auth.lastUsed.Load().(HTTPAuthMethod); ok { - switch lastUsed { - case AuthBasic: - auth.addBasic(req) - case AuthBearer: - auth.addBearer(req) - case AuthDigest: - // Cannot add AuthDigest without the challenge from the initial request. - } - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusUnauthorized { - return resp, nil - } - - wwwAuth := resp.Header.Values("WWW-Authenticate") - - ok := false - var usedMethod HTTPAuthMethod - req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - for _, method := range auth.SupportedMethods { - switch method { - case AuthBasic: - if auth.authIndex(wwwAuth, "Basic") >= 0 { - ok = auth.addBasic(req) - } - case AuthBearer: - if auth.authIndex(wwwAuth, "Bearer") >= 0 { - ok = auth.addBearer(req) - } - case AuthDigest: - if idx := auth.authIndex(wwwAuth, "Digest"); idx >= 0 { - ok = auth.addDigest(req, wwwAuth[idx]) - } - } - if ok { - usedMethod = method - break - } - } - - if ok { - defer resp.Body.Close() // Close the original request before we discard it. - resp, err = httpClient.Do(req) - } - if resp.StatusCode == http.StatusOK { - auth.lastUsed.Store(usedMethod) - } - // The original request's response will be returned if there is no matching methods. - return resp, err -} - -func (auth *HTTPAuthentication) authIndex(wwwAuth []string, authScheme string) int { - return slices.IndexFunc(wwwAuth, func(s string) bool { - scheme, _, _ := strings.Cut(s, " ") - return scheme == authScheme - }) -} - -func (auth *HTTPAuthentication) addBasic(req *http.Request) bool { - if auth.BasicAuth != "" { - req.Header.Set("Authorization", "Basic "+auth.BasicAuth) - - return true - } - - if auth.Username != "" && auth.Password != "" { - authStr := base64.StdEncoding.EncodeToString([]byte(auth.Username + ":" + auth.Password)) - req.Header.Set("Authorization", "Basic "+authStr) - - return true - } - - return false -} - -func (auth *HTTPAuthentication) addBearer(req *http.Request) bool { - if auth.BearerToken != "" { - req.Header.Set("Authorization", "Bearer "+auth.BearerToken) - - return true - } - - return false -} - -func (auth *HTTPAuthentication) addDigest(req *http.Request, challenge string) bool { - // The original implementation of this function depends on crypto/md5, which - // is not allowed internally so comment out the implementation for now. - return false - // Mostly following the algorithm as outlined in https://en.wikipedia.org/wiki/Digest_access_authentication - // And also https://datatracker.ietf.org/doc/html/rfc2617 - /* - if auth.Username == "" || auth.Password == "" { - return false - } - params := auth.parseChallenge(challenge) - realm, ok := params["realm"] - if !ok { - return false - } - - nonce, ok := params["nonce"] - if !ok { - return false - } - var cnonce string - - ha1 := md5.Sum([]byte(auth.Username + ":" + realm + ":" + auth.Password)) //nolint:gosec - switch params["algorithm"] { - case "MD5-sess": - cnonce = auth.cnonce() - if cnonce == "" { - return false - } - var b bytes.Buffer - fmt.Fprintf(&b, "%x:%s:%s", ha1, nonce, cnonce) - ha1 = md5.Sum(b.Bytes()) //nolint:gosec - case "MD5": - case "": - default: - return false - } - - // Only support "auth" qop - if qop, ok := params["qop"]; ok && !slices.Contains(strings.Split(qop, ","), "auth") { - return false - } - - uri := req.URL.Path // is this sufficient? - - ha2 := md5.Sum([]byte(req.Method + ":" + uri)) //nolint:gosec - - // hard-coding nonceCount to 1 since we don't make a request more than once - nonceCount := "00000001" - - var b bytes.Buffer - if _, ok := params["qop"]; ok { - if cnonce == "" { - cnonce = auth.cnonce() - if cnonce == "" { - return false - } - } - fmt.Fprintf(&b, "%x:%s:%s:%s:%s:%x", ha1, nonce, nonceCount, cnonce, "auth", ha2) - } else { - fmt.Fprintf(&b, "%x:%s:%x", ha1, nonce, ha2) - } - response := md5.Sum(b.Bytes()) //nolint:gosec - - var sb strings.Builder - fmt.Fprintf(&sb, "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\"", - auth.Username, realm, nonce, uri) - if _, ok := params["qop"]; ok { - fmt.Fprintf(&sb, ", qop=auth, nc=%s, cnonce=\"%s\"", nonceCount, cnonce) - } - if alg, ok := params["algorithm"]; ok { - fmt.Fprintf(&sb, ", algorithm=%s", alg) - } - fmt.Fprintf(&sb, ", response=\"%x\", opaque=\"%s\"", response, params["opaque"]) - - req.Header.Add("Authorization", sb.String()) - - return true - */ -} - -func (auth *HTTPAuthentication) parseChallenge(challenge string) map[string]string { - // Parse the params out of the auth challenge header. - // e.g. Digest realm="testrealm@host.com", qop="auth,auth-int" -> - // {"realm": "testrealm@host.com", "qop", "auth,auth-int"} - // - // This isn't perfectly robust - some edge cases / weird headers may parse incorrectly. - - // Get rid of "Digest" prefix - _, challenge, _ = strings.Cut(challenge, " ") - - parts := strings.Split(challenge, ",") - // parts may have had a quoted comma, recombine if there's an unclosed quote. - - for i := 0; i < len(parts); { - if strings.Count(parts[i], "\"")%2 == 1 && len(parts) > i+1 { - parts[i] = parts[i] + "," + parts[i+1] - parts = append(parts[:i+1], parts[i+2:]...) - - continue - } - i++ - } - - m := make(map[string]string) - for _, part := range parts { - key, val, _ := strings.Cut(part, "=") - key = strings.Trim(key, " ") - val = strings.Trim(val, " ") - // remove quotes from quoted string - val = strings.Trim(val, "\"") - m[key] = val - } - - return m -} - -func (auth *HTTPAuthentication) cnonce() string { - if auth.CnonceFunc != nil { - return auth.CnonceFunc() - } - - // for a default nonce use a random 8 bytes - b := make([]byte, 8) - if _, err := rand.Read(b); err != nil { - return "" - } - - return hex.EncodeToString(b) -} diff --git a/internal/datasource/http_auth_test.go b/internal/datasource/http_auth_test.go deleted file mode 100644 index e7416420..00000000 --- a/internal/datasource/http_auth_test.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource_test - -import ( - "context" - "net/http" - "testing" - - "github.com/google/osv-scalibr/internal/datasource" -) - -// mockTransport is used to inspect the requests being made by HTTPAuthentications -type mockTransport struct { - Requests []*http.Request // All requests made to this transport - UnauthedResponse *http.Response // Response sent when request does not have an 'Authorization' header. - AuthedReponse *http.Response // Response to sent when request does include 'Authorization' (not checked). -} - -func (mt *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - mt.Requests = append(mt.Requests, req) - var resp *http.Response - if req.Header.Get("Authorization") == "" { - resp = mt.UnauthedResponse - } else { - resp = mt.AuthedReponse - } - if resp == nil { - resp = &http.Response{StatusCode: http.StatusOK} - } - - return resp, nil -} - -func TestHTTPAuthentication(t *testing.T) { - tests := []struct { - name string - httpAuth *datasource.HTTPAuthentication - requestURL string - wwwAuth []string - expectedAuths []string // expected Authentication headers received. - expectedResponseCodes []int // expected final response codes received (length may be less than expectedAuths) - }{ - { - name: "nil auth", - httpAuth: nil, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Basic"}, - expectedAuths: []string{""}, - expectedResponseCodes: []int{http.StatusUnauthorized}, - }, - { - name: "default auth", - httpAuth: &datasource.HTTPAuthentication{}, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Basic"}, - expectedAuths: []string{""}, - expectedResponseCodes: []int{http.StatusUnauthorized}, - }, - { - name: "basic auth", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, - AlwaysAuth: true, - Username: "Aladdin", - Password: "open sesame", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "basic auth from token", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, - AlwaysAuth: true, - Username: "ignored", - Password: "ignored", - BasicAuth: "QWxhZGRpbjpvcGVuIHNlc2FtZQ==", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "basic auth missing username", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, - AlwaysAuth: true, - Username: "", - Password: "ignored", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{""}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "basic auth missing password", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, - AlwaysAuth: true, - Username: "ignored", - Password: "", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{""}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "basic auth not always", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic}, - AlwaysAuth: false, - BasicAuth: "YTph", - }, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Basic realm=\"User Visible Realm\""}, - expectedAuths: []string{"", "Basic YTph"}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "bearer auth", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer}, - AlwaysAuth: true, - BearerToken: "abcdefgh", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{"Bearer abcdefgh"}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "bearer auth not always", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer}, - AlwaysAuth: false, - BearerToken: "abcdefgh", - }, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Bearer"}, - expectedAuths: []string{"", "Bearer abcdefgh"}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "always auth priority", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBasic, datasource.AuthBearer}, - AlwaysAuth: true, - BasicAuth: "UseThisOne", - BearerToken: "NotThisOne", - }, - requestURL: "http://127.0.0.1/", - expectedAuths: []string{"Basic UseThisOne"}, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "not always auth priority", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthBearer, datasource.AuthDigest, datasource.AuthBasic}, - AlwaysAuth: false, - Username: "DoNotUse", - Password: "ThisField", - BearerToken: "PleaseUseThis", - }, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Basic", "Bearer"}, - expectedAuths: []string{"", "Bearer PleaseUseThis"}, - expectedResponseCodes: []int{http.StatusOK}, - }, - // Digest authentication is not supported for now so temperatily comment out the tests. - /* - { - name: "digest auth", - // Example from https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, - AlwaysAuth: false, - Username: "Mufasa", - Password: "Circle Of Life", - CnonceFunc: func() string { return "0a4f113b" }, - }, - requestURL: "https://127.0.0.1/dir/index.html", - wwwAuth: []string{ - "Digest realm=\"testrealm@host.com\", " + - "qop=\"auth,auth-int\", " + - "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + - "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", - }, - expectedAuths: []string{ - "", - // The order of these fields shouldn't actually matter - "Digest username=\"Mufasa\", " + - "realm=\"testrealm@host.com\", " + - "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + - "uri=\"/dir/index.html\", " + - "qop=auth, " + - "nc=00000001, " + - "cnonce=\"0a4f113b\", " + - "response=\"6629fae49393a05397450978507c4ef1\", " + - "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", - }, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "digest auth rfc2069", // old spec, without qop header - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, - AlwaysAuth: false, - Username: "Mufasa", - Password: "Circle Of Life", - }, - requestURL: "https://127.0.0.1/dir/index.html", - wwwAuth: []string{ - "Digest realm=\"testrealm@host.com\", " + - "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + - "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", - }, - expectedAuths: []string{ - "", - // The order of these fields shouldn't actually matter - "Digest username=\"Mufasa\", " + - "realm=\"testrealm@host.com\", " + - "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + - "uri=\"/dir/index.html\", " + - "response=\"670fd8c2df070c60b045671b8b24ff02\", " + - "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", - }, - expectedResponseCodes: []int{http.StatusOK}, - }, - { - name: "digest auth mvn", - // From what mvn sends. - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest}, - AlwaysAuth: false, - Username: "my-username", - Password: "cool-password", - CnonceFunc: func() string { return "f7ef2d457dabcd54" }, - }, - requestURL: "https://127.0.0.1:41565/commons-io/commons-io/1.0/commons-io-1.0.pom", - wwwAuth: []string{ - "Digest realm=\"test@osv.dev\"," + - "qop=\"auth\"," + - "nonce=\"deadbeef\"," + - "opaque=\"aaaa\"," + - "algorithm=\"MD5-sess\"," + - "domain=\"/test\"", - }, - expectedAuths: []string{ - "", - // The order of these fields shouldn't actually matter - "Digest username=\"my-username\", " + - "realm=\"test@osv.dev\", " + - "nonce=\"deadbeef\", " + - "uri=\"/commons-io/commons-io/1.0/commons-io-1.0.pom\", " + - "qop=auth, " + - "nc=00000001, " + - "cnonce=\"f7ef2d457dabcd54\", " + - "algorithm=MD5-sess, " + - "response=\"15a35e7018a0fc7db05d31185e0d2c9e\", " + - "opaque=\"aaaa\"", - }, - expectedResponseCodes: []int{http.StatusOK}, - }, - */ - { - name: "basic auth reuse on subsequent", - httpAuth: &datasource.HTTPAuthentication{ - SupportedMethods: []datasource.HTTPAuthMethod{datasource.AuthDigest, datasource.AuthBasic}, - AlwaysAuth: false, - Username: "user", - Password: "pass", - }, - requestURL: "http://127.0.0.1/", - wwwAuth: []string{"Basic realm=\"Realm\""}, - expectedAuths: []string{"", "Basic dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"}, - expectedResponseCodes: []int{http.StatusOK, http.StatusOK}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mt := &mockTransport{} - if len(tt.wwwAuth) > 0 { - mt.UnauthedResponse = &http.Response{ - StatusCode: http.StatusUnauthorized, - Header: make(http.Header), - } - for _, v := range tt.wwwAuth { - mt.UnauthedResponse.Header.Add("WWW-Authenticate", v) - } - } - httpClient := &http.Client{Transport: mt} - for _, want := range tt.expectedResponseCodes { - resp, err := tt.httpAuth.Get(context.Background(), httpClient, tt.requestURL) - if err != nil { - t.Fatalf("error making request: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != want { - t.Errorf("authorization response status code got = %d, want %d", resp.StatusCode, want) - } - } - if len(mt.Requests) != len(tt.expectedAuths) { - t.Fatalf("unexpected number of requests got = %d, want %d", len(mt.Requests), len(tt.expectedAuths)) - } - for i, want := range tt.expectedAuths { - got := mt.Requests[i].Header.Get("Authorization") - if got != want { - t.Errorf("authorization header got = \"%s\", want \"%s\"", got, want) - } - } - }) - } -} diff --git a/internal/datasource/insights.go b/internal/datasource/insights.go deleted file mode 100644 index 2359dab2..00000000 --- a/internal/datasource/insights.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource provides clients to fetch data from different APIs. -package datasource - -import ( - "context" - "crypto/x509" - "fmt" - "sync" - "time" - - pb "deps.dev/api/v3" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" -) - -// CachedInsightsClient is a wrapper for InsightsClient that caches requests. -type CachedInsightsClient struct { - pb.InsightsClient - - // cache fields - mu sync.Mutex - cacheTimestamp *time.Time - packageCache *RequestCache[packageKey, *pb.Package] - versionCache *RequestCache[versionKey, *pb.Version] - requirementsCache *RequestCache[versionKey, *pb.Requirements] -} - -// Comparable types to use as map keys for cache. -type packageKey struct { - System pb.System - Name string -} - -func makePackageKey(k *pb.PackageKey) packageKey { - return packageKey{ - System: k.GetSystem(), - Name: k.GetName(), - } -} - -type versionKey struct { - System pb.System - Name string - Version string -} - -func makeVersionKey(k *pb.VersionKey) versionKey { - return versionKey{ - System: k.GetSystem(), - Name: k.GetName(), - Version: k.GetVersion(), - } -} - -// NewCachedInsightsClient creates a CachedInsightsClient. -func NewCachedInsightsClient(addr string, userAgent string) (*CachedInsightsClient, error) { - certPool, err := x509.SystemCertPool() - if err != nil { - return nil, fmt.Errorf("getting system cert pool: %w", err) - } - creds := credentials.NewClientTLSFromCert(certPool, "") - dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} - - if userAgent != "" { - dialOpts = append(dialOpts, grpc.WithUserAgent(userAgent)) - } - - conn, err := grpc.NewClient(addr, dialOpts...) - if err != nil { - return nil, fmt.Errorf("dialling %q: %w", addr, err) - } - - return &CachedInsightsClient{ - InsightsClient: pb.NewInsightsClient(conn), - packageCache: NewRequestCache[packageKey, *pb.Package](), - versionCache: NewRequestCache[versionKey, *pb.Version](), - requirementsCache: NewRequestCache[versionKey, *pb.Requirements](), - }, nil -} - -// GetPackage returns metadata about a package by querying deps.dev API. -func (c *CachedInsightsClient) GetPackage(ctx context.Context, in *pb.GetPackageRequest, opts ...grpc.CallOption) (*pb.Package, error) { - return c.packageCache.Get(makePackageKey(in.GetPackageKey()), func() (*pb.Package, error) { - return c.InsightsClient.GetPackage(ctx, in, opts...) - }) -} - -// GetVersion returns metadata about a version by querying deps.dev API. -func (c *CachedInsightsClient) GetVersion(ctx context.Context, in *pb.GetVersionRequest, opts ...grpc.CallOption) (*pb.Version, error) { - return c.versionCache.Get(makeVersionKey(in.GetVersionKey()), func() (*pb.Version, error) { - return c.InsightsClient.GetVersion(ctx, in, opts...) - }) -} - -// GetRequirements returns requirements of the given version by querying deps.dev API. -func (c *CachedInsightsClient) GetRequirements(ctx context.Context, in *pb.GetRequirementsRequest, opts ...grpc.CallOption) (*pb.Requirements, error) { - return c.requirementsCache.Get(makeVersionKey(in.GetVersionKey()), func() (*pb.Requirements, error) { - return c.InsightsClient.GetRequirements(ctx, in, opts...) - }) -} diff --git a/internal/datasource/insights_cache.go b/internal/datasource/insights_cache.go deleted file mode 100644 index 3bc165f8..00000000 --- a/internal/datasource/insights_cache.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "time" - - pb "deps.dev/api/v3" - "google.golang.org/protobuf/proto" -) - -type depsdevAPICache struct { - Timestamp *time.Time - PackageCache map[packageKey][]byte - VersionCache map[versionKey][]byte - RequirementsCache map[versionKey][]byte -} - -func protoMarshalCache[K comparable, V proto.Message](protoMap map[K]V) (map[K][]byte, error) { - byteMap := make(map[K][]byte) - for k, v := range protoMap { - b, err := proto.Marshal(v) - if err != nil { - return nil, err - } - byteMap[k] = b - } - - return byteMap, nil -} - -func protoUnmarshalCache[K comparable, V any, PV interface { - proto.Message - *V -}](byteMap map[K][]byte, protoMap *map[K]PV) error { - *protoMap = make(map[K]PV) - for k, b := range byteMap { - v := PV(new(V)) - if err := proto.Unmarshal(b, v); err != nil { - return err - } - (*protoMap)[k] = v - } - - return nil -} - -// GobEncode encodes cache to bytes. -func (c *CachedInsightsClient) GobEncode() ([]byte, error) { - var cache depsdevAPICache - c.mu.Lock() - defer c.mu.Unlock() - - if c.cacheTimestamp == nil { - now := time.Now().UTC() - c.cacheTimestamp = &now - } - - cache.Timestamp = c.cacheTimestamp - var err error - cache.PackageCache, err = protoMarshalCache(c.packageCache.GetMap()) - if err != nil { - return nil, err - } - cache.VersionCache, err = protoMarshalCache(c.versionCache.GetMap()) - if err != nil { - return nil, err - } - cache.RequirementsCache, err = protoMarshalCache(c.requirementsCache.GetMap()) - if err != nil { - return nil, err - } - - return gobMarshal(cache) -} - -// GobDecode decodes bytes to cache. -func (c *CachedInsightsClient) GobDecode(b []byte) error { - var cache depsdevAPICache - if err := gobUnmarshal(b, &cache); err != nil { - return err - } - - if cache.Timestamp != nil && time.Since(*cache.Timestamp) >= cacheExpiry { - // Cache expired - return nil - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.cacheTimestamp = cache.Timestamp - - var pkgMap map[packageKey]*pb.Package - if err := protoUnmarshalCache(cache.PackageCache, &pkgMap); err != nil { - return err - } - - var verMap map[versionKey]*pb.Version - if err := protoUnmarshalCache(cache.VersionCache, &verMap); err != nil { - return err - } - - var reqMap map[versionKey]*pb.Requirements - if err := protoUnmarshalCache(cache.RequirementsCache, &reqMap); err != nil { - return err - } - - c.packageCache.SetMap(pkgMap) - c.versionCache.SetMap(verMap) - c.requirementsCache.SetMap(reqMap) - - return nil -} diff --git a/internal/datasource/maven_registry.go b/internal/datasource/maven_registry.go deleted file mode 100644 index 22467d47..00000000 --- a/internal/datasource/maven_registry.go +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "bytes" - "context" - "encoding/xml" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "slices" - "strings" - "sync" - "time" - - "deps.dev/util/maven" - "deps.dev/util/semver" - "golang.org/x/net/html/charset" -) - -// MavenCentral holds the URL of Maven Central Repository. -const MavenCentral = "https://repo.maven.apache.org/maven2" - -var errAPIFailed = errors.New("API query failed") - -// MavenRegistryAPIClient defines a client to fetch metadata from a Maven registry. -type MavenRegistryAPIClient struct { - defaultRegistry MavenRegistry // The default registry that we are making requests - registries []MavenRegistry // Additional registries specified to fetch projects - registryAuths map[string]*HTTPAuthentication // Authentication for the registries keyed by registry ID. From settings.xml - - // Cache fields - mu *sync.Mutex - cacheTimestamp *time.Time // If set, this means we loaded from a cache - responses *RequestCache[string, response] -} - -type response struct { - StatusCode int - Body []byte -} - -// MavenRegistry defines a Maven registry. -type MavenRegistry struct { - URL string - Parsed *url.URL - - // Information from pom.xml - ID string - ReleasesEnabled bool - SnapshotsEnabled bool -} - -// NewMavenRegistryAPIClient returns a new MavenRegistryAPIClient. -func NewMavenRegistryAPIClient(registry MavenRegistry) (*MavenRegistryAPIClient, error) { - if registry.URL == "" { - registry.URL = MavenCentral - registry.ID = "central" - } - u, err := url.Parse(registry.URL) - if err != nil { - return nil, fmt.Errorf("invalid Maven registry %s: %w", registry.URL, err) - } - registry.Parsed = u - - // TODO: allow for manual specification of settings files - globalSettings := ParseMavenSettings(globalMavenSettingsFile()) - userSettings := ParseMavenSettings(userMavenSettingsFile()) - - return &MavenRegistryAPIClient{ - // We assume only downloading releases is allowed on the default registry. - defaultRegistry: registry, - mu: &sync.Mutex{}, - responses: NewRequestCache[string, response](), - registryAuths: MakeMavenAuth(globalSettings, userSettings), - }, nil -} - -// WithoutRegistries makes MavenRegistryAPIClient including its cache but not registries. -func (m *MavenRegistryAPIClient) WithoutRegistries() *MavenRegistryAPIClient { - return &MavenRegistryAPIClient{ - defaultRegistry: m.defaultRegistry, - mu: m.mu, - cacheTimestamp: m.cacheTimestamp, - responses: m.responses, - } -} - -// AddRegistry adds the given registry to the list of registries if it has not been added. -func (m *MavenRegistryAPIClient) AddRegistry(registry MavenRegistry) error { - for _, reg := range m.registries { - if reg.ID == registry.ID { - return nil - } - } - - u, err := url.Parse(registry.URL) - if err != nil { - return err - } - - registry.Parsed = u - m.registries = append(m.registries, registry) - - return nil -} - -// GetRegistries returns the registries added to this client. -func (m *MavenRegistryAPIClient) GetRegistries() (registries []MavenRegistry) { - return m.registries -} - -// GetProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project. -// Each registry in the list is tried until we find the project. -// For a snapshot version, version level metadata is used to find the extact version string. -// More about Maven Repository Metadata Model: https://maven.apache.org/ref/3.9.9/maven-repository-metadata/ -// More about Maven Metadata: https://maven.apache.org/repositories/metadata.html -func (m *MavenRegistryAPIClient) GetProject(ctx context.Context, groupID, artifactID, version string) (maven.Project, error) { - if !strings.HasSuffix(version, "-SNAPSHOT") { - for _, registry := range append(m.registries, m.defaultRegistry) { - if !registry.ReleasesEnabled { - continue - } - project, err := m.getProject(ctx, registry, groupID, artifactID, version, "") - if err == nil { - return project, nil - } - } - - return maven.Project{}, fmt.Errorf("failed to fetch Maven project %s:%s@%s", groupID, artifactID, version) - } - - for _, registry := range append(m.registries, m.defaultRegistry) { - // Fetch version metadata for snapshot versions from the registries enabling that. - if !registry.SnapshotsEnabled { - continue - } - metadata, err := m.getVersionMetadata(ctx, registry, groupID, artifactID, version) - if err != nil { - continue - } - - snapshot := "" - for _, sv := range metadata.Versioning.SnapshotVersions { - if sv.Extension == "pom" { - // We only look for pom.xml for project metadata. - snapshot = string(sv.Value) - break - } - } - - project, err := m.getProject(ctx, registry, groupID, artifactID, version, snapshot) - if err == nil { - return project, nil - } - } - - return maven.Project{}, fmt.Errorf("failed to fetch Maven project %s:%s@%s", groupID, artifactID, version) -} - -// GetVersions returns the list of available versions of a Maven package specified by groupID and artifactID. -// Versions found in all registries are unioned, then sorted by semver. -func (m *MavenRegistryAPIClient) GetVersions(ctx context.Context, groupID, artifactID string) ([]maven.String, error) { - var versions []maven.String - for _, registry := range append(m.registries, m.defaultRegistry) { - metadata, err := m.getArtifactMetadata(ctx, registry, groupID, artifactID) - if err != nil { - continue - } - versions = append(versions, metadata.Versioning.Versions...) - } - slices.SortFunc(versions, func(a, b maven.String) int { return semver.Maven.Compare(string(a), string(b)) }) - - return slices.Compact(versions), nil -} - -// getProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project. -// For snapshot versions, the exact version value is specified by snapshot. -func (m *MavenRegistryAPIClient) getProject(ctx context.Context, registry MavenRegistry, groupID, artifactID, version, snapshot string) (maven.Project, error) { - if snapshot == "" { - snapshot = version - } - u := registry.Parsed.JoinPath(strings.ReplaceAll(groupID, ".", "/"), artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, snapshot)).String() - - var project maven.Project - if err := m.get(ctx, m.registryAuths[registry.ID], u, &project); err != nil { - return maven.Project{}, err - } - - return project, nil -} - -// getVersionMetadata fetches a version level maven-metadata.xml and parses it to maven.Metadata. -func (m *MavenRegistryAPIClient) getVersionMetadata(ctx context.Context, registry MavenRegistry, groupID, artifactID, version string) (maven.Metadata, error) { - u := registry.Parsed.JoinPath(strings.ReplaceAll(groupID, ".", "/"), artifactID, version, "maven-metadata.xml").String() - - var metadata maven.Metadata - if err := m.get(ctx, m.registryAuths[registry.ID], u, &metadata); err != nil { - return maven.Metadata{}, err - } - - return metadata, nil -} - -// GetArtifactMetadata fetches an artifact level maven-metadata.xml and parses it to maven.Metadata. -func (m *MavenRegistryAPIClient) getArtifactMetadata(ctx context.Context, registry MavenRegistry, groupID, artifactID string) (maven.Metadata, error) { - u := registry.Parsed.JoinPath(strings.ReplaceAll(groupID, ".", "/"), artifactID, "maven-metadata.xml").String() - - var metadata maven.Metadata - if err := m.get(ctx, m.registryAuths[registry.ID], u, &metadata); err != nil { - return maven.Metadata{}, err - } - - return metadata, nil -} - -func (m *MavenRegistryAPIClient) get(ctx context.Context, auth *HTTPAuthentication, url string, dst any) error { - resp, err := m.responses.Get(url, func() (response, error) { - resp, err := auth.Get(ctx, http.DefaultClient, url) - if err != nil { - return response{}, fmt.Errorf("%w: Maven registry query failed: %w", errAPIFailed, err) - } - defer resp.Body.Close() - - if !slices.Contains([]int{http.StatusOK, http.StatusNotFound, http.StatusUnauthorized}, resp.StatusCode) { - // Only cache responses with Status OK, NotFound, or Unauthorized - return response{}, fmt.Errorf("%w: Maven registry query status: %d", errAPIFailed, resp.StatusCode) - } - - if b, err := io.ReadAll(resp.Body); err == nil { - return response{StatusCode: resp.StatusCode, Body: b}, nil - } - - return response{}, fmt.Errorf("failed to read body: %w", err) - }) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("%w: Maven registry query status: %d", errAPIFailed, resp.StatusCode) - } - - return NewMavenDecoder(bytes.NewReader(resp.Body)).Decode(dst) -} - -// NewMavenDecoder returns an xml decoder with CharsetReader and Entity set. -func NewMavenDecoder(reader io.Reader) *xml.Decoder { - decoder := xml.NewDecoder(reader) - // Set charset reader for conversion from non-UTF-8 charset into UTF-8. - decoder.CharsetReader = charset.NewReaderLabel - // Set HTML entity map for translation between non-standard entity names - // and string replacements. - decoder.Entity = xml.HTMLEntity - - return decoder -} diff --git a/internal/datasource/maven_registry_cache.go b/internal/datasource/maven_registry_cache.go deleted file mode 100644 index d168653e..00000000 --- a/internal/datasource/maven_registry_cache.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "time" -) - -type mavenRegistryCache struct { - Timestamp *time.Time - Responses map[string]response // url -> response -} - -// GobEncode encodes cache to bytes. -func (m *MavenRegistryAPIClient) GobEncode() ([]byte, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.cacheTimestamp == nil { - now := time.Now().UTC() - m.cacheTimestamp = &now - } - - cache := mavenRegistryCache{ - Timestamp: m.cacheTimestamp, - Responses: m.responses.GetMap(), - } - - return gobMarshal(&cache) -} - -// GobDecode encodes bytes to cache. -func (m *MavenRegistryAPIClient) GobDecode(b []byte) error { - var cache mavenRegistryCache - if err := gobUnmarshal(b, &cache); err != nil { - return err - } - - if cache.Timestamp != nil && time.Since(*cache.Timestamp) >= cacheExpiry { - // Cache expired - return nil - } - - m.mu.Lock() - defer m.mu.Unlock() - - m.cacheTimestamp = cache.Timestamp - m.responses.SetMap(cache.Responses) - - return nil -} diff --git a/internal/datasource/maven_registry_test.go b/internal/datasource/maven_registry_test.go deleted file mode 100644 index de03cd4a..00000000 --- a/internal/datasource/maven_registry_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource_test - -import ( - "context" - "reflect" - "testing" - - "deps.dev/util/maven" - "github.com/google/osv-scalibr/internal/datasource" - "github.com/google/osv-scalibr/internal/resolution/clienttest" -) - -func TestGetProject(t *testing.T) { - srv := clienttest.NewMockHTTPServer(t) - client, _ := datasource.NewMavenRegistryAPIClient(datasource.MavenRegistry{URL: srv.URL, ReleasesEnabled: true}) - srv.SetResponse(t, "org/example/x.y.z/1.0.0/x.y.z-1.0.0.pom", []byte(` - - org.example - x.y.z - 1.0.0 - - `)) - - got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "1.0.0") - if err != nil { - t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "1.0.0", err) - } - want := maven.Project{ - ProjectKey: maven.ProjectKey{ - GroupID: "org.example", - ArtifactID: "x.y.z", - Version: "1.0.0", - }, - } - if !reflect.DeepEqual(got, want) { - t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "1.0.0", got, want) - } -} - -func TestGetProjectSnapshot(t *testing.T) { - srv := clienttest.NewMockHTTPServer(t) - client, _ := datasource.NewMavenRegistryAPIClient(datasource.MavenRegistry{URL: srv.URL, SnapshotsEnabled: true}) - srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/maven-metadata.xml", []byte(` - - org.example - x.y.z - - - 20230302.052731 - 9 - - 20230302052731 - - - jar - 3.3.1-20230302.052731-9 - 20230302052731 - - - pom - 3.3.1-20230302.052731-9 - 20230302052731 - - - - - `)) - srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/x.y.z-3.3.1-20230302.052731-9.pom", []byte(` - - org.example - x.y.z - 3.3.1-SNAPSHOT - - `)) - - got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "3.3.1-SNAPSHOT") - if err != nil { - t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "3.3.1-SNAPSHOT", err) - } - want := maven.Project{ - ProjectKey: maven.ProjectKey{ - GroupID: "org.example", - ArtifactID: "x.y.z", - Version: "3.3.1-SNAPSHOT", - }, - } - if !reflect.DeepEqual(got, want) { - t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "3.3.1-SNAPSHOT", got, want) - } -} - -func TestMultipleRegistry(t *testing.T) { - dft := clienttest.NewMockHTTPServer(t) - client, _ := datasource.NewMavenRegistryAPIClient(datasource.MavenRegistry{URL: dft.URL, ReleasesEnabled: true}) - dft.SetResponse(t, "org/example/x.y.z/maven-metadata.xml", []byte(` - - org.example - x.y.z - - 3.0.0 - 3.0.0 - - 2.0.0 - 3.0.0 - - - - `)) - dft.SetResponse(t, "org/example/x.y.z/2.0.0/x.y.z-2.0.0.pom", []byte(` - - org.example - x.y.z - 2.0.0 - - `)) - dft.SetResponse(t, "org/example/x.y.z/3.0.0/x.y.z-3.0.0.pom", []byte(` - - org.example - x.y.z - 3.0.0 - - `)) - - srv := clienttest.NewMockHTTPServer(t) - if err := client.AddRegistry(datasource.MavenRegistry{URL: srv.URL, ReleasesEnabled: true}); err != nil { - t.Fatalf("failed to add registry %s: %v", srv.URL, err) - } - srv.SetResponse(t, "org/example/x.y.z/maven-metadata.xml", []byte(` - - org.example - x.y.z - - 2.0.0 - 2.0.0 - - 1.0.0 - 2.0.0 - - - - `)) - srv.SetResponse(t, "org/example/x.y.z/1.0.0/x.y.z-1.0.0.pom", []byte(` - - org.example - x.y.z - 1.0.0 - - `)) - srv.SetResponse(t, "org/example/x.y.z/2.0.0/x.y.z-2.0.0.pom", []byte(` - - org.example - x.y.z - 2.0.0 - - `)) - - gotProj, err := client.GetProject(context.Background(), "org.example", "x.y.z", "1.0.0") - if err != nil { - t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "1.0.0", err) - } - wantProj := maven.Project{ - ProjectKey: maven.ProjectKey{ - GroupID: "org.example", - ArtifactID: "x.y.z", - Version: "1.0.0", - }, - } - if !reflect.DeepEqual(gotProj, wantProj) { - t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "1.0.0", gotProj, wantProj) - } - - gotVersions, err := client.GetVersions(context.Background(), "org.example", "x.y.z") - if err != nil { - t.Fatalf("failed to get versions for Maven package %s:%s: %v", "org.example", "x.y.z", err) - } - wantVersions := []maven.String{"1.0.0", "2.0.0", "3.0.0"} - if !reflect.DeepEqual(gotVersions, wantVersions) { - t.Errorf("GetVersions(%s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", gotVersions, wantVersions) - } -} diff --git a/internal/datasource/maven_settings.go b/internal/datasource/maven_settings.go deleted file mode 100644 index 99fdc161..00000000 --- a/internal/datasource/maven_settings.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource - -import ( - "encoding/xml" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "strings" - "unicode" -) - -// Maven settings.xml file parsing for registry authentication. -// https://maven.apache.org/settings.html - -// MavenSettingsXML defines Maven settings.xml. -type MavenSettingsXML struct { - Servers []MavenSettingsXMLServer `xml:"servers>server"` -} - -// MavenSettingsXMLServer defines a Maven server in settings.xml. -type MavenSettingsXMLServer struct { - ID string `xml:"id"` - Username string `xml:"username"` - Password string `xml:"password"` -} - -// ParseMavenSettings parses Maven settings at the given path. -func ParseMavenSettings(path string) MavenSettingsXML { - f, err := os.Open(path) - if err != nil { - return MavenSettingsXML{} - } - defer f.Close() - - var settings MavenSettingsXML - if err := xml.NewDecoder(f).Decode(&settings); err != nil { - return MavenSettingsXML{} - } - - // interpolate strings with environment variables only - // system properties are too hard to determine. - re := regexp.MustCompile(`\${env\.[^}]*}`) - replFn := func(match string) string { - // grab just the environment variable string - env := match[len("${env.") : len(match)-1] - - // Environment variables on Windows are case-insensitive, - // but Maven will only replace them if they are in all-caps. - if runtime.GOOS == "windows" && strings.ContainsFunc(env, unicode.IsLower) { - return match // No replacement. - } - - if val, ok := os.LookupEnv(env); ok { - return val - } - - // Don't do any replacement if the environment variable isn't set - return match - } - for i := range settings.Servers { - settings.Servers[i].ID = re.ReplaceAllStringFunc(settings.Servers[i].ID, replFn) - settings.Servers[i].Username = re.ReplaceAllStringFunc(settings.Servers[i].Username, replFn) - settings.Servers[i].Password = re.ReplaceAllStringFunc(settings.Servers[i].Password, replFn) - } - - return settings -} - -// TODO(#409): How to use with virtual filesystem + environment variables. -func globalMavenSettingsFile() string { - // ${maven.home}/conf/settings.xml - // Find ${maven.home} from the installed mvn binary - mvnExec, err := exec.LookPath("mvn") - if err != nil { - return "" - } - mvnExec, err = filepath.EvalSymlinks(mvnExec) - if err != nil { - return "" - } - - settings := filepath.Join(filepath.Dir(mvnExec), "..", "conf", "settings.xml") - settings, err = filepath.Abs(settings) - if err != nil { - return "" - } - - return settings -} - -func userMavenSettingsFile() string { - // ${user.home}/.m2/settings.xml - home, err := os.UserHomeDir() - if err != nil { - return "" - } - - return filepath.Join(home, ".m2", "settings.xml") -} - -var mavenSupportedAuths = []HTTPAuthMethod{AuthDigest, AuthBasic} - -// MakeMavenAuth returns a map of Maven authentication information index by repository ID. -func MakeMavenAuth(globalSettings, userSettings MavenSettingsXML) map[string]*HTTPAuthentication { - auth := make(map[string]*HTTPAuthentication) - for _, s := range globalSettings.Servers { - auth[s.ID] = &HTTPAuthentication{ - SupportedMethods: mavenSupportedAuths, - AlwaysAuth: false, - Username: s.Username, - Password: s.Password, - } - } - - for _, s := range userSettings.Servers { - auth[s.ID] = &HTTPAuthentication{ - SupportedMethods: mavenSupportedAuths, - AlwaysAuth: false, - Username: s.Username, - Password: s.Password, - } - } - - return auth -} diff --git a/internal/datasource/maven_settings_test.go b/internal/datasource/maven_settings_test.go deleted file mode 100644 index debd45f0..00000000 --- a/internal/datasource/maven_settings_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 datasource_test - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/osv-scalibr/internal/datasource" -) - -func TestParseMavenSettings(t *testing.T) { - t.Setenv("MAVEN_SETTINGS_TEST_USR", "UsErNaMe") - t.Setenv("MAVEN_SETTINGS_TEST_PWD", "P455W0RD") - t.Setenv("MAVEN_SETTINGS_TEST_SID", "my-cool-server") - t.Setenv("MAVEN_SETTINGS_TEST_NIL", "") - want := datasource.MavenSettingsXML{ - Servers: []datasource.MavenSettingsXMLServer{ - { - ID: "server1", - Username: "user", - Password: "pass", - }, - { - ID: "server2", - Username: "UsErNaMe", - Password: "~~P455W0RD~~", - }, - { - ID: "my-cool-server", - Username: "${env.maven_settings_test_usr}-", - Password: "${env.MAVEN_SETTINGS_TEST_BAD}", - }, - }, - } - - got := datasource.ParseMavenSettings("./testdata/maven_settings/settings.xml") - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("ParseMavenSettings() (-want +got):\n%s", diff) - } -} - -func TestMakeMavenAuth(t *testing.T) { - globalSettings := datasource.MavenSettingsXML{ - Servers: []datasource.MavenSettingsXMLServer{ - { - ID: "global", - Username: "global-user", - Password: "global-pass", - }, - { - ID: "overwrite1", - Username: "original-user", - Password: "original-pass", - }, - { - ID: "overwrite2", - Username: "user-to-be-deleted", - // no password - }, - }, - } - userSettings := datasource.MavenSettingsXML{ - Servers: []datasource.MavenSettingsXMLServer{ - { - ID: "user", - Username: "user", - Password: "pass", - }, - { - ID: "overwrite1", - Username: "new-user", - Password: "new-pass", - }, - { - ID: "overwrite2", - // no username - Password: "lone-password", - }, - }, - } - - wantSupportedMethods := []datasource.HTTPAuthMethod{datasource.AuthDigest, datasource.AuthBasic} - want := map[string]*datasource.HTTPAuthentication{ - "global": { - SupportedMethods: wantSupportedMethods, - AlwaysAuth: false, - Username: "global-user", - Password: "global-pass", - }, - "user": { - SupportedMethods: wantSupportedMethods, - AlwaysAuth: false, - Username: "user", - Password: "pass", - }, - "overwrite1": { - SupportedMethods: wantSupportedMethods, - AlwaysAuth: false, - Username: "new-user", - Password: "new-pass", - }, - "overwrite2": { - SupportedMethods: wantSupportedMethods, - AlwaysAuth: false, - Username: "", - Password: "lone-password", - }, - } - - got := datasource.MakeMavenAuth(globalSettings, userSettings) - if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(datasource.HTTPAuthentication{})); diff != "" { - t.Errorf("MakeMavenAuth() (-want +got):\n%s", diff) - } -} diff --git a/internal/datasource/testdata/maven_settings/settings.xml b/internal/datasource/testdata/maven_settings/settings.xml deleted file mode 100644 index f47fecd7..00000000 --- a/internal/datasource/testdata/maven_settings/settings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - server1 - user - pass - - - server2 - ${env.MAVEN_SETTINGS_TEST_USR} - ~~${env.MAVEN_SETTINGS_TEST_PWD}~~ - - - ${env.MAVEN_SETTINGS_TEST_SID} - ${env.maven_settings_test_usr}-${env.MAVEN_SETTINGS_TEST_NIL} - ${env.MAVEN_SETTINGS_TEST_BAD} - - - diff --git a/internal/mavenutil/maven.go b/internal/mavenutil/maven.go deleted file mode 100644 index c2b08730..00000000 --- a/internal/mavenutil/maven.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 mavenutil provides utilities for merging Maven pom/xml. -package mavenutil - -import ( - "context" - "errors" - "fmt" - "path/filepath" - - "deps.dev/util/maven" - "github.com/google/osv-scalibr/extractor/filesystem" - "github.com/google/osv-scalibr/internal/datasource" -) - -// Origin of the dependencies. -const ( - OriginManagement = "management" - OriginParent = "parent" - OriginPlugin = "plugin" - OriginProfile = "profile" -) - -// MaxParent sets a limit on the number of parents to avoid indefinite loop. -const MaxParent = 100 - -// MergeParents parses local accessible parent pom.xml or fetches it from -// upstream, merges into root project, then interpolate the properties. -// - result holds the Maven project to merge into, this is modified in place. -// - current holds the current parent project to merge. -// - parentIndex indicates the index of the current parent project, which is -// used to check if the packaging has to be `pom`. -// - allowLocal indicates whether parsing local parent pom.xml is allowed. -// - path holds the path to the current pom.xml, which is used to compute the -// relative path of parent. -func MergeParents(ctx context.Context, input *filesystem.ScanInput, mavenClient *datasource.MavenRegistryAPIClient, result *maven.Project, current maven.Parent, initialParentIndex int, allowLocal bool) error { - currentPath := "" - if input != nil { - currentPath = input.Path - } - - visited := make(map[maven.ProjectKey]struct{}, MaxParent) - for n := initialParentIndex; n < MaxParent; n++ { - if current.GroupID == "" || current.ArtifactID == "" || current.Version == "" { - break - } - if _, ok := visited[current.ProjectKey]; ok { - // A cycle of parents is detected - return errors.New("a cycle of parents is detected") - } - visited[current.ProjectKey] = struct{}{} - - var proj maven.Project - parentFoundLocally := false - if allowLocal { - var parentPath string - var err error - parentFoundLocally, parentPath, err = loadParentLocal(input, current, currentPath, &proj) - if err != nil { - return fmt.Errorf("failed to load parent at %s: %w", currentPath, err) - } - if parentPath != "" { - currentPath = parentPath - } - } - if !parentFoundLocally { - // Once we fetch a parent pom.xml from upstream, we should not - // allow parsing parent pom.xml locally anymore. - allowLocal = false - var err error - proj, err = loadParentRemote(ctx, mavenClient, current, n) - if err != nil { - return fmt.Errorf("failed to load parent from remote: %w", err) - } - } - // Use an empty JDK string and ActivationOS here to merge the default profiles. - if err := result.MergeProfiles("", maven.ActivationOS{}); err != nil { - return fmt.Errorf("failed to merge default profiles: %w", err) - } - for _, repo := range proj.Repositories { - if err := mavenClient.AddRegistry(datasource.MavenRegistry{ - URL: string(repo.URL), - ID: string(repo.ID), - ReleasesEnabled: repo.Releases.Enabled.Boolean(), - SnapshotsEnabled: repo.Snapshots.Enabled.Boolean(), - }); err != nil { - return fmt.Errorf("failed to add registry %s: %w", repo.URL, err) - } - } - result.MergeParent(proj) - current = proj.Parent - } - // Interpolate the project to resolve the properties. - return result.Interpolate() -} - -// loadParentLocal loads a parent Maven project from local file system -// and returns whether parent is found locally as well as parent path. -func loadParentLocal(input *filesystem.ScanInput, parent maven.Parent, path string, result *maven.Project) (bool, string, error) { - parentPath := parentPOMPath(input, path, string(parent.RelativePath)) - if parentPath == "" { - return false, "", nil - } - f, err := input.FS.Open(parentPath) - if err != nil { - return false, "", fmt.Errorf("failed to open parent file %s: %w", parentPath, err) - } - err = datasource.NewMavenDecoder(f).Decode(result) - if closeErr := f.Close(); closeErr != nil { - return false, "", fmt.Errorf("failed to close file: %w", err) - } - if err != nil { - return false, "", fmt.Errorf("failed to unmarshal project: %w", err) - } - return true, parentPath, nil -} - -// loadParentRemote loads a parent from remote registry. -func loadParentRemote(ctx context.Context, mavenClient *datasource.MavenRegistryAPIClient, parent maven.Parent, parentIndex int) (maven.Project, error) { - proj, err := mavenClient.GetProject(ctx, string(parent.GroupID), string(parent.ArtifactID), string(parent.Version)) - if err != nil { - return maven.Project{}, fmt.Errorf("failed to get Maven project %s:%s:%s: %w", parent.GroupID, parent.ArtifactID, parent.Version, err) - } - if parentIndex > 0 && proj.Packaging != "pom" { - // A parent project should only be of "pom" packaging type. - return maven.Project{}, fmt.Errorf("invalid packaging for parent project %s", proj.Packaging) - } - if ProjectKey(proj) != parent.ProjectKey { - // The identifiers in parent does not match what we want. - return maven.Project{}, fmt.Errorf("parent identifiers mismatch: %v, expect %v", proj.ProjectKey, parent.ProjectKey) - } - return proj, nil -} - -// ProjectKey returns a project key with empty groupId/version -// filled by corresponding fields in parent. -func ProjectKey(proj maven.Project) maven.ProjectKey { - if proj.GroupID == "" { - proj.GroupID = proj.Parent.GroupID - } - if proj.Version == "" { - proj.Version = proj.Parent.Version - } - - return proj.ProjectKey -} - -// parentPOMPath returns the path of a parent pom.xml. -// Maven looks for the parent POM first in 'relativePath', then -// the local repository '../pom.xml', and lastly in the remote repo. -// An empty string is returned if failed to resolve the parent path. -func parentPOMPath(input *filesystem.ScanInput, currentPath, relativePath string) string { - if relativePath == "" { - relativePath = "../pom.xml" - } - - path := filepath.ToSlash(filepath.Join(filepath.Dir(currentPath), relativePath)) - if info, err := input.FS.Stat(path); err == nil { - if !info.IsDir() { - return path - } - // Current path is a directory, so look for pom.xml in the directory. - path = filepath.ToSlash(filepath.Join(path, "pom.xml")) - if _, err := input.FS.Stat(path); err == nil { - return path - } - } - - return "" -} - -// GetDependencyManagement returns managed dependencies in the specified Maven project by fetching remote pom.xml. -func GetDependencyManagement(ctx context.Context, client *datasource.MavenRegistryAPIClient, groupID, artifactID, version maven.String) (maven.DependencyManagement, error) { - root := maven.Parent{ProjectKey: maven.ProjectKey{GroupID: groupID, ArtifactID: artifactID, Version: version}} - var result maven.Project - // To get dependency management from another project, we need the - // project with parents merged, so we call MergeParents by passing - // an empty project. - if err := MergeParents(ctx, nil, client.WithoutRegistries(), &result, root, 0, false); err != nil { - return maven.DependencyManagement{}, err - } - - return result.DependencyManagement, nil -} diff --git a/internal/mavenutil/maven_test.go b/internal/mavenutil/maven_test.go deleted file mode 100644 index 1cb307f2..00000000 --- a/internal/mavenutil/maven_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 mavenutil - -import ( - "path/filepath" - "testing" - - "github.com/google/osv-scalibr/testing/extracttest" -) - -func TestParentPOMPath(t *testing.T) { - input := extracttest.GenerateScanInputMock(t, extracttest.ScanInputMockConfig{ - Path: filepath.Join("testdata", "my-app", "pom.xml"), - }) - defer extracttest.CloseTestScanInput(t, input) - - tests := []struct { - currentPath, relativePath string - want string - }{ - // testdata - // |- maven - // | |- my-app - // | | |- pom.xml - // | |- parent - // | | |- pom.xml - // |- pom.xml - { - // Parent path is specified correctly. - currentPath: filepath.Join("testdata", "my-app", "pom.xml"), - relativePath: "../parent/pom.xml", - want: filepath.Join("testdata", "parent", "pom.xml"), - }, - { - // Wrong file name is specified in relative path. - currentPath: filepath.Join("testdata", "my-app", "pom.xml"), - relativePath: "../parent/abc.xml", - want: "", - }, - { - // Wrong directory is specified in relative path. - currentPath: filepath.Join("testdata", "my-app", "pom.xml"), - relativePath: "../not-found/pom.xml", - want: "", - }, - { - // Only directory is specified. - currentPath: filepath.Join("testdata", "my-app", "pom.xml"), - relativePath: "../parent", - want: filepath.Join("testdata", "parent", "pom.xml"), - }, - { - // Parent relative path is default to '../pom.xml'. - currentPath: filepath.Join("testdata", "my-app", "pom.xml"), - relativePath: "", - want: filepath.Join("testdata", "pom.xml"), - }, - { - // No pom.xml is found even in the default path. - currentPath: filepath.Join("testdata", "pom.xml"), - relativePath: "", - want: "", - }, - } - for _, tt := range tests { - got := parentPOMPath(&input, tt.currentPath, tt.relativePath) - if got != filepath.ToSlash(tt.want) { - t.Errorf("ParentPOMPath(%s, %s): got %s, want %s", tt.currentPath, tt.relativePath, got, tt.want) - } - } -} diff --git a/internal/mavenutil/testdata/my-app/pom.xml b/internal/mavenutil/testdata/my-app/pom.xml deleted file mode 100644 index d01d0478..00000000 --- a/internal/mavenutil/testdata/my-app/pom.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - org.test - my-app - 1.0.0 - - diff --git a/internal/mavenutil/testdata/parent/pom.xml b/internal/mavenutil/testdata/parent/pom.xml deleted file mode 100644 index fe2e50f5..00000000 --- a/internal/mavenutil/testdata/parent/pom.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - org.test - parent-pom - 1.0.0 - - diff --git a/internal/mavenutil/testdata/pom.xml b/internal/mavenutil/testdata/pom.xml deleted file mode 100644 index 1d289685..00000000 --- a/internal/mavenutil/testdata/pom.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - org.test - test - 1.0.0 - - diff --git a/internal/resolution/client/client.go b/internal/resolution/client/client.go deleted file mode 100644 index 227b0580..00000000 --- a/internal/resolution/client/client.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 client provides clients required by dependency resolution. -package client - -import ( - "deps.dev/util/resolve" -) - -// DependencyClient is the interface of the client required by dependency resolution. -type DependencyClient interface { - resolve.Client - // WriteCache writes a manifest-specific resolution cache. - WriteCache(filepath string) error - // LoadCache loads a manifest-specific resolution cache. - LoadCache(filepath string) error - // AddRegistries adds the specified registries to fetch data. - AddRegistries(registries []Registry) error -} - -// Registry is the interface of a registry to fetch data. -type Registry any diff --git a/internal/resolution/client/depsdev_client.go b/internal/resolution/client/depsdev_client.go deleted file mode 100644 index 04dd8563..00000000 --- a/internal/resolution/client/depsdev_client.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 client - -import ( - "encoding/gob" - "os" - - "deps.dev/util/resolve" - "github.com/google/osv-scalibr/internal/datasource" -) - -const depsDevCacheExt = ".resolve.deps" - -// DepsDevClient is a ResolutionClient wrapping the official resolve.APIClient -type DepsDevClient struct { - resolve.APIClient - c *datasource.CachedInsightsClient -} - -// NewDepsDevClient creates a new DepsDevClient. -func NewDepsDevClient(addr string, userAgent string) (*DepsDevClient, error) { - c, err := datasource.NewCachedInsightsClient(addr, userAgent) - if err != nil { - return nil, err - } - - return &DepsDevClient{APIClient: *resolve.NewAPIClient(c), c: c}, nil -} - -// AddRegistries is a placeholder here for DepsDevClient. -func (d *DepsDevClient) AddRegistries(_ []Registry) error { return nil } - -// WriteCache writes cache at the given path. -func (d *DepsDevClient) WriteCache(path string) error { - f, err := os.Create(path + depsDevCacheExt) - if err != nil { - return err - } - defer f.Close() - - return gob.NewEncoder(f).Encode(d.c) -} - -// LoadCache loads the cache at the given path. -func (d *DepsDevClient) LoadCache(path string) error { - f, err := os.Open(path + depsDevCacheExt) - if err != nil { - return err - } - defer f.Close() - - return gob.NewDecoder(f).Decode(&d.c) -} diff --git a/internal/resolution/client/maven_registry_client.go b/internal/resolution/client/maven_registry_client.go deleted file mode 100644 index 8a7c6943..00000000 --- a/internal/resolution/client/maven_registry_client.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 client - -import ( - "context" - "errors" - "fmt" - "strings" - - "deps.dev/util/maven" - "deps.dev/util/resolve" - "deps.dev/util/resolve/version" - "github.com/google/osv-scalibr/internal/datasource" - "github.com/google/osv-scalibr/internal/mavenutil" -) - -// MavenRegistryClient is a client to fetch data from Maven registry. -type MavenRegistryClient struct { - api *datasource.MavenRegistryAPIClient -} - -// NewMavenRegistryClient makes a new MavenRegistryClient. -func NewMavenRegistryClient(registry string) (*MavenRegistryClient, error) { - client, err := datasource.NewMavenRegistryAPIClient(datasource.MavenRegistry{URL: registry, ReleasesEnabled: true}) - if err != nil { - return nil, err - } - - return &MavenRegistryClient{api: client}, nil -} - -// Version returns metadata of a version specified by the VersionKey. -func (c *MavenRegistryClient) Version(ctx context.Context, vk resolve.VersionKey) (resolve.Version, error) { - g, a, found := strings.Cut(vk.Name, ":") - if !found { - return resolve.Version{}, fmt.Errorf("invalid Maven package name %s", vk.Name) - } - proj, err := c.api.GetProject(ctx, g, a, vk.Version) - if err != nil { - return resolve.Version{}, err - } - - regs := make([]string, len(proj.Repositories)) - // Repositories are served as dependency registries. - // https://github.com/google/deps.dev/blob/main/util/resolve/api.go#L106 - for i, repo := range proj.Repositories { - regs[i] = "dep:" + string(repo.URL) - } - var attr version.AttrSet - if len(regs) > 0 { - attr.SetAttr(version.Registries, strings.Join(regs, "|")) - } - - return resolve.Version{VersionKey: vk, AttrSet: attr}, nil -} - -// Versions returns all the available versions of the package specified by the given PackageKey. -// TODO: we should also include versions not listed in the metadata file -// There exist versions in the repository but not listed in the metada file, -// for example version 20030203.000550 of package commons-io:commons-io -// https://repo1.maven.org/maven2/commons-io/commons-io/20030203.000550/. -// A package may depend on such version if a soft requirement of this version -// is declared. -// We need to find out if there are such versions and include them in the -// returned versions. -func (c *MavenRegistryClient) Versions(ctx context.Context, pk resolve.PackageKey) ([]resolve.Version, error) { - if pk.System != resolve.Maven { - return nil, fmt.Errorf("wrong system: %v", pk.System) - } - - g, a, found := strings.Cut(pk.Name, ":") - if !found { - return nil, fmt.Errorf("invalid Maven package name %s", pk.Name) - } - versions, err := c.api.GetVersions(ctx, g, a) - if err != nil { - return nil, err - } - - vks := make([]resolve.Version, len(versions)) - for i, v := range versions { - vks[i] = resolve.Version{ - VersionKey: resolve.VersionKey{ - PackageKey: pk, - Version: string(v), - VersionType: resolve.Concrete, - }} - } - - return vks, nil -} - -// Requirements returns requirements of a version specified by the VersionKey. -func (c *MavenRegistryClient) Requirements(ctx context.Context, vk resolve.VersionKey) ([]resolve.RequirementVersion, error) { - if vk.System != resolve.Maven { - return nil, fmt.Errorf("wrong system: %v", vk.System) - } - - g, a, found := strings.Cut(vk.Name, ":") - if !found { - return nil, fmt.Errorf("invalid Maven package name %s", vk.Name) - } - proj, err := c.api.GetProject(ctx, g, a, vk.Version) - if err != nil { - return nil, err - } - - // Only merge default profiles by passing empty JDK and OS information. - if err := proj.MergeProfiles("", maven.ActivationOS{}); err != nil { - return nil, err - } - - // We should not add registries defined in dependencies pom.xml files. - apiWithoutRegistries := c.api.WithoutRegistries() - // We need to merge parents for potential dependencies in parents. - if err := mavenutil.MergeParents(ctx, nil, apiWithoutRegistries, &proj, proj.Parent, 1, false); err != nil { - return nil, err - } - proj.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) { - return mavenutil.GetDependencyManagement(ctx, apiWithoutRegistries, groupID, artifactID, version) - }) - - reqs := make([]resolve.RequirementVersion, 0, len(proj.Dependencies)) - for _, d := range proj.Dependencies { - reqs = append(reqs, resolve.RequirementVersion{ - VersionKey: resolve.VersionKey{ - PackageKey: resolve.PackageKey{ - System: resolve.Maven, - Name: d.Name(), - }, - VersionType: resolve.Requirement, - Version: string(d.Version), - }, - Type: resolve.MavenDepType(d, ""), - }) - } - - return reqs, nil -} - -// MatchingVersions returns versions matching the requirement specified by the VersionKey. -func (c *MavenRegistryClient) MatchingVersions(ctx context.Context, vk resolve.VersionKey) ([]resolve.Version, error) { - if vk.System != resolve.Maven { - return nil, fmt.Errorf("wrong system: %v", vk.System) - } - - versions, err := c.Versions(ctx, vk.PackageKey) - if err != nil { - return nil, err - } - - return resolve.MatchRequirement(vk, versions), nil -} - -// AddRegistries adds registries to the MavenRegistryClient. -func (c *MavenRegistryClient) AddRegistries(registries []Registry) error { - for _, reg := range registries { - specific, ok := reg.(datasource.MavenRegistry) - if !ok { - return errors.New("invalid Maven registry information") - } - if err := c.api.AddRegistry(specific); err != nil { - return err - } - } - - return nil -} diff --git a/internal/resolution/client/override_client.go b/internal/resolution/client/override_client.go deleted file mode 100644 index 0dd11fc6..00000000 --- a/internal/resolution/client/override_client.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 client - -import ( - "context" - "slices" - - "deps.dev/util/resolve" -) - -// OverrideClient wraps a DependencyClient, allowing for custom packages & versions to be added -type OverrideClient struct { - DependencyClient - // Can't quite reuse resolve.LocalClient because it automatically creates dependencies - pkgVers map[resolve.PackageKey][]resolve.Version // versions of a package - verDeps map[resolve.VersionKey][]resolve.RequirementVersion // dependencies of a version -} - -// NewOverrideClient makes a new OverrideClient. -func NewOverrideClient(c DependencyClient) *OverrideClient { - return &OverrideClient{ - DependencyClient: c, - pkgVers: make(map[resolve.PackageKey][]resolve.Version), - verDeps: make(map[resolve.VersionKey][]resolve.RequirementVersion), - } -} - -// AddVersion adds the specified version and dependencies to the client. -func (c *OverrideClient) AddVersion(v resolve.Version, deps []resolve.RequirementVersion) { - // TODO: Inserting multiple co-dependent requirements may not work, depending on order - versions := c.pkgVers[v.PackageKey] - sem := v.Semver() - // Only add it to the versions if not already there (and keep versions sorted) - idx, ok := slices.BinarySearchFunc(versions, v, func(a, b resolve.Version) int { - return sem.Compare(a.Version, b.Version) - }) - if !ok { - versions = slices.Insert(versions, idx, v) - } - c.pkgVers[v.PackageKey] = versions - c.verDeps[v.VersionKey] = slices.Clone(deps) // overwrites dependencies if called multiple times with same version -} - -// Version returns the version specified by the VersionKey. -func (c *OverrideClient) Version(ctx context.Context, vk resolve.VersionKey) (resolve.Version, error) { - for _, v := range c.pkgVers[vk.PackageKey] { - if v.VersionKey == vk { - return v, nil - } - } - - return c.DependencyClient.Version(ctx, vk) -} - -// Versions returns the versions of a package specified by the PackageKey. -func (c *OverrideClient) Versions(ctx context.Context, pk resolve.PackageKey) ([]resolve.Version, error) { - if vers, ok := c.pkgVers[pk]; ok { - return vers, nil - } - - return c.DependencyClient.Versions(ctx, pk) -} - -// Requirements returns the requirement versions of the version specified by the VersionKey. -func (c *OverrideClient) Requirements(ctx context.Context, vk resolve.VersionKey) ([]resolve.RequirementVersion, error) { - if deps, ok := c.verDeps[vk]; ok { - return deps, nil - } - - return c.DependencyClient.Requirements(ctx, vk) -} - -// MatchingVersions returns the versions matching the requirement specified by the VersionKey. -func (c *OverrideClient) MatchingVersions(ctx context.Context, vk resolve.VersionKey) ([]resolve.Version, error) { - if vs, ok := c.pkgVers[vk.PackageKey]; ok { - return resolve.MatchRequirement(vk, vs), nil - } - - return c.DependencyClient.MatchingVersions(ctx, vk) -} diff --git a/internal/resolution/clienttest/mock_http.go b/internal/resolution/clienttest/mock_http.go deleted file mode 100644 index 0a4bc710..00000000 --- a/internal/resolution/clienttest/mock_http.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 clienttest provides mock servers for testing. -package clienttest - -import ( - "log" - "net/http" - "net/http/httptest" - "os" - "strings" - "sync" - "testing" -) - -// MockHTTPServer is a simple HTTP Server for mocking basic requests. -type MockHTTPServer struct { - *httptest.Server - mu sync.Mutex - response map[string][]byte // path -> response - authorization string // expected Authorization header contents -} - -// NewMockHTTPServer starts and returns a new simple HTTP Server for mocking basic requests. -// The Server will automatically be shut down with Close() in the test Cleanup function. -// -// Use the SetResponse / SetResponseFromFile to set the responses for specific URL paths. -func NewMockHTTPServer(t *testing.T) *MockHTTPServer { - t.Helper() - mock := &MockHTTPServer{response: make(map[string][]byte)} - mock.Server = httptest.NewServer(mock) - t.Cleanup(func() { mock.Server.Close() }) - - return mock -} - -// SetResponse sets the Server's response for the URL path to be response bytes. -func (m *MockHTTPServer) SetResponse(t *testing.T, path string, response []byte) { - t.Helper() - m.mu.Lock() - defer m.mu.Unlock() - path = strings.TrimPrefix(path, "/") - m.response[path] = response -} - -// SetResponseFromFile sets the Server's response for the URL path to be the contents of the file at filename. -func (m *MockHTTPServer) SetResponseFromFile(t *testing.T, path string, filename string) { - t.Helper() - b, err := os.ReadFile(filename) - if err != nil { - t.Fatalf("failed to read response file: %v", err) - } - m.SetResponse(t, path, b) -} - -// SetAuthorization sets the contents of the 'Authorization' header the server expects for all endpoints. -// -// The incoming requests' headers must match the auth string exactly, otherwise the server will response with 401 Unauthorized. -// If authorization is unset or empty, the server will not require authorization. -func (m *MockHTTPServer) SetAuthorization(t *testing.T, auth string) { - t.Helper() - m.mu.Lock() - defer m.mu.Unlock() - m.authorization = auth -} - -// ServeHTTP is the http.Handler for the underlying httptest.Server. -func (m *MockHTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - m.mu.Lock() - wantAuth := m.authorization - resp, ok := m.response[strings.TrimPrefix(r.URL.EscapedPath(), "/")] - m.mu.Unlock() - - if wantAuth != "" && r.Header.Get("Authorization") != wantAuth { - w.WriteHeader(http.StatusUnauthorized) - resp = []byte("unauthorized") - } else if !ok { - w.WriteHeader(http.StatusNotFound) - resp = []byte("not found") - } - - if _, err := w.Write(resp); err != nil { - log.Fatalf("Write: %v", err) - } -} diff --git a/internal/resolution/clienttest/mock_resolution_client.go b/internal/resolution/clienttest/mock_resolution_client.go deleted file mode 100644 index 55aafb46..00000000 --- a/internal/resolution/clienttest/mock_resolution_client.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2025 Google LLC -// -// 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 clienttest - -import ( - "os" - "strings" - "testing" - - "deps.dev/util/resolve" - "deps.dev/util/resolve/schema" - "github.com/google/osv-scalibr/internal/resolution/client" - "gopkg.in/yaml.v3" -) - -// ResolutionUniverse defines a mock resolution universe. -type ResolutionUniverse struct { - System string `yaml:"system"` - Schema string `yaml:"schema"` -} - -type mockDependencyClient struct { - *resolve.LocalClient -} - -func (mdc mockDependencyClient) LoadCache(string) error { return nil } -func (mdc mockDependencyClient) WriteCache(string) error { return nil } -func (mdc mockDependencyClient) AddRegistries(_ []client.Registry) error { return nil } - -// NewMockResolutionClient creates a new mock resolution client from the given universe YAML. -func NewMockResolutionClient(t *testing.T, universeYAML string) client.DependencyClient { - t.Helper() - f, err := os.Open(universeYAML) - if err != nil { - t.Fatalf("failed opening mock universe: %v", err) - } - defer f.Close() - dec := yaml.NewDecoder(f) - - var universe ResolutionUniverse - if err := dec.Decode(&universe); err != nil { - t.Fatalf("failed decoding mock universe: %v", err) - } - - var sys resolve.System - switch strings.ToLower(universe.System) { - case "npm": - sys = resolve.NPM - case "maven": - sys = resolve.Maven - default: - t.Fatalf("unknown ecosystem in universe: %s", universe.System) - } - - // schema needs a strict tab indentation, which is awkward to do within the YAML. - // Replace double space from yaml with single tab - universe.Schema = strings.ReplaceAll(universe.Schema, " ", "\t") - sch, err := schema.New(universe.Schema, sys) - if err != nil { - t.Fatalf("failed parsing schema: %v", err) - } - - return mockDependencyClient{sch.NewClient()} -}