Skip to content

Commit df825ff

Browse files
authored
Merge branch 'main' into INS-170-Unify-JDBC-URL-parsing-across-detectors-and-analyzers
2 parents 3ec6860 + 05cccb5 commit df825ff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2915
-854
lines changed

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pkg/giturl/ @trufflesecurity/Scanning @trufflesecurity/OSS
2727
pkg/handlers/ @trufflesecurity/Scanning @trufflesecurity/OSS
2828
pkg/iobuf/ @trufflesecurity/Scanning @trufflesecurity/OSS
2929
pkg/sanitizer/ @trufflesecurity/Scanning @trufflesecurity/OSS
30-
proto/ @trufflesecurity/Scanning @trufflesecurity/OSS
30+
proto/ @trufflesecurity/Scanning @trufflesecurity/Integrations
3131

3232
# OSS
3333
pkg/detectors/ @trufflesecurity/OSS

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ var (
8888
// Add feature flags
8989
forceSkipBinaries = cli.Flag("force-skip-binaries", "Force skipping binaries.").Bool()
9090
forceSkipArchives = cli.Flag("force-skip-archives", "Force skipping archives.").Bool()
91+
gitCloneTimeout = cli.Flag("git-clone-timeout", "Maximum time to spend cloning a repository, as a duration.").Hidden().Duration()
9192
skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional references.").Bool()
9293
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()
9394

@@ -451,6 +452,10 @@ func run(state overseer.State) {
451452
feature.ForceSkipArchives.Store(true)
452453
}
453454

455+
if gitCloneTimeout != nil {
456+
feature.GitCloneTimeoutDuration.Store(int64(*gitCloneTimeout))
457+
}
458+
454459
if *skipAdditionalRefs {
455460
feature.SkipAdditionalRefs.Store(true)
456461
}

pkg/common/http.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ func (t *InstrumentedTransport) RoundTrip(req *http.Request) (*http.Response, er
133133
}
134134

135135
if resp != nil {
136-
// record latency and increment counter for non-200 status code
137-
recordHTTPResponse(sanitizedURL, resp.StatusCode, duration.Seconds())
136+
// record latency, response size and increment counter for non-200 status code
137+
recordHTTPResponse(sanitizedURL, resp.StatusCode, duration.Seconds(), resp.ContentLength)
138138
}
139139

140140
return resp, err
@@ -198,7 +198,7 @@ func WithRetryWaitMax(wait time.Duration) ClientOption {
198198
func PinnedRetryableHttpClient() *http.Client {
199199
httpClient := retryablehttp.NewClient()
200200
httpClient.Logger = nil
201-
httpClient.HTTPClient.Transport = NewCustomTransport(&http.Transport{
201+
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(&http.Transport{
202202
TLSClientConfig: &tls.Config{
203203
RootCAs: PinnedCertPool(),
204204
},
@@ -212,15 +212,15 @@ func PinnedRetryableHttpClient() *http.Client {
212212
IdleConnTimeout: 90 * time.Second,
213213
TLSHandshakeTimeout: 10 * time.Second,
214214
ExpectContinueTimeout: 1 * time.Second,
215-
})
215+
}))
216216
return httpClient.StandardClient()
217217
}
218218

219219
func RetryableHTTPClient(opts ...ClientOption) *http.Client {
220220
httpClient := retryablehttp.NewClient()
221221
httpClient.RetryMax = 3
222222
httpClient.Logger = nil
223-
httpClient.HTTPClient.Transport = NewCustomTransport(nil)
223+
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(nil))
224224

225225
for _, opt := range opts {
226226
opt(httpClient)
@@ -234,7 +234,7 @@ func RetryableHTTPClientTimeout(timeOutSeconds int64, opts ...ClientOption) *htt
234234
httpClient.RetryMax = 3
235235
httpClient.Logger = nil
236236
httpClient.HTTPClient.Timeout = time.Duration(timeOutSeconds) * time.Second
237-
httpClient.HTTPClient.Transport = NewCustomTransport(nil)
237+
httpClient.HTTPClient.Transport = NewInstrumentedTransport(NewCustomTransport(nil))
238238

239239
for _, opt := range opts {
240240
opt(httpClient)

pkg/common/http_metrics.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ var (
3939
},
4040
[]string{"url", "status_code"},
4141
)
42+
43+
httpResponseBodySizeBytes = promauto.NewHistogramVec(
44+
prometheus.HistogramOpts{
45+
Namespace: MetricsNamespace,
46+
Subsystem: "http_client",
47+
Name: "response_body_size_bytes",
48+
Help: "Size of HTTP response bodies in bytes, labeled by URL.",
49+
Buckets: prometheus.ExponentialBuckets(100, 10, 5), // [100B, 1KB, 10KB, 100KB, 1MB]
50+
},
51+
[]string{"url"},
52+
)
4253
)
4354

4455
// sanitizeURL sanitizes a URL to avoid high cardinality metrics.
@@ -92,14 +103,19 @@ func recordHTTPRequest(sanitizedURL string) {
92103
}
93104

94105
// recordHTTPResponse records metrics for an HTTP response.
95-
func recordHTTPResponse(sanitizedURL string, statusCode int, durationSeconds float64) {
106+
func recordHTTPResponse(sanitizedURL string, statusCode int, durationSeconds float64, contentLength int64) {
96107
// Record latency
97108
httpRequestDuration.WithLabelValues(sanitizedURL).Observe(durationSeconds)
98109

99110
// Record non-200 responses
100111
if statusCode != 200 {
101112
httpNon200ResponsesTotal.WithLabelValues(sanitizedURL, strconv.Itoa(statusCode)).Inc()
102113
}
114+
115+
// Record response body size if known
116+
if contentLength >= 0 {
117+
httpResponseBodySizeBytes.WithLabelValues(sanitizedURL).Observe(float64(contentLength))
118+
}
103119
}
104120

105121
// recordNetworkError records metrics for failed HTTP response

pkg/common/http_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,73 @@ func TestSaneHttpClientMetrics(t *testing.T) {
405405
}
406406
}
407407

408+
func TestRetryableHttpClientMetrics(t *testing.T) {
409+
// Create a test server that returns different status codes
410+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
411+
switch r.URL.Path {
412+
case "/success":
413+
w.WriteHeader(http.StatusOK)
414+
_, _ = w.Write([]byte("success"))
415+
case "/error":
416+
w.WriteHeader(http.StatusInternalServerError)
417+
_, _ = w.Write([]byte("error"))
418+
case "/notfound":
419+
w.WriteHeader(http.StatusNotFound)
420+
_, _ = w.Write([]byte("not found"))
421+
default:
422+
w.WriteHeader(http.StatusOK)
423+
_, _ = w.Write([]byte("default"))
424+
}
425+
}))
426+
defer server.Close()
427+
428+
// Create a RetryableHttpClient
429+
client := RetryableHTTPClient()
430+
431+
testCases := []struct {
432+
name string
433+
path string
434+
expectedStatusCode int
435+
}{
436+
{
437+
name: "successful request",
438+
path: "/success",
439+
expectedStatusCode: 200,
440+
},
441+
{
442+
name: "not found request",
443+
path: "/notfound",
444+
expectedStatusCode: 404,
445+
},
446+
}
447+
448+
for _, tc := range testCases {
449+
t.Run(tc.name, func(t *testing.T) {
450+
var requestURL string
451+
if strings.HasPrefix(tc.path, "http") {
452+
requestURL = tc.path
453+
} else {
454+
requestURL = server.URL + tc.path
455+
}
456+
457+
// Get initial metric values
458+
sanitizedURL := sanitizeURL(requestURL)
459+
initialRequestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
460+
461+
// Make the request
462+
resp, err := client.Get(requestURL)
463+
464+
require.NoError(t, err)
465+
defer resp.Body.Close()
466+
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
467+
468+
// Check that request counter was incremented
469+
requestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
470+
assert.Equal(t, initialRequestsTotal+1, requestsTotal)
471+
})
472+
}
473+
}
474+
408475
func TestInstrumentedTransport(t *testing.T) {
409476
// Create a mock transport that we can control
410477
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

pkg/detectors/copper/copper.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package copper
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
58
"net/http"
69
"strings"
710

@@ -27,6 +30,11 @@ var (
2730
idPat = regexp.MustCompile(`\b([a-z0-9]{4,25}@[a-zA-Z0-9]{2,12}.[a-zA-Z0-9]{2,6})\b`)
2831
)
2932

33+
type UserApiResponse struct {
34+
Id int `json:"id"`
35+
Email string `json:"email"`
36+
}
37+
3038
// Keywords are used for efficiently pre-filtering chunks.
3139
// Use identifiers in the secret preferably, or the provider name.
3240
func (s Scanner) Keywords() []string {
@@ -52,26 +60,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
5260
}
5361

5462
if verify {
55-
56-
payload := strings.NewReader(`{
57-
"page_size": 25,
58-
"sort_by": "name"
59-
}`)
60-
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.copper.com/developer_api/v1/tasks/search", payload)
61-
if err != nil {
62-
continue
63-
}
64-
req.Header.Add("X-PW-AccessToken", resMatch)
65-
req.Header.Add("X-PW-Application", "developer_api")
66-
req.Header.Add("X-PW-UserEmail", resIdMatch)
67-
req.Header.Add("Content-Type", "application/json")
68-
res, err := client.Do(req)
69-
if err == nil {
70-
defer res.Body.Close()
71-
if res.StatusCode >= 200 && res.StatusCode < 300 {
72-
s1.Verified = true
73-
}
74-
}
63+
isVerified, verificationErr := verifyCopper(ctx, client, resIdMatch, resMatch)
64+
s1.Verified = isVerified
65+
s1.SetVerificationError(verificationErr, resMatch)
7566
}
7667

7768
results = append(results, s1)
@@ -83,6 +74,54 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
8374
return results, nil
8475
}
8576

