Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ coverage.html
.vscode
results.json
pipeleek.yaml
pipeleek_test_build
4 changes: 4 additions & 0 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,7 @@ pipeleek config gen
# Use trace logging to see which keys are loaded
pipeleek --log-level=trace gl enum
```

## HTTP Client Settings

See [Using Pipeleek with Proxies](proxying.md) for proxy, TLS, and timeout configuration flags.
45 changes: 43 additions & 2 deletions docs/introduction/proxying.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ SOCKS5 can be used as well.
HTTP_PROXY=socks5://127.0.0.1:1080 pipeleek gl scan -u https://gitlab.internal.company.com -t glpat-xxxxx
```

### Using the `--proxy` Flag

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:

```bash
# HTTP proxy
pipeleek --proxy http://127.0.0.1:8080 gl scan -u https://gitlab.com -t glpat-xxxxx

# SOCKS5 proxy
pipeleek --proxy socks5://127.0.0.1:1080 gl scan -u https://gitlab.internal.company.com -t glpat-xxxxx
```

## Ignoring Proxy Configuration

In some environments, `HTTP_PROXY` may be set system-wide but you don't want Pipeleek to use it. Use the `--ignore-proxy` flag to bypass proxy detection:
Expand All @@ -40,6 +52,35 @@ In some environments, `HTTP_PROXY` may be set system-wide but you don't want Pip
HTTP_PROXY=http://127.0.0.1:8080 pipeleek --ignore-proxy gl scan -u https://gitlab.com -t glpat-xxxxx
```

## TLS/SSL
## TLS Certificate Verification

By default, Pipeleek skips TLS certificate verification so that self-hosted instances with self-signed certificates work out of the box. Use `--tls-verification` to enforce certificate validation:

```bash
pipeleek --tls-verification gl scan --token glpat-xxx --url https://gitlab.example.com
```

## HTTP Timeout

Use `--http-timeout` to set a per-request timeout. This is useful when scanning slow or unreliable targets:

```bash
pipeleek --http-timeout 30s gl scan --token glpat-xxx --url https://gitlab.example.com
```

Accepts any Go duration string: `30s`, `2m`, `90s`, etc. The default is no timeout.

> **Note:** `--http-timeout` applies to platforms that use `GetPipeleekHTTPClient` (GitLab, Gitea, GitHub, Jenkins, CircleCI, NIST, and rule downloads). Bitbucket and Azure DevOps inject only the transport via `GetPipeleekTransport` and are not affected by this flag.

## Platform Scope

All proxy and TLS flags share a single HTTP transport injected into every platform client:

| Flag | Default | Applies to |
|---|---|---|
| `--tls-verification` | `false` | All platforms |
| `--ignore-proxy` | `false` | All platforms |
| `--proxy <url>` | _(none)_ | All platforms |
| `--http-timeout <duration>` | _(no timeout)_ | GitLab, Gitea, Jenkins, CircleCI, NIST (not Bitbucket/DevOps) |

Pipeleek automatically skips TLS certificate verification (required for self signed certificates).
> **Note:** The GitHub SDK uses a dedicated rate-limit transport (`go-github-ratelimit`) that cannot be replaced. TLS and proxy settings still apply to GitHub via the shared transport layer.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ require (
github.com/google/go-querystring v1.2.0 // indirect
github.com/h2non/filetype v1.1.3
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down
22 changes: 15 additions & 7 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,22 @@ var (
setGlobalLogLevel(cmd)
loadConfigFile(cmd)
httpclient.SetIgnoreProxy(IgnoreProxy)
httpclient.SetInsecureSkipVerify(!TLSVerification)
httpclient.SetProxy(Proxy)
httpclient.SetHTTPTimeout(HTTPTimeout)
go logging.ShortcutListeners(nil)
},
}
JsonLogoutput bool
LogFile string
LogColor bool
LogDebug bool
LogLevel string
IgnoreProxy bool
ConfigFile string
JsonLogoutput bool
LogFile string
LogColor bool
LogDebug bool
LogLevel string
IgnoreProxy bool
ConfigFile string
TLSVerification bool
Proxy string
HTTPTimeout time.Duration
// runLogFileHandle holds the file handle when logging to a file is enabled
runLogFileHandle *os.File
)
Expand Down Expand Up @@ -91,6 +97,8 @@ func init() {
rootCmd.PersistentFlags().StringVar(&LogLevel, "log-level", "", "Set log level globally (debug, info, warn, error). Example: --log-level=warn")
rootCmd.PersistentFlags().BoolVar(&LogColor, "color", true, "Enable colored log output (auto-disabled when using --logfile)")
rootCmd.PersistentFlags().BoolVar(&IgnoreProxy, "ignore-proxy", false, "Ignore HTTP_PROXY environment variable")
rootCmd.PersistentFlags().StringVar(&Proxy, "proxy", "", "Proxy URL, e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080 (takes precedence over HTTP_PROXY)")
rootCmd.PersistentFlags().DurationVar(&HTTPTimeout, "http-timeout", 0, "HTTP request timeout, e.g. 30s or 2m (default: no timeout)")

// Set custom version template to show detailed version info
rootCmd.SetVersionTemplate(`{{.Version}}
Expand Down
3 changes: 2 additions & 1 deletion pkg/bitbucket/scan/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"
"time"

