diff --git a/pkg/detectors/detectors.go b/pkg/detectors/detectors.go index 1be6d1c1f6e3..267a4f2cae1b 100644 --- a/pkg/detectors/detectors.go +++ b/pkg/detectors/detectors.go @@ -4,15 +4,20 @@ import ( "context" "crypto/rand" "errors" + "io" "math/big" + "net/http" "net/url" + "strconv" "strings" "unicode" + "github.com/cespare/xxhash/v2" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" "github.com/trufflesecurity/trufflehog/v3/pkg/sources" + "golang.org/x/sync/singleflight" ) // Detector defines an interface for scanning for and verifying secrets. @@ -316,3 +321,45 @@ func ParseURLAndStripPathAndParams(u string) (*url.URL, error) { parsedURL.RawQuery = "" return parsedURL, nil } + +type VerificationResult struct { + StatusCode int + Body []byte +} + +var verificationGroup = new(singleflight.Group) + +func VerificationRequest(identifier string, request *http.Request, client *http.Client) (*VerificationResult, error) { + result, err, _ := verificationGroup.Do(identifier, func() (interface{}, error) { + resp, err := client.Do(request) + if err != nil { + return nil, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return &VerificationResult{ + StatusCode: resp.StatusCode, + Body: bodyBytes, + }, nil + }) + if err != nil { + return nil, err + } + + return result.(*VerificationResult), nil +} + +// ComputeXXHash computes the XXHash of the given secret and returns it as a string. +// This hash can be used as a unique identifier for caching purposes. +func ComputeXXHash(secret []byte) string { + return strconv.FormatUint(xxhash.Sum64(secret), 10) +} diff --git a/pkg/detectors/meraki/meraki.go b/pkg/detectors/meraki/meraki.go index e326e84ced34..2e5c9099ca91 100644 --- a/pkg/detectors/meraki/meraki.go +++ b/pkg/detectors/meraki/meraki.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" regexp "github.com/wasilibs/go-re2" @@ -58,13 +57,13 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result dataStr := string(data) // uniqueMatches will hold unique match values and ensure we only process unique matches found in the data string - var uniqueMatches = make(map[string]struct{}) + var matches = make([]string, 0) for _, match := range apiKey.FindAllStringSubmatch(dataStr, -1) { - uniqueMatches[match[1]] = struct{}{} + matches = append(matches, match[1]) } - for match := range uniqueMatches { + for _, match := range matches { s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_Meraki, Raw: []byte(match), @@ -110,20 +109,16 @@ func verifyMerakiApiKey(ctx context.Context, client *http.Client, match string) // set the required auth header req.Header.Set("X-Cisco-Meraki-API-Key", match) - resp, err := client.Do(req) + result, err := detectors.VerificationRequest(match, req, client) if err != nil { return nil, false, err } - defer func() { - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() - switch resp.StatusCode { + switch result.StatusCode { case http.StatusOK: // in case token is verified, capture the organization id's and name which are accessible via token. var organizations []merakiOrganizations - if err = json.NewDecoder(resp.Body).Decode(&organizations); err != nil { + if err = json.Unmarshal(result.Body, &organizations); err != nil { return nil, false, err } @@ -131,6 +126,6 @@ func verifyMerakiApiKey(ctx context.Context, client *http.Client, match string) case http.StatusUnauthorized: return nil, false, nil default: - return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return nil, false, fmt.Errorf("unexpected status code: %d", result.StatusCode) } } diff --git a/pkg/output/plain.go b/pkg/output/plain.go index 62c08c506fb5..2a5e8ceaa002 100644 --- a/pkg/output/plain.go +++ b/pkg/output/plain.go @@ -57,9 +57,11 @@ func (p *PlainPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) yellowPrinter.Printf("Verification issue: %s\n", out.VerificationError) } } + if r.VerificationFromCache { - cyanPrinter.Print("(Verification info cached)\n") + cyanPrinter.Print("(🔍 Using cached verification)\n") } + printer.Printf("Detector Type: %s\n", out.DetectorType) printer.Printf("Decoder Type: %s\n", out.DecoderType) printer.Printf("Raw result: %s\n", whitePrinter.Sprint(out.Raw))