77+
func verifyCopper(ctx context.Context, client *http.Client, email, apiKey string) (bool, error) {
78+
req, err := http.NewRequestWithContext(
79+
ctx,
80+
http.MethodGet,
81+
"https://api.copper.com/developer_api/v1/users/me",
82+
http.NoBody,
83+
)
84+
if err != nil {
85+
return false, err
86+
}
87+
req.Header.Add("X-PW-AccessToken", apiKey)
88+
req.Header.Add("X-PW-Application", "developer_api")
89+
req.Header.Add("X-PW-UserEmail", email)
90+
req.Header.Add("Content-Type", "application/json")
91+
res, err := client.Do(req)
92+
if err != nil {
93+
return false, err
94+
}
95+
defer func() {
96+
_, _ = io.Copy(io.Discard, res.Body)
97+
_ = res.Body.Close()
98+
}()
99+
100+
switch res.StatusCode {
101+
case http.StatusOK:
102+
respBytes, err := io.ReadAll(res.Body)
103+
if err != nil {
104+
return false, err
105+
}
106+
107+
var respBody UserApiResponse
108+
if err := json.Unmarshal(respBytes, &respBody); err != nil {
109+
return false, err
110+
}
111+
112+
// strict verification with email in credentials
113+
if respBody.Email == email {
114+
return true, nil
115+
}
116+
117+
return false, fmt.Errorf("email mismatch in verification response")
118+
case http.StatusUnauthorized:
119+
return false, nil
120+
default:
121+
return false, fmt.Errorf("unexpected status code :%d", res.StatusCode)
122+
}
123+
}
124+
86125
func (s Scanner) Type() detectorspb.DetectorType {
87126
return detectorspb.DetectorType_Copper
88127
}

