Detect Organization ID to pass into AnalysisInfo for Atlassian Detector (#4480)

* detect organization id and pass into analysis info

* add non integration tests for analysis info, examples of credentials matching the regexes

* switch from tabular tests to separate tests

* move gock setup outside test functions

---------

Co-authored-by: Shahzad Haider <[email protected]>
Co-authored-by: Amaan Ullah <[email protected]>
Co-authored-by: Kashif Khan <[email protected]>
This commit is contained in:
Mustansir
2025-10-09 10:57:51 +05:00
committed by GitHub
co-authored by Shahzad Haider Amaan Ullah Kashif Khan
parent d005847359
commit b140e2b58e
3 changed files with 129 additions and 22 deletions
+46 -21
View File
@@ -35,7 +35,11 @@ var _ detectors.Versioner = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// Example: ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A
keyPat = regexp.MustCompile(`\b(ATCTT3xFfG[A-Za-z0-9+/=_-]+=[A-Za-z0-9]{8})\b`)
// Example: 123e4567-e89b-12d3-a456-426614174000
organizationIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"org", "id"}) + `\b([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\b`)
)
// Keywords are used for efficiently pre-filtering chunks.
@@ -58,31 +62,52 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
uniqueMatches[match[1]] = struct{}{}
}
uniqueOrgIdMatches := make(map[string]struct{})
for _, match := range organizationIdPat.FindAllStringSubmatch(dataStr, -1) {
uniqueOrgIdMatches[match[1]] = struct{}{}
}
if len(uniqueOrgIdMatches) == 0 {
// we only need an org ID to pass into AnalysisInfo
// if we don't find one, we can still verify the key
// we can add a dummy entry here just to make sure a result is returned
uniqueOrgIdMatches[""] = struct{}{}
}
for match := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Atlassian,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/atlassian/",
"version": fmt.Sprintf("%d", s.Version()),
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
for orgId := range uniqueOrgIdMatches {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Atlassian,
Raw: []byte(match),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/atlassian/",
"version": fmt.Sprintf("%d", s.Version()),
},
}
isVerified, orgResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if orgResponse != nil && len(orgResponse.Data) > 0 {
s1.ExtraData["Organization"] = orgResponse.Data[0].Attributes.Name
}
s1.SetVerificationError(verificationErr, match)
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
results = append(results, s1)
isVerified, orgResponse, verificationErr := verifyMatch(ctx, client, match)
s1.Verified = isVerified
if orgResponse != nil && len(orgResponse.Data) > 0 {
s1.ExtraData["Organization"] = orgResponse.Data[0].Attributes.Name
}
s1.SetVerificationError(verificationErr, match)
if s1.Verified {
s1.AnalysisInfo = map[string]string{
"key": match,
}
if orgId != "" {
s1.AnalysisInfo["organization_id"] = orgId
}
}
}
results = append(results, s1)
}
}
return
@@ -136,7 +136,7 @@ func TestAtlassian_FromChunk(t *testing.T) {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "ExtraData")
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "ExtraData", "primarySecret", "AnalysisInfo")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Atlassian.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
@@ -2,12 +2,17 @@ package atlassian
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"gopkg.in/h2non/gock.v1"
)
func TestAtlassian_Pattern(t *testing.T) {
@@ -79,3 +84,80 @@ func TestAtlassian_Pattern(t *testing.T) {
})
}
}
// TestAtlassian_AnalysisInfo_KeyAndOrgId tests if both the key and organization id are populated into AnalysisInfo
// given that they are present in the input data chunk
func TestAtlassian_AnalysisInfo_KeyAndOrgId(t *testing.T) {
client := common.SaneHttpClient()
d := Scanner{client: client}
key := "ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A"
orgId := "123j4567-e89b-12d3-a456-426614174000"
defer gock.Off()
defer gock.RestoreClient(client)
gock.InterceptClient(client)
gock.New("https://api.atlassian.com").
Get("/admin/v1/orgs").
MatchHeader("Accept", "application/json").
MatchHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
Reply(http.StatusOK).
JSON(map[string]any{
"Data": []map[string]any{},
})
t.Run("key and organization id both present", func(t *testing.T) {
input := fmt.Sprintf(`
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=%s
[DEBUG] Using Organization ID=%s
[INFO] Response received: 200 OK
`, key, orgId)
results, err := d.FromData(context.Background(), true, []byte(input))
require.NoError(t, err)
require.Len(t, results, 1, "mismatch in result count: expected %d, got %d", 1, len(results))
result := results[0]
require.NotNil(t, result.AnalysisInfo, "AnalysisInfo is nil")
assert.Equal(t, key, result.AnalysisInfo["key"], "mismatch in key")
assert.Equal(t, orgId, result.AnalysisInfo["organization_id"], "mismatch in organization_id")
})
}
// TestAtlassian_AnalysisInfo_KeyOnly tests if only key is populated into AnalysisInfo
// given that only the key and no organization_id is present in the input data chunk
func TestAtlassian_AnalysisInfo_KeyOnly(t *testing.T) {
client := common.SaneHttpClient()
d := Scanner{client: client}
key := "ATCTT3xFfGN0GsZNgOGrQSHSnxiJVi00oHlRicyM0yMNuKCBfw6qOHVcCy4Hm89GnclGb_W-1qAkxqCn5XbuyoX54bNhpK5yFKGFR7ocV6FByvL_P9Sb3tFnbUg3T3I3S_RGCBLMSN7Nsa4GJv8JEJ6bzvDmX-oJ8AnrazMU-zZ5hb-u3t2ERew=366BFE3A"
defer gock.Off()
defer gock.RestoreClient(client)
gock.InterceptClient(client)
gock.New("https://api.atlassian.com").
Get("/admin/v1/orgs").
MatchHeader("Accept", "application/json").
MatchHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
Reply(http.StatusOK).
JSON(map[string]any{
"Data": []map[string]any{},
})
t.Run("only key present", func(t *testing.T) {
input := fmt.Sprintf(`
[INFO] Sending request to the atlassian API
[DEBUG] Using Key=%s
[INFO] Response received: 200 OK
`, key)
results, err := d.FromData(context.Background(), true, []byte(input))
require.NoError(t, err)
require.Len(t, results, 1, "mismatch in result count: expected %d, got %d", 1, len(results))
result := results[0]
require.NotNil(t, result.AnalysisInfo, "AnalysisInfo is nil")
assert.Equal(t, key, result.AnalysisInfo["key"], "mismatch in key")
})
}