"github.com/CompassSecurity/pipeleek/pkg/httpclient"
"github.com/rs/zerolog/log"

"resty.dev/v3"
Expand Down Expand Up @@ -38,7 +39,7 @@ func NewClient(username string, password string, bitBucketCookie string, baseURL
}
internalBase := parsedBase.Scheme + "://" + internalHost + "/!api"

client := *resty.New().SetBasicAuth(username, password).SetRedirectPolicy(resty.FlexibleRedirectPolicy(5))
client := *httpclient.GetPipeleekHTTPClient("", nil, nil).SetBasicAuth(username, password).SetRedirectPolicy(resty.FlexibleRedirectPolicy(5))
if len(bitBucketCookie) > 0 {
jar, _ := cookiejar.New(nil)
// set cookie on the internal host root so requests to internal endpoints include it
Expand Down
96 changes: 96 additions & 0 deletions pkg/bitbucket/scan/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package scan

import (
"crypto/tls"
"net/http"
"net/url"
"testing"

"github.com/CompassSecurity/pipeleek/pkg/httpclient"
)

func TestNewClient_UsesPipeleekTransport(t *testing.T) {
// Arrange: configure a distinct InsecureSkipVerify value so we can detect it.
httpclient.SetInsecureSkipVerify(false)
t.Cleanup(func() { httpclient.SetInsecureSkipVerify(true) })

c := NewClient("user", "pass", "", "https://api.bitbucket.org/2.0")

tr, ok := c.Client.Transport().(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport as client transport, got %T", c.Client.Transport())
}
if tr.TLSClientConfig == nil {
t.Fatal("expected TLSClientConfig to be set on transport")
}
if tr.TLSClientConfig.InsecureSkipVerify {
t.Error("expected InsecureSkipVerify=false to be reflected from Pipeleek global config")
}
}

func TestNewClient_DefaultBaseURL(t *testing.T) {
c := NewClient("user", "pass", "", "")
if c.BaseURL != "https://api.bitbucket.org/2.0" {
t.Errorf("unexpected default BaseURL: %s", c.BaseURL)
}
}

func TestNewClient_InternalBaseURLDerivedFromDefault(t *testing.T) {
c := NewClient("user", "pass", "", "")
if c.InternalBaseURL != "https://bitbucket.org/!api" {
t.Errorf("unexpected InternalBaseURL: %s", c.InternalBaseURL)
}
}

func TestNewClient_CookieJarSetWhenCookieProvided(t *testing.T) {
c := NewClient("user", "pass", "tok123", "https://api.bitbucket.org/2.0")
if c.Client.CookieJar() == nil {
t.Error("expected cookie jar to be set when cookie is provided")
}
}

func TestNewClient_NoCookieSetWhenNoCookie(t *testing.T) {
// Resty v3 always creates a default cookie jar. When no BitBucket cookie is
// provided, no cloud.session.token cookie should be present in the jar.
c := NewClient("user", "pass", "", "https://api.bitbucket.org/2.0")
jar := c.Client.CookieJar()
if jar == nil {
// If Resty ever stops setting a default jar this test still passes.
return
}
u, _ := url.Parse("https://bitbucket.org/!api")
for _, ck := range jar.Cookies(u) {
if ck.Name == "cloud.session.token" {
t.Error("cloud.session.token should not be set in jar when no cookie provided")
}
}
}

func TestNewClient_TLSReflectsGlobalConfig(t *testing.T) {
tests := []struct {
name string
insecureSkipVerify bool
}{
{"skip=true", true},
{"skip=false", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpclient.SetInsecureSkipVerify(tt.insecureSkipVerify)
t.Cleanup(func() { httpclient.SetInsecureSkipVerify(true) })

c := NewClient("u", "p", "", "https://api.bitbucket.org/2.0")
rawTr, ok := c.Client.Transport().(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport, got %T", c.Client.Transport())
}
got := rawTr.TLSClientConfig.InsecureSkipVerify
if got != tt.insecureSkipVerify {
t.Errorf("InsecureSkipVerify: want %v, got %v", tt.insecureSkipVerify, got)
}
})
}
}