pkg/detectors/copper/copper_integration_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"testing"
1010
"time"
1111

12-
"github.com/kylelemons/godebug/pretty"
13-
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
1414
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1515
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1616
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -96,9 +96,11 @@ func TestCopper_FromChunk(t *testing.T) {
9696
t.Fatalf("no raw secret present: \n %+v", got[i])
9797
}
9898
got[i].Raw = nil
99+
got[i].RawV2 = nil
99100
}
100-
if diff := pretty.Compare(got, tt.want); diff != "" {
101-
t.Errorf("Copper.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
101+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
102+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
103+
t.Errorf("Abstract.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
102104
}
103105
})
104106
}

pkg/detectors/ftp/ftp.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,15 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
7878
}
7979

8080
if verify {
81-
timeout := s.verificationTimeout
82-
if timeout == 0 {
81+
var timeout time.Duration
82+
if s.verificationTimeout > 0 {
83+
// Use to configure a simulate timeout for interation tests
84+
timeout = s.verificationTimeout
85+
} else if dl, ok := ctx.Deadline(); ok {
86+
// Here we assign the remaining time before our context expires
87+
// This help us cater the timelimit set through --detector-timeout flag
88+
timeout = time.Until(dl)
89+
} else {
8390
timeout = defaultVerificationTimeout
8491
}
8592
verificationErr := verifyFTP(timeout, parsedURL)
@@ -126,6 +133,9 @@ func verifyFTP(timeout time.Duration, u *url.URL) error {
126133
return err
127134
}
128135

136+
defer func() {
137+
_ = c.Quit()
138+
}()
129139
password, _ := u.User.Password()
130140
return c.Login(u.User.Username(), password)
131141
}

pkg/detectors/ftp/ftp_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func TestFTP_FromChunk(t *testing.T) {
149149
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
150150
}
151151
}
152-
opts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError")
152+
opts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret")
153153
if diff := cmp.Diff(got, tt.want, opts); diff != "" {
154154
t.Errorf("FTP.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
155155
}

0 commit comments

Comments
 (0)