Fixed Kontent Detector (#4122)

* fixed kontent detector

* avoid false positives for env uuid
This commit is contained in:
Kashif Khan
2025-05-07 16:22:30 +05:00
committed by GitHub
parent e42153d44a
commit 1cdadf1f26
3 changed files with 95 additions and 49 deletions
+76 -22
View File
@@ -3,10 +3,12 @@ package kontent
import ( import (
"context" "context"
"fmt" "fmt"
regexp "github.com/wasilibs/go-re2" "io"
"net/http" "net/http"
"strings" "strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -19,9 +21,12 @@ var _ detectors.Detector = (*Scanner)(nil)
var ( var (
client = common.SaneHttpClient() client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives. // Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"kontent"}) + `\b([a-z0-9-]{36})\b`) apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"kontent"}) + common.BuildRegexJWT("30,34", "200,400", "40,43"))
envIDPat = regexp.MustCompile(detectors.PrefixRegex([]string{"kontent", "env"}) + common.UUIDPattern)
// API return this error when the environment does not exist or the api key does not have the persmission to access that environment
envErr = "The specified API key does not provide the permissions required to access the environment"
) )
// Keywords are used for efficiently pre-filtering chunks. // Keywords are used for efficiently pre-filtering chunks.
@@ -34,31 +39,40 @@ func (s Scanner) Keywords() []string {
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data) dataStr := string(data)
matches := keyPat.FindAllStringSubmatch(dataStr, -1) var uniqueAPIKeys, uniqueEnvIDs = make(map[string]struct{}), make(map[string]struct{})
for _, match := range matches { for _, apiKey := range apiKeyPat.FindAllStringSubmatch(dataStr, -1) {
resMatch := strings.TrimSpace(match[1]) uniqueAPIKeys[apiKey[1]] = struct{}{}
}
s1 := detectors.Result{ for _, envID := range envIDPat.FindAllStringSubmatch(dataStr, -1) {
DetectorType: detectorspb.DetectorType_Kontent, uniqueEnvIDs[envID[1]] = struct{}{}
Raw: []byte(resMatch), }
for envID := range uniqueEnvIDs {
if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(envID)]; ok {
continue
} }
if verify { if detectors.StringShannonEntropy(envID) < 3 {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://deliver.kontent.ai/%s/items", resMatch), nil) continue
if err != nil {
continue
}
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
} }
results = append(results, s1) for apiKey := range uniqueAPIKeys {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Kontent,
Raw: []byte(envID),
RawV2: []byte(envID + apiKey),
}
if verify {
isVerified, verificationErr := verifyKontentAPIKey(client, envID, apiKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr)
}
results = append(results, s1)
}
} }
return results, nil return results, nil
@@ -71,3 +85,43 @@ func (s Scanner) Type() detectorspb.DetectorType {
func (s Scanner) Description() string { func (s Scanner) Description() string {
return "Kontent is a headless CMS (Content Management System) that allows users to manage and deliver content to any device or application. Kontent API keys can be used to access and manage this content." return "Kontent is a headless CMS (Content Management System) that allows users to manage and deliver content to any device or application. Kontent API keys can be used to access and manage this content."
} }
// api docs: https://kontent.ai/learn/docs/apis/openapi/management-api-v2/#operation/retrieve-environment-information
func verifyKontentAPIKey(client *http.Client, envID, apiKey string) (bool, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://manage.kontent.ai/v2/projects/%s", envID), nil)
if err != nil {
return false, nil
}
req.Header.Add("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden:
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
if strings.Contains(string(bodyBytes), envErr) {
return true, nil
}
return false, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
@@ -19,11 +19,13 @@ import (
func TestKontent_FromChunk(t *testing.T) { func TestKontent_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1") testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil { if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err) t.Fatalf("could not get test secrets from GCP: %s", err)
} }
secret := testSecrets.MustGetField("KONTENT")
envID := testSecrets.MustGetField("KONTENT_ENV_ID")
secret := testSecrets.MustGetField("KONTENT_API_KEY")
inactiveSecret := testSecrets.MustGetField("KONTENT_INACTIVE") inactiveSecret := testSecrets.MustGetField("KONTENT_INACTIVE")
type args struct { type args struct {
@@ -43,7 +45,7 @@ func TestKontent_FromChunk(t *testing.T) {
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a kontent secret %s within", secret)), data: []byte(fmt.Sprintf("You can find a kontent env id: %s and kontent secret %s within", envID, secret)),
verify: true, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
@@ -59,7 +61,7 @@ func TestKontent_FromChunk(t *testing.T) {
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a kontent secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation data: []byte(fmt.Sprintf("You can find a kontent env id: %s and kontent secret %s within but not valid", envID, inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
@@ -95,6 +97,7 @@ func TestKontent_FromChunk(t *testing.T) {
t.Fatal("no raw secret present") t.Fatal("no raw secret present")
} }
got[i].Raw = nil got[i].Raw = nil
got[i].RawV2 = nil
} }
if diff := pretty.Compare(got, tt.want); diff != "" { if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Kontent.FromData() %s diff: (-got +want)\n%s", tt.name, diff) t.Errorf("Kontent.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
+12 -23
View File
@@ -2,7 +2,6 @@ package kontent
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -11,12 +10,6 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
) )
var (
validPattern = "jca9is4icbynssyi1y4spdwcbwe3vwv9jn4d"
invalidPattern = "jca9is4icbynssyi1y4spdwcbwe3vwv9jn4"
keyword = "kontent"
)
func TestKontent_Pattern(t *testing.T) { func TestKontent_Pattern(t *testing.T) {
d := Scanner{} d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -26,24 +19,20 @@ func TestKontent_Pattern(t *testing.T) {
want []string want []string
}{ }{
{ {
name: "valid pattern - with keyword kontent", name: "valid pattern - with keyword kontent",
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern), input: `
want: []string{validPattern}, // the following are credentials for kontent.ai APIs - do not share with anyone
kontent_personal_api_key = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjOTE4OThlMWZlMGI0NDcwOTczOGM0ZmE0YzVlYzk0MyIsImlhdCI6MTc0NjUyNzQyNSwibmJmIjoxNzQ2NTI3NDI1LCJleHAiOjE3NjI0MjQ5NDAsInZlciI6IjMuMC4wIiwidWlkIjoidmlydHVhbF8zNTI4OGIxNC00YmE3LTQ5MzgtODZiNC1lYjFhYjczMDBiZTciLCJzY29wZV9pZCI6IjAyYmYxZDg5NzYzMjQ3ZWE4MTFkYjkwMjVhYjc0MTRhIiwicHJvamVjdF9jb250YWluZXJfaWQiOiI0MDFkMzg1NmMyYzUwMGZlOTYwMTE5YzFhMThkNWY4OCIsImF1ZCI6Im1hbmFnZS5rZW50aWNvY2xvdWQuY29tIn0.yfZTic9Zba6Dui8N6UO6t-SGbZYf17bKAd-uJ9enYPw
kontent_env_id = 3d5f4d88-0511-00b3-37f1-31bb55c25ab4`,
want: []string{"3d5f4d88-0511-00b3-37f1-31bb55c25ab4eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjOTE4OThlMWZlMGI0NDcwOTczOGM0ZmE0YzVlYzk0MyIsImlhdCI6MTc0NjUyNzQyNSwibmJmIjoxNzQ2NTI3NDI1LCJleHAiOjE3NjI0MjQ5NDAsInZlciI6IjMuMC4wIiwidWlkIjoidmlydHVhbF8zNTI4OGIxNC00YmE3LTQ5MzgtODZiNC1lYjFhYjczMDBiZTciLCJzY29wZV9pZCI6IjAyYmYxZDg5NzYzMjQ3ZWE4MTFkYjkwMjVhYjc0MTRhIiwicHJvamVjdF9jb250YWluZXJfaWQiOiI0MDFkMzg1NmMyYzUwMGZlOTYwMTE5YzFhMThkNWY4OCIsImF1ZCI6Im1hbmFnZS5rZW50aWNvY2xvdWQuY29tIn0.yfZTic9Zba6Dui8N6UO6t-SGbZYf17bKAd-uJ9enYPw"},
}, },
{ {
name: "valid pattern - ignore duplicate", name: "invalid pattern",
input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern), input: `
want: []string{validPattern}, // the following are credentials for kontent.ai APIs - do not share with anyone
}, kontent_personal_api_key = eyJhbGciOiJIUzI1NiIsInR5cCVCJ9.eyJqdGkiOiJjOTE4OThlMWZlMGI0NDcwOTczOGM0ZmE0YzVlYzk0MyIsImlhdCI6MTc0NjUyNzQyNSwibmJmIjoxNzQ2NTI3NDI1LCJleHAiOjE3NjI0MjQ5NDAsInZlciI6IjMuMC4wIiwidWlkIjoidmlydHVhbF8zNTI4OGIxNC00YmE3LTQ5MzgtODZiNC1lYjFhYjczMDBiZTciLCJzY29wZV9pZCI6IjAyYmYxZDg5NzYzMjQ3ZWE4MTFkYjkwMjVhYjc0MTRhIiwicHJvamVjdF9jb250YWluZXJfaWQiOiI0MDFkMzg1NmMyYzUwMGZlOTYwMTE5YzFhMThkNWY4OCIsImF1ZCI6Im1hbmFnZS5rZW50aWNvY2xvdWQuY29tIn0.yfZTic9Zba6Dui8N6UO6t-SGbZYf17bKAd-uJ9enYPw
{ kontent_env_id = 3d5f4d88-051-00b3-37f1-31bb55c25ab4`,
name: "valid pattern - key out of prefix range", want: []string{},
input: fmt.Sprintf("%s keyword is not close to the real key in the data\n = '%s'", keyword, validPattern),
want: []string{},
},
{
name: "invalid pattern",
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
want: []string{},
}, },
} }