From b4b3279573988ea1a7d8d02fe64e4bbaf5b63236 Mon Sep 17 00:00:00 2001 From: Phavya Jayakumar Date: Thu, 27 Aug 2026 12:28:54 +0530 Subject: [PATCH] XRAY-158270 - Implement http HEAD GET security for uv --- commands/curation/curationaudit.go | 14 +++++++------- commands/curation/curationaudit_test.go | 16 +++++++++++----- sca/bom/buildinfo/technologies/pnpm/pnpm_test.go | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 0053baa59..9d7f166c7 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -1301,7 +1301,7 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map if err != nil { return err } - if tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Pipenv { + if tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Pipenv || tech == techutils.Uv { rtManager, err = boundedRedirectManager(serverDetails) if err != nil { return err @@ -2208,7 +2208,7 @@ func (nc *treeAnalyzer) fetchNodeStatus(node xrayUtils.GraphNode, p *sync.Map) e requestDetails := nc.httpClientDetails.Clone() var resp *http.Response var err error - if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv { + if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv || nc.tech == techutils.Uv { resp, _, err = nc.sendBoundedRequest(http.MethodHead, packageUrl, requestDetails) } else { resp, _, err = nc.rtManager.Client().SendHead(packageUrl, requestDetails) @@ -2289,7 +2289,7 @@ func (ca *CurationAuditCommand) runCvsFallback(cvsErr *python.CvsBlockedError, t if err != nil { return fmt.Errorf("curation-blocked resolution fallback: failed to get Artifactory manager (%w); %s error: %w", err, tech, cvsErr) } - if tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Pipenv { + if tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Pipenv || tech == techutils.Uv { rtManager, err = boundedRedirectManager(serverDetails) if err != nil { return fmt.Errorf("curation-blocked resolution fallback: failed to create bounded HTTP manager: %w", err) @@ -2570,7 +2570,7 @@ func (nc *treeAnalyzer) lookupPypiAllVersions(name string) ([]string, error) { var resp *http.Response var body []byte var err error - if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv { + if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv || nc.tech == techutils.Uv { resp, body, err = nc.sendBoundedRequest(http.MethodGet, metadataURL, requestDetails) } else { resp, body, _, err = nc.rtManager.Client().SendGet(metadataURL, true, requestDetails) @@ -2610,7 +2610,7 @@ func (nc *treeAnalyzer) lookupPypiNormalDownloadURL(name, ver string) (string, e var resp *http.Response var body []byte var err error - if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv { + if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv || nc.tech == techutils.Uv { resp, body, err = nc.sendBoundedRequest(http.MethodGet, metadataURL, requestDetails) } else { resp, body, _, err = nc.rtManager.Client().SendGet(metadataURL, true, requestDetails) @@ -2711,7 +2711,7 @@ func (nc *treeAnalyzer) fetchCvsBlockedStatus(pins []python.PinnedRequirement) [ headDetails := nc.httpClientDetails.Clone() var headResp *http.Response var headErr error - if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv { + if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv || nc.tech == techutils.Uv { headResp, _, headErr = nc.sendBoundedRequest(http.MethodHead, dlURL, headDetails) } else { headResp, _, headErr = nc.rtManager.Client().SendHead(dlURL, headDetails) @@ -2811,7 +2811,7 @@ func (nc *treeAnalyzer) getBlockedPackageDetails(packageUrl string, name string, var getResp *http.Response var respBody []byte var err error - if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv { + if nc.tech == techutils.Pip || nc.tech == techutils.Poetry || nc.tech == techutils.Pipenv || nc.tech == techutils.Uv { getResp, respBody, err = nc.sendBoundedRequest(http.MethodGet, packageUrl, requestDetails) } else { getResp, respBody, _, err = nc.rtManager.Client().SendGet(packageUrl, true, requestDetails) diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index 776ed9b3a..59cfb1707 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -4497,7 +4497,7 @@ url = "https://pypi.org/simple" } func TestSendBoundedRequestRejectsRedirectOutsideRepository(t *testing.T) { - for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv} { + for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv, techutils.Uv} { t.Run(tech.String(), func(t *testing.T) { var outsideRequested atomic.Bool var requests atomic.Int32 @@ -4554,7 +4554,7 @@ func TestCvsMetadataRejectsRedirectOutsideRepository(t *testing.T) { }, }, } - for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv} { + for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv, techutils.Uv} { for _, test := range tests { t.Run(tech.String()+"/"+test.name, func(t *testing.T) { var outsideRequested atomic.Bool @@ -4594,7 +4594,7 @@ func TestCvsMetadataRejectsRedirectOutsideRepository(t *testing.T) { // tech-branch in fetchNodeStatus (not sendBoundedRequest directly) to guard against a // regression that silently narrows the bounded-redirect condition back to Pipenv only. func TestFetchNodeStatusRoutesPipAndPoetryThroughBoundedRedirects(t *testing.T) { - for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv} { + for _, tech := range []techutils.Technology{techutils.Pip, techutils.Poetry, techutils.Pipenv, techutils.Uv} { t.Run(tech.String(), func(t *testing.T) { var outsideRequested atomic.Bool var requests atomic.Int32 @@ -5046,7 +5046,7 @@ func TestFetchCvsBlockedStatusUv(t *testing.T) { blockResponse := fmt.Sprintf(`{"errors":[{"status":403,"message":%q}]}`, blockMsg) versionMetaJSON := fmt.Sprintf(`{"urls":[{"packagetype":"bdist_wheel","url":"../../%s"}]}`, whlRelativePath) - serverMock, _, rtManager := coreCommonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { + serverMock, serverDetails, _ := coreCommonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/pypi/"+blockedPkg+"/"+blockedVer+"/json"): w.WriteHeader(http.StatusOK) @@ -5062,6 +5062,9 @@ func TestFetchCvsBlockedStatusUv(t *testing.T) { }) defer serverMock.Close() + // Zero retries required; see SendWithBoundedRedirects. + rtManager, err := rtUtils.CreateServiceManager(serverDetails, 0, 0, false) + require.NoError(t, err) rtAuth := rtManager.GetConfig().GetServiceDetails() httpClientDetails := rtAuth.CreateHttpClientDetails() @@ -5120,7 +5123,7 @@ func TestFetchCvsBlockedStatusUvTransitive(t *testing.T) { allVersionsJSON := `{"releases":{"1.4.0":[],"1.4.1":[],"1.4.5":[],"1.4.7":[]}}` versionMetaJSON := fmt.Sprintf(`{"urls":[{"packagetype":"bdist_wheel","url":"../../%s"}]}`, whlRelativePath) - serverMock, _, rtManager := coreCommonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { + serverMock, serverDetails, _ := coreCommonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/pypi/"+blockedPkg+"/json"): w.WriteHeader(http.StatusOK) @@ -5139,6 +5142,9 @@ func TestFetchCvsBlockedStatusUvTransitive(t *testing.T) { }) defer serverMock.Close() + // Zero retries required; see SendWithBoundedRedirects. + rtManager, err := rtUtils.CreateServiceManager(serverDetails, 0, 0, false) + require.NoError(t, err) rtAuth := rtManager.GetConfig().GetServiceDetails() httpClientDetails := rtAuth.CreateHttpClientDetails() diff --git a/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go b/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go index 6a9eec42b..8feab5115 100644 --- a/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go +++ b/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go @@ -46,7 +46,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) { name: "With transitive dependencies", treeDepth: "1", expectedUniqueDeps: []string{ - "npm://axios:1.19.0", + "npm://axios:1.20.0", "npm://balaganjs:1.0.0", "npm://yargs:13.3.0", "npm://zen-website:1.0.0", @@ -56,7 +56,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) { Nodes: []*xrayUtils.GraphNode{ { Id: "npm://balaganjs:1.0.0", - Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.19.0"}, {Id: "npm://yargs:13.3.0"}}, + Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.20.0"}, {Id: "npm://yargs:13.3.0"}}, }, }, },