// ensure the helper compiles even when tls package is not directly used in tests above
var _ = (*tls.Config)(nil)
3 changes: 2 additions & 1 deletion pkg/circle/scan/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/CompassSecurity/pipeleek/pkg/format"
"github.com/CompassSecurity/pipeleek/pkg/httpclient"
"github.com/CompassSecurity/pipeleek/pkg/logging"
"github.com/CompassSecurity/pipeleek/pkg/scan/logline"
"github.com/CompassSecurity/pipeleek/pkg/scan/result"
Expand Down Expand Up @@ -567,7 +568,7 @@ func InitializeOptions(input InitializeOptionsInput) (ScanOptions, error) {
return ScanOptions{}, err
}

httpClient := &http.Client{Timeout: 45 * time.Second}
httpClient := httpclient.GetPipeleekHTTPClient("", nil, nil).Client()
apiClient := newCircleAPIClient(baseURL, input.Token, httpClient)

if len(projects) == 0 {
Expand Down
3 changes: 2 additions & 1 deletion pkg/devops/scan/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"path"
"strconv"

"github.com/CompassSecurity/pipeleek/pkg/httpclient"
"github.com/rs/zerolog/log"

"resty.dev/v3"
Expand All @@ -22,7 +23,7 @@ func NewClient(username string, password string, baseURL string) AzureDevOpsApiC
baseURL = "https://dev.azure.com"
}
bbClient := AzureDevOpsApiClient{
Client: *resty.New().SetBasicAuth(username, password).SetRedirectPolicy(resty.FlexibleRedirectPolicy(5)),
Client: *httpclient.GetPipeleekHTTPClient("", nil, nil).SetBasicAuth(username, password).SetRedirectPolicy(resty.FlexibleRedirectPolicy(5)),
BaseURL: baseURL,
VsspsURL: "https://app.vssps.visualstudio.com",
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/devops/scan/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package scan

import (
"net/http"
"testing"

"github.com/CompassSecurity/pipeleek/pkg/httpclient"
)

func TestAzureDevOpsNewClient_UsesPipeleekTransport(t *testing.T) {
httpclient.SetInsecureSkipVerify(false)
t.Cleanup(func() { httpclient.SetInsecureSkipVerify(true) })

c := NewClient("user", "pass", "https://dev.azure.com")

tr, ok := c.Client.Transport().(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport as client transport, got %T", c.Client.Transport())
}
if tr.TLSClientConfig == nil {
t.Fatal("expected TLSClientConfig to be set on transport")
}
if tr.TLSClientConfig.InsecureSkipVerify {
t.Error("expected InsecureSkipVerify=false to be reflected from Pipeleek global config")
}
}

func TestAzureDevOpsNewClient_DefaultBaseURL(t *testing.T) {
c := NewClient("user", "pass", "")
if c.BaseURL != "https://dev.azure.com" {
t.Errorf("unexpected default BaseURL: %s", c.BaseURL)
}
}

func TestAzureDevOpsNewClient_VsspsURLIsFixed(t *testing.T) {
c := NewClient("user", "pass", "https://dev.azure.com")
if c.VsspsURL != "https://app.vssps.visualstudio.com" {
t.Errorf("unexpected VsspsURL: %s", c.VsspsURL)
}
}

func TestAzureDevOpsNewClient_TLSReflectsGlobalConfig(t *testing.T) {
tests := []struct {
name string
insecureSkipVerify bool
}{
{"skip=true", true},
{"skip=false", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpclient.SetInsecureSkipVerify(tt.insecureSkipVerify)
t.Cleanup(func() { httpclient.SetInsecureSkipVerify(true) })

c := NewClient("u", "p", "")
rawTr, ok := c.Client.Transport().(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport, got %T", c.Client.Transport())
}
got := rawTr.TLSClientConfig.InsecureSkipVerify
if got != tt.insecureSkipVerify {
t.Errorf("InsecureSkipVerify: want %v, got %v", tt.insecureSkipVerify, got)
}
})
}
}
Loading
Loading