From 5a0f877b8afbe5ed69b5f13536eed05d259872ff Mon Sep 17 00:00:00 2001 From: Shahzad Haider <76992801+shahzadhaider1@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:52:58 +0500 Subject: [PATCH] Enigma detector enhance verification to prevent false verified findings (#5011) * enhance the verification in the enigma detector * feat(enigma): gate detector behind feature flag --- main.go | 1 + pkg/detectors/enigma/enigma.go | 64 ++++++++++++++----- .../enigma/enigma_integration_test.go | 8 ++- pkg/engine/defaults/defaults.go | 2 + pkg/engine/defaults/defaults_test.go | 1 + pkg/feature/feature.go | 1 + 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index 8022145fe..8f2c66b2e 100644 --- a/main.go +++ b/main.go @@ -532,6 +532,7 @@ func run(state overseer.State, logSync func() error) { feature.PineconeDetectorEnabled.Store(true) feature.CloudinaryDetectorEnabled.Store(true) feature.GitLabOAuthDetectorEnabled.Store(true) + feature.EnigmaDetectorEnabled.Store(true) conf := &config.Config{} if *configFilename != "" { diff --git a/pkg/detectors/enigma/enigma.go b/pkg/detectors/enigma/enigma.go index df6f175d7..f8a9e76b5 100644 --- a/pkg/detectors/enigma/enigma.go +++ b/pkg/detectors/enigma/enigma.go @@ -2,6 +2,8 @@ package enigma import ( "context" + "fmt" + "io" "net/http" "strings" @@ -33,7 +35,6 @@ func (s Scanner) Keywords() []string { // FromData will find and optionally verify Enigma secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) - matches := keyPat.FindAllStringSubmatch(dataStr, -1) for _, match := range matches { @@ -46,20 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - payload := strings.NewReader(`{"name":"Enigma Technologies, Inc.","person":{"first_name":"","last_name":""},"address":{"street_address1":"245 5th Ave","street_address2":"","city":"New York","state":"NY","postal_code":"10016"}}`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://api.enigma.com/businesses/match", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("x-api-key", resMatch) - res, err := client.Do(req) - if err == nil { - defer func() { _ = res.Body.Close() }() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyKey(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -68,10 +58,52 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +// verifyKey checks an Enigma API key against the GraphQL endpoint. +// +// The query selects only `__typename`, which the GraphQL spec guarantees on +// every root type, so verification does not depend on Enigma's evolving schema +// or any specific entity id. Authentication is driven by the HTTP status code: +// 200 means the key is valid, 401 mean it is not. +// +// A 200 is additionally guarded by a JSON Content-Type check: server could return +// a 200 HTML block page, which we must not mistake for a live key. +func verifyKey(ctx context.Context, client *http.Client, key string) (bool, error) { + payload := strings.NewReader(`{"query":"{ __typename }"}`) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.enigma.com/graphql", payload) + if err != nil { + return false, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("x-api-key", key) + + res, err := client.Do(req) + if err != nil { + return false, err + } + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + switch res.StatusCode { + case http.StatusOK: + if contentType := res.Header.Get("Content-Type"); !strings.Contains(contentType, "application/json") { + return false, fmt.Errorf("got HTTP 200 with unexpected Content-Type %q", contentType) + } + + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_Enigma } func (s Scanner) Description() string { - return "Enigma is a data intelligence company that provides comprehensive data about businesses. Enigma API keys can be used to access and interact with this data." + return "Enigma is a business data intelligence platform. Enigma API keys grant access to comprehensive firmographic, legal, and identity datasets." } diff --git a/pkg/detectors/enigma/enigma_integration_test.go b/pkg/detectors/enigma/enigma_integration_test.go index c0fbb725d..576db484c 100644 --- a/pkg/detectors/enigma/enigma_integration_test.go +++ b/pkg/detectors/enigma/enigma_integration_test.go @@ -19,7 +19,7 @@ import ( func TestEnigma_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } @@ -50,6 +50,9 @@ func TestEnigma_FromChunk(t *testing.T) { { DetectorType: detector_typepb.DetectorType_Enigma, Verified: true, + SecretParts: map[string]string{ + "key": secret, + }, }, }, wantErr: false, @@ -66,6 +69,9 @@ func TestEnigma_FromChunk(t *testing.T) { { DetectorType: detector_typepb.DetectorType_Enigma, Verified: false, + SecretParts: map[string]string{ + "key": inactiveSecret, + }, }, }, wantErr: false, diff --git a/pkg/engine/defaults/defaults.go b/pkg/engine/defaults/defaults.go index 21a4f6171..bd55ef76a 100644 --- a/pkg/engine/defaults/defaults.go +++ b/pkg/engine/defaults/defaults.go @@ -1778,6 +1778,8 @@ func buildDetectorList() []detectors.Detector { return !feature.CloudinaryDetectorEnabled.Load() case *gitlaboauth2.Scanner: return !feature.GitLabOAuthDetectorEnabled.Load() + case *enigma.Scanner: + return !feature.EnigmaDetectorEnabled.Load() default: return false } diff --git a/pkg/engine/defaults/defaults_test.go b/pkg/engine/defaults/defaults_test.go index 120457c92..50e15358e 100644 --- a/pkg/engine/defaults/defaults_test.go +++ b/pkg/engine/defaults/defaults_test.go @@ -119,6 +119,7 @@ var excludedFromDefaultList = map[detector_typepb.DetectorType]struct{}{ // Remove each entry once its detector has been carefully added. detector_typepb.DetectorType_Cloudinary: {}, detector_typepb.DetectorType_DatadogApikey: {}, + detector_typepb.DetectorType_Enigma: {}, detector_typepb.DetectorType_GitLabOauth2: {}, detector_typepb.DetectorType_Guru: {}, detector_typepb.DetectorType_IPInfo: {}, diff --git a/pkg/feature/feature.go b/pkg/feature/feature.go index 61a50040e..86bbd48f9 100644 --- a/pkg/feature/feature.go +++ b/pkg/feature/feature.go @@ -19,6 +19,7 @@ var ( PineconeDetectorEnabled atomic.Bool CloudinaryDetectorEnabled atomic.Bool GitLabOAuthDetectorEnabled atomic.Bool + EnigmaDetectorEnabled atomic.Bool ) type AtomicString struct {