diff --git a/docs/introduction/proxying.md b/docs/introduction/proxying.md index 9dba8032..ac0f4244 100644 --- a/docs/introduction/proxying.md +++ b/docs/introduction/proxying.md @@ -36,6 +36,8 @@ HTTP_PROXY=socks5://127.0.0.1:1080 pipeleek gl scan -u https://gitlab.internal.c Alternatively, use the `--proxy` flag to set any proxy from the command line without relying on `HTTP_PROXY`. It accepts both HTTP and SOCKS5 URLs and takes precedence over the environment variable: +All Gitea commands (`scan`, `enum`, `secrets`, `variables`, and `vuln`) use these shared HTTP settings, including `--ignore-proxy`, `--tls-verification`, and `--http-timeout`. For `vuln`, the settings apply to both the Gitea version lookup and NIST requests. + ```bash # HTTP proxy pipeleek --proxy http://127.0.0.1:8080 gl scan -u https://gitlab.com -t glpat-xxxxx diff --git a/pkg/gitea/enum/enum.go b/pkg/gitea/enum/enum.go index 363e50dc..ae34b22e 100644 --- a/pkg/gitea/enum/enum.go +++ b/pkg/gitea/enum/enum.go @@ -4,12 +4,14 @@ import ( "fmt" "code.gitea.io/sdk/gitea" + "github.com/CompassSecurity/pipeleek/pkg/httpclient" "github.com/rs/zerolog/log" ) // RunEnum performs the enumeration of Gitea access rights. func RunEnum(giteaURL, apiToken string) error { - client, err := gitea.NewClient(giteaURL, gitea.SetToken(apiToken)) + client, err := gitea.NewClient(giteaURL, gitea.SetToken(apiToken), + gitea.SetHTTPClient(httpclient.GetPipeleekStandardHTTPClient())) if err != nil { return err } diff --git a/pkg/gitea/http_settings_test.go b/pkg/gitea/http_settings_test.go new file mode 100644 index 00000000..889cae3f --- /dev/null +++ b/pkg/gitea/http_settings_test.go @@ -0,0 +1,207 @@ +package gitea_test + +import ( + "bytes" + "errors" + "net" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + giteaenum "github.com/CompassSecurity/pipeleek/pkg/gitea/enum" + "github.com/CompassSecurity/pipeleek/pkg/gitea/secrets" + "github.com/CompassSecurity/pipeleek/pkg/gitea/variables" + "github.com/CompassSecurity/pipeleek/pkg/gitea/vuln" + "github.com/CompassSecurity/pipeleek/pkg/httpclient" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func serveGitea(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api/v1/version": + _, _ = w.Write([]byte(`{"version":"1.20.0"}`)) + case "/api/v1/user": + _, _ = w.Write([]byte(`{"id":1,"login":"testuser"}`)) + case "/api/v1/user/repos", "/api/v1/user/orgs": + _, _ = w.Write([]byte(`[]`)) + default: + w.WriteHeader(http.StatusNotFound) + } +} + +func TestCommandsHTTPSettings(t *testing.T) { + commands := []struct { + name string + run func(string) error + }{ + { + name: "enum", + run: func(url string) error { + return giteaenum.RunEnum(url, "test-token") + }, + }, + { + name: "secrets", + run: func(url string) error { + return secrets.ListAllSecrets(secrets.Config{URL: url, Token: "test-token"}) + }, + }, + { + name: "variables", + run: func(url string) error { + return variables.ListAllVariables(variables.Config{URL: url, Token: "test-token"}) + }, + }, + } + + runCommandsHTTPSettings(t, commands) +} + +func TestVulnHTTPSettings(t *testing.T) { + httpclient.SetIgnoreProxy(true) + t.Cleanup(func() { + httpclient.SetProxy("") + httpclient.SetIgnoreProxy(false) + httpclient.SetInsecureSkipVerify(true) + httpclient.SetHTTPTimeout(0) + }) + nist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"totalResults":0,"vulnerabilities":[]}`)) + })) + defer nist.Close() + t.Setenv("PIPELEEK_NIST_BASE_URL", nist.URL) + + for _, mode := range []string{"explicit proxy", "self-signed TLS", "enforced TLS", "timeout"} { + t.Run(mode, func(t *testing.T) { + httpclient.SetProxy("") + httpclient.SetInsecureSkipVerify(true) + httpclient.SetHTTPTimeout(0) + var output bytes.Buffer + savedLogger := log.Logger + log.Logger = zerolog.New(&output) + defer func() { log.Logger = savedLogger }() + + var versionRequests atomic.Int32 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/nist" { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"totalResults":0,"vulnerabilities":[]}`)) + return + } + versionRequests.Add(1) + if mode == "timeout" { + time.Sleep(100 * time.Millisecond) + } + serveGitea(w, r) + }) + var target *httptest.Server + if mode == "self-signed TLS" || mode == "enforced TLS" { + target = httptest.NewTLSServer(handler) + } else { + target = httptest.NewServer(handler) + } + defer target.Close() + url := target.URL + switch mode { + case "explicit proxy": + httpclient.SetProxy(target.URL) + t.Setenv("PIPELEEK_NIST_BASE_URL", "http://nist.invalid/nist") + url = "http://gitea.invalid" + case "enforced TLS": + httpclient.SetInsecureSkipVerify(false) + case "timeout": + httpclient.SetHTTPTimeout(20 * time.Millisecond) + } + + vuln.RunCheckVulns(url, "test-token") + assert.Contains(t, output.String(), "Finished vuln scan") + if mode == "enforced TLS" || mode == "timeout" { + assert.Contains(t, output.String(), "Failed creating Gitea client") + assert.Contains(t, output.String(), `"version":"none"`) + } else { + assert.Contains(t, output.String(), `"version":"1.20.0"`) + assert.NotContains(t, output.String(), "Failed creating Gitea client") + assert.Positive(t, versionRequests.Load()) + } + }) + } +} + +func runCommandsHTTPSettings(t *testing.T, commands []struct { + name string + run func(string) error +}) { + t.Helper() + for _, command := range commands { + t.Run(command.name, func(t *testing.T) { + httpclient.SetProxy("") + httpclient.SetIgnoreProxy(true) + httpclient.SetInsecureSkipVerify(true) + httpclient.SetHTTPTimeout(0) + t.Cleanup(func() { + httpclient.SetProxy("") + httpclient.SetIgnoreProxy(false) + httpclient.SetInsecureSkipVerify(true) + httpclient.SetHTTPTimeout(0) + }) + + t.Run("explicit proxy takes precedence over ignore-proxy", func(t *testing.T) { + var directRequests, proxyRequests atomic.Int32 + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + directRequests.Add(1) + serveGitea(w, r) + })) + defer target.Close() + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proxyRequests.Add(1) + assert.True(t, r.URL.IsAbs(), "proxy must receive an absolute request URL") + assert.Equal(t, "token test-token", r.Header.Get("Authorization")) + serveGitea(w, r) + })) + defer proxy.Close() + + httpclient.SetProxy(proxy.URL) + defer httpclient.SetProxy("") + + require.NoError(t, command.run(target.URL)) + assert.GreaterOrEqual(t, proxyRequests.Load(), int32(3), "all SDK requests must use the proxy") + assert.Zero(t, directRequests.Load(), "requests must not bypass the explicit proxy") + }) + + t.Run("TLS verification settings", func(t *testing.T) { + target := httptest.NewTLSServer(http.HandlerFunc(serveGitea)) + defer target.Close() + + httpclient.SetInsecureSkipVerify(true) + require.NoError(t, command.run(target.URL), "default settings must support self-signed targets") + + httpclient.SetInsecureSkipVerify(false) + defer httpclient.SetInsecureSkipVerify(true) + require.Error(t, command.run(target.URL), "enforced verification must reject an untrusted certificate") + }) + + t.Run("HTTP timeout", func(t *testing.T) { + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + serveGitea(w, r) + })) + defer target.Close() + + httpclient.SetHTTPTimeout(10 * time.Millisecond) + defer httpclient.SetHTTPTimeout(0) + err := command.run(target.URL) + require.Error(t, err) + var netErr net.Error + require.True(t, errors.As(err, &netErr), "expected a network timeout, got %v", err) + assert.True(t, netErr.Timeout()) + }) + }) + } +} diff --git a/pkg/gitea/secrets/secrets.go b/pkg/gitea/secrets/secrets.go index dd93fd3e..8f97a66c 100644 --- a/pkg/gitea/secrets/secrets.go +++ b/pkg/gitea/secrets/secrets.go @@ -4,6 +4,7 @@ import ( "fmt" "code.gitea.io/sdk/gitea" + "github.com/CompassSecurity/pipeleek/pkg/httpclient" "github.com/rs/zerolog/log" ) @@ -58,7 +59,8 @@ func ListAllSecrets(cfg Config) error { } func createClientContext(cfg Config) (*clientContext, error) { - client, err := gitea.NewClient(cfg.URL, gitea.SetToken(cfg.Token)) + client, err := gitea.NewClient(cfg.URL, gitea.SetToken(cfg.Token), + gitea.SetHTTPClient(httpclient.GetPipeleekStandardHTTPClient())) if err != nil { return nil, err } diff --git a/pkg/gitea/variables/variables.go b/pkg/gitea/variables/variables.go index 1de76b2d..c144eeef 100644 --- a/pkg/gitea/variables/variables.go +++ b/pkg/gitea/variables/variables.go @@ -60,7 +60,8 @@ func ListAllVariables(cfg Config) error { } func createClientContext(cfg Config) (*clientContext, error) { - client, err := gitea.NewClient(cfg.URL, gitea.SetToken(cfg.Token)) + client, err := gitea.NewClient(cfg.URL, gitea.SetToken(cfg.Token), + gitea.SetHTTPClient(httpclient.GetPipeleekStandardHTTPClient())) if err != nil { return nil, err } diff --git a/pkg/gitea/vuln/vuln.go b/pkg/gitea/vuln/vuln.go index fda8de31..58bd7596 100644 --- a/pkg/gitea/vuln/vuln.go +++ b/pkg/gitea/vuln/vuln.go @@ -14,7 +14,8 @@ import ( // RunCheckVulns checks the Gitea instance for vulnerabilities func RunCheckVulns(giteaUrl, giteaApiToken string) { version := "none" - giteaClient, err := gitea.NewClient(giteaUrl, gitea.SetToken(giteaApiToken)) + giteaClient, err := gitea.NewClient(giteaUrl, gitea.SetToken(giteaApiToken), + gitea.SetHTTPClient(httpclient.GetPipeleekStandardHTTPClient())) if err != nil { log.Warn().Err(err).Msg("Failed creating Gitea client") } else { diff --git a/tests/e2e/gitea/enum/enum_test.go b/tests/e2e/gitea/enum/enum_test.go index 777700c6..07a82adb 100644 --- a/tests/e2e/gitea/enum/enum_test.go +++ b/tests/e2e/gitea/enum/enum_test.go @@ -58,19 +58,26 @@ func TestGiteaEnum(t *testing.T) { }) defer cleanup() - stdout, stderr, exitErr := testutil.RunCLI(t, []string{ - "gitea", "enum", - "--url", server.URL, - "--token", "gitea-token", - }, nil, 10*time.Second) + for _, mode := range []string{"direct", "explicit proxy"} { + t.Run(mode, func(t *testing.T) { + args := []string{"gitea", "enum", "--token", "gitea-token"} + if mode == "explicit proxy" { + args = append(args, "--url", "http://gitea.invalid", "--proxy", server.URL, "--ignore-proxy") + } else { + args = append(args, "--url", server.URL) + } + before := len(getRequests()) + stdout, stderr, exitErr := testutil.RunCLI(t, args, nil, 10*time.Second) - assert.Nil(t, exitErr, "Enum command should succeed") - - requests := getRequests() - assert.True(t, len(requests) >= 1, "Should make API requests") - - t.Logf("STDOUT:\n%s", stdout) - t.Logf("STDERR:\n%s", stderr) + assert.Nil(t, exitErr, "Enum command should succeed") + assert.Greater(t, len(getRequests()), before, "Should make API requests") + output := stdout + stderr + assert.Contains(t, output, "testuser") + assert.Contains(t, output, "repo1") + assert.Contains(t, output, "repo2") + assert.Contains(t, output, "my-org") + }) + } } // TestGitea_APIErrors tests various API error responses diff --git a/tests/e2e/gitea/secrets/secrets_test.go b/tests/e2e/gitea/secrets/secrets_test.go index a6c3d5de..0e700561 100644 --- a/tests/e2e/gitea/secrets/secrets_test.go +++ b/tests/e2e/gitea/secrets/secrets_test.go @@ -72,28 +72,28 @@ func TestGiteaSecrets_Success(t *testing.T) { }) defer cleanup() - stdout, stderr, exitErr := testutil.RunCLI(t, []string{ - "gitea", "secrets", - "--url", server.URL, - "--token", "test-token", - }, nil, 10*time.Second) - - assert.Nil(t, exitErr, "Secrets command should succeed") - - output := stdout + stderr - t.Logf("Output:\n%s", output) - - // Verify expected API calls were made - requests := getRequests() - assert.True(t, len(requests) >= 4, "Should make multiple API requests") - - // Verify output contains secrets - assert.Contains(t, output, "ORG_SECRET_1", "Should output org secret 1") - assert.Contains(t, output, "ORG_SECRET_2", "Should output org secret 2") - assert.Contains(t, output, "REPO_SECRET_1", "Should output repo secret 1") - assert.Contains(t, output, "REPO_SECRET_2", "Should output repo secret 2") - assert.Contains(t, output, "test-org", "Should output organization name") - assert.Contains(t, output, "test-repo", "Should output repository name") + for _, mode := range []string{"direct", "explicit proxy"} { + t.Run(mode, func(t *testing.T) { + args := []string{"gitea", "secrets", "--token", "test-token"} + if mode == "explicit proxy" { + args = append(args, "--url", "http://gitea.invalid", "--proxy", server.URL, "--ignore-proxy") + } else { + args = append(args, "--url", server.URL) + } + before := len(getRequests()) + stdout, stderr, exitErr := testutil.RunCLI(t, args, nil, 10*time.Second) + + assert.Nil(t, exitErr, "Secrets command should succeed") + assert.GreaterOrEqual(t, len(getRequests())-before, 4, "Should make multiple API requests") + output := stdout + stderr + assert.Contains(t, output, "ORG_SECRET_1", "Should output org secret 1") + assert.Contains(t, output, "ORG_SECRET_2", "Should output org secret 2") + assert.Contains(t, output, "REPO_SECRET_1", "Should output repo secret 1") + assert.Contains(t, output, "REPO_SECRET_2", "Should output repo secret 2") + assert.Contains(t, output, "test-org", "Should output organization name") + assert.Contains(t, output, "test-repo", "Should output repository name") + }) + } } func TestGiteaSecrets_OrgPagination(t *testing.T) { diff --git a/tests/e2e/gitea/variables/variables_test.go b/tests/e2e/gitea/variables/variables_test.go index adcf5ebd..8c26731a 100644 --- a/tests/e2e/gitea/variables/variables_test.go +++ b/tests/e2e/gitea/variables/variables_test.go @@ -100,6 +100,23 @@ func TestGiteaVariables_Success(t *testing.T) { assert.Contains(t, output, "REPO_VAR_2", "Should output repo variable 2") assert.Contains(t, output, "test-org", "Should output organization name") assert.Contains(t, output, "test-repo", "Should output repository name") + + t.Run("explicit proxy", func(t *testing.T) { + before := len(getRequests()) + stdout, stderr, exitErr := testutil.RunCLI(t, []string{ + "gitea", "variables", + "--url", "http://gitea.invalid", + "--token", "test-token", + "--proxy", server.URL, + "--ignore-proxy", + }, nil, 10*time.Second) + assert.Nil(t, exitErr, "Variables command should succeed through the proxy") + assert.GreaterOrEqual(t, len(getRequests())-before, 4) + output := stdout + stderr + for _, value := range []string{"ORG_VAR_1", "ORG_VAR_2", "REPO_VAR_1", "REPO_VAR_2", "org_value_1", "repo_value_1"} { + assert.Contains(t, output, value) + } + }) } func TestGiteaVariables_Pagination(t *testing.T) { diff --git a/tests/e2e/gitea/vuln/vuln_test.go b/tests/e2e/gitea/vuln/vuln_test.go index 52e5c01a..f9a76af9 100644 --- a/tests/e2e/gitea/vuln/vuln_test.go +++ b/tests/e2e/gitea/vuln/vuln_test.go @@ -109,6 +109,47 @@ func TestGiteaVuln_MissingToken(t *testing.T) { assert.Contains(t, stdout, "required configuration missing", "Should mention missing required configuration") } +func TestGiteaVuln_Proxy(t *testing.T) { + server, getRequests, cleanup := testutil.StartMockServerWithRecording(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api/v1/version": + _, _ = w.Write([]byte(`{"version":"1.20.0"}`)) + case "/nist": + assert.Contains(t, r.URL.Query().Get("cpeName"), ":gitea:1.20.0:") + _, _ = w.Write([]byte(`{"totalResults":1,"vulnerabilities":[{"cve":{"id":"CVE-2023-1234","descriptions":[{"value":"Test vulnerability"}]}}]}`)) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + defer cleanup() + + stdout, stderr, exitErr := testutil.RunCLI(t, []string{ + "gitea", "vuln", + "--url", "http://gitea.invalid", + "--token", "test-token", + "--proxy", server.URL, + "--ignore-proxy", + }, []string{"PIPELEEK_NIST_BASE_URL=http://nist.invalid/nist"}, 15*time.Second) + + assert.Nil(t, exitErr) + output := stdout + stderr + assert.Contains(t, output, "1.20.0") + assert.Contains(t, output, "CVE-2023-1234") + assert.NotContains(t, output, "Failed creating Gitea client") + var versionSeen, nistSeen bool + for _, request := range getRequests() { + switch request.Path { + case "/api/v1/version": + versionSeen = true + case "/nist": + nistSeen = true + } + } + assert.True(t, versionSeen, "Gitea version lookup must use the proxy") + assert.True(t, nistSeen, "NIST lookup must use the proxy") +} + func TestGiteaVuln_MissingGitea(t *testing.T) { stdout, _, exitErr := testutil.RunCLI(t, []string{ "gitea", "vuln",