-
Notifications
You must be signed in to change notification settings - Fork 2.2k
OpenAI Admin Key Detector #4689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
+497
−9
Merged
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
8916176
restrict openai detector regex to skip admin key
amanfcp ca48c1e
add openai admin key detector
amanfcp 31af778
fix: add missing hyphen in openai detector regex
amanfcp 43bbdf3
Merge branch 'main' into INS-259
amanfcp 2e05fe8
incorporated feedback
amanfcp e484c02
Merge branch 'INS-259' of github.com:trufflesecurity/trufflehog into …
amanfcp 4579be3
Merge branch 'main' into INS-259
amanfcp 43e68ae
Merge branch 'main' into INS-259
amanfcp dee6d24
Merge branch 'main' into INS-259
amanfcp c1eca6e
remove redundant comment
amanfcp d7b0552
Merge branch 'INS-259' of github.com:trufflesecurity/trufflehog into …
amanfcp 65172a3
Merge branch 'main' into INS-259
amanfcp 7583188
Merge branch 'main' into INS-259
amanfcp 0e1beb8
Merge branch 'main' into INS-259
amanfcp 4fdc1c0
uniformity in openai regex
amanfcp 2e32de0
Merge branch 'main' into INS-259
amanfcp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package openaiadmin | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| regexp "github.com/wasilibs/go-re2" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
| ) | ||
|
|
||
| type Scanner struct { | ||
| client *http.Client | ||
| } | ||
|
|
||
| // Ensure the Scanner satisfies the interface at compile time. | ||
| var _ detectors.Detector = (*Scanner)(nil) | ||
|
|
||
| var ( | ||
| defaultClient = common.SaneHttpClient() | ||
|
|
||
| // Admin keys follow the format: sk-admin-{58 chars}T3BlbkFJ{58 chars} | ||
| // Total length: 133 chars (9 char prefix + 124 chars for key) | ||
| // where T3BlbkFJ is the base64-encoded string: OpenAI | ||
| keyPat = regexp.MustCompile(`\b(sk-admin-[A-Za-z0-9_-]{58}T3BlbkFJ[A-Za-z0-9_-]{58})\b`) | ||
| ) | ||
|
|
||
| // Keywords are used for efficiently pre-filtering chunks. | ||
| func (s Scanner) Keywords() []string { | ||
| // Using both keywords for better detection coverage | ||
| // T3BlbkFJ is the OpenAI signature, sk-admin- is the specific prefix | ||
amanfcp marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return []string{"sk-admin-"} | ||
amanfcp marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
amanfcp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // FromData will find and optionally verify Openaiadmin 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) | ||
|
|
||
| uniqueMatches := make(map[string]struct{}) | ||
| for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { | ||
| uniqueMatches[match[1]] = struct{}{} | ||
| } | ||
|
|
||
| for token := range uniqueMatches { | ||
| s1 := detectors.Result{ | ||
| DetectorType: detectorspb.DetectorType_OpenAIAdmin, | ||
| Redacted: token[:11] + "..." + token[len(token)-4:], | ||
| Raw: []byte(token), | ||
| } | ||
|
|
||
| if verify { | ||
| client := s.client | ||
| if client == nil { | ||
| client = defaultClient | ||
| } | ||
|
|
||
| isVerified, verificationErr := verifyMatch(ctx, client, token) | ||
| s1.Verified = isVerified | ||
| s1.SetVerificationError(verificationErr, token) | ||
| s1.AnalysisInfo = map[string]string{ | ||
| "key": token, | ||
| } | ||
| } | ||
|
|
||
| results = append(results, s1) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { | ||
| // Use the Admin API Keys list endpoint to verify the admin key | ||
| // https://platform.openai.com/docs/api-reference/admin-api-keys/list | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.openai.com/v1/organization/admin_api_keys", http.NoBody) | ||
amanfcp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| req.Header.Set("Content-Type", "application/json") | ||
| req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) | ||
|
|
||
| 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: | ||
| return true, nil | ||
| case http.StatusUnauthorized: | ||
| // Invalid admin key - determinate failure | ||
| return false, nil | ||
| default: | ||
| // Unexpected response - indeterminate failure | ||
| return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) | ||
| } | ||
| } | ||
|
|
||
| func (s Scanner) Type() detectorspb.DetectorType { | ||
| return detectorspb.DetectorType_OpenAIAdmin | ||
| } | ||
|
|
||
| func (s Scanner) Description() string { | ||
| return "OpenAI Admin API keys provide administrative access to OpenAI organization resources. These keys can be used to manage API keys, audit logs, and other organization-level settings." | ||
| } | ||
162 changes: 162 additions & 0 deletions
162
pkg/detectors/openaiadmin/openaiadmin_integration_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| //go:build detectors | ||
| // +build detectors | ||
|
|
||
| package openaiadmin | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
| ) | ||
|
|
||
| func TestOpenAIAdmin_FromChunk(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | ||
| defer cancel() | ||
| testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") | ||
| if err != nil { | ||
| t.Fatalf("could not get test secrets from GCP: %s", err) | ||
| } | ||
| secret := testSecrets.MustGetField("OPENAI_ADMIN") | ||
| inactiveSecret := testSecrets.MustGetField("OPENAI_ADMIN_INACTIVE") | ||
|
|
||
| type args struct { | ||
| ctx context.Context | ||
| data []byte | ||
| verify bool | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| s Scanner | ||
| args args | ||
| want []detectors.Result | ||
| wantErr bool | ||
| wantVerificationErr bool | ||
| }{ | ||
| { | ||
| name: "found, verified", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf("You can find an OpenAI admin secret %s within", secret)), | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_OpenAIAdmin, | ||
| Verified: true, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| wantVerificationErr: false, | ||
| }, | ||
| { | ||
| name: "found, unverified", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf("You can find an OpenAI admin secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_OpenAIAdmin, | ||
| Verified: false, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| wantVerificationErr: false, | ||
| }, | ||
| { | ||
| name: "not found", | ||
| s: Scanner{}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte("You cannot find the secret within"), | ||
| verify: true, | ||
| }, | ||
| want: nil, | ||
| wantErr: false, | ||
| wantVerificationErr: false, | ||
| }, | ||
| { | ||
| name: "found, would be verified if not for timeout", | ||
| s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf("You can find an OpenAI admin secret %s within", secret)), | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_OpenAIAdmin, | ||
| Verified: false, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| wantVerificationErr: true, | ||
| }, | ||
| { | ||
| name: "found, verified but unexpected api surface", | ||
| s: Scanner{client: common.ConstantResponseHttpClient(404, "")}, | ||
| args: args{ | ||
| ctx: context.Background(), | ||
| data: []byte(fmt.Sprintf("You can find an OpenAI admin secret %s within", secret)), | ||
| verify: true, | ||
| }, | ||
| want: []detectors.Result{ | ||
| { | ||
| DetectorType: detectorspb.DetectorType_OpenAIAdmin, | ||
| Verified: false, | ||
| }, | ||
| }, | ||
| wantErr: false, | ||
| wantVerificationErr: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("OpenAIAdmin.FromData() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
| for i := range got { | ||
| if len(got[i].Raw) == 0 { | ||
| t.Fatalf("no raw secret present: \n %+v", got[i]) | ||
| } | ||
| if (got[i].VerificationError() != nil) != tt.wantVerificationErr { | ||
| t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError()) | ||
| } | ||
| } | ||
| ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo", "Redacted") | ||
| ignoreUnexported := cmpopts.IgnoreUnexported(detectors.Result{}) | ||
| if diff := cmp.Diff(got, tt.want, ignoreOpts, ignoreUnexported); diff != "" { | ||
| t.Errorf("OpenAIAdmin.FromData() %s diff: (-got +want)\n%s", tt.name, diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkFromData(benchmark *testing.B) { | ||
| ctx := context.Background() | ||
| s := Scanner{} | ||
| for name, data := range detectors.MustGetBenchmarkData() { | ||
| benchmark.Run(name, func(b *testing.B) { | ||
| b.ResetTimer() | ||
| for n := 0; n < b.N; n++ { | ||
| _, err := s.FromData(ctx, false, data) | ||
| if err != nil { | ||
| b.Fatal(err) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.