Files
trufflehog/pkg/analyzer/analyzers/openai/openai.go
T
Amaan Ullah 0fa069c12f Enable errcheck and staticcheck for golangci-lint v2 and resolve all issues (#4924)
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues

* skip lint on intentional reference of deprecated DetectorType values
2026-05-15 17:07:14 +05:00

366 lines
9.3 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go openai
package openai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeOpenAI }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
info, err := AnalyzePermissions(a.Cfg, credInfo["key"])
if err != nil {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err)
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *AnalyzerJSON) *analyzers.AnalyzerResult {
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeOpenAI,
Metadata: map[string]any{
"user": info.me.Name,
"email": info.me.Email,
"mfa": strconv.FormatBool(info.me.MfaEnabled),
"is_admin": strconv.FormatBool(info.isAdmin),
"is_restricted": strconv.FormatBool(info.isRestricted),
},
}
perms := convertPermissions(info.isAdmin, info.perms)
for _, org := range info.me.Orgs.Data {
resource := analyzers.Resource{
Name: org.Title,
FullyQualifiedName: org.ID,
Type: "organization",
Metadata: map[string]any{
"description": org.Description,
"user": org.User,
},
}
// Copy each permission into this resource.
result.Bindings = append(result.Bindings, analyzers.BindAllPermissions(resource, perms...)...)
}
return &result
}
func convertPermissions(isAdmin bool, perms []permissionData) []analyzers.Permission {
var permissions []analyzers.Permission
if isAdmin {
permissions = append(permissions, analyzers.Permission{Value: analyzers.FullAccess})
} else {
for _, perm := range flattenPerms(perms...) {
permName := PermissionStrings[perm]
permissions = append(permissions, analyzers.Permission{Value: permName})
}
}
return permissions
}
// flattenPerms takes a slice of permissionData and returns all of the
// individual Permission values in a single slice.
func flattenPerms(perms ...permissionData) []Permission {
var output []Permission
for _, perm := range perms {
output = append(output, perm.permissions...)
}
return output
}
const (
BASE_URL = "https://api.openai.com"
ORGS_ENDPOINT = "/v1/organizations"
ME_ENDPOINT = "/v1/me"
)
type MeJSON struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone_number"`
MfaEnabled bool `json:"mfa_flag_enabled"`
Orgs struct {
Data []struct {
ID string `json:"id"`
Title string `json:"title"`
User string `json:"name"`
Description string `json:"description"`
Personal bool `json:"personal"`
Default bool `json:"is_default"`
Role string `json:"role"`
} `json:"data"`
} `json:"orgs"`
}
type permissionData struct {
name string
endpoints []string
status analyzers.PermissionType
permissions []Permission
}
type AnalyzerJSON struct {
me MeJSON
isAdmin bool
isRestricted bool
perms []permissionData
}
var POST_PAYLOAD = map[string]interface{}{"speed": 1}
func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey string) {
data, err := AnalyzePermissions(cfg, apiKey)
if err != nil {
color.Red("[x] %s", err.Error())
return
}
color.Green("[!] Valid OpenAI Token\n\n")
printAPIKeyType(apiKey)
printData(data.me)
if data.isAdmin {
color.Green("[!] Admin API Key. All permissions available.")
} else if data.isRestricted {
color.Yellow("[!] Restricted API Key. Limited permissions available.")
printPermissions(data.perms, cfg.ShowAll)
}
}
// AnalyzePermissions will analyze the permissions of an OpenAI API key
func AnalyzePermissions(cfg *config.Config, key string) (*AnalyzerJSON, error) {
data := AnalyzerJSON{
isAdmin: false,
isRestricted: false,
}
meJSON, err := getUserData(cfg, key)
if err != nil {
return nil, err
}
data.me = meJSON
isAdmin, err := checkAdminKey(cfg, key)
if err != nil {
return nil, err
}
if isAdmin {
data.isAdmin = true
} else {
data.isRestricted = true
if err := analyzeScopes(key); err != nil {
return nil, err
}
data.perms = getPermissions()
}
return &data, nil
}
func analyzeScopes(key string) error {
for _, scope := range SCOPES {
if err := scope.RunTests(key); err != nil {
return err
}
}
return nil
}
func openAIRequest(cfg *config.Config, method string, url string, key string, data map[string]interface{}) ([]byte, *http.Response, error) {
var inBody io.Reader
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, nil, err
}
inBody = bytes.NewBuffer(jsonData)
}
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(method, url, inBody)
if err != nil {
return nil, nil, err
}
req.Header.Add("Authorization", "Bearer "+key)
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer func() { _ = resp.Body.Close() }()
outBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return outBody, resp, nil
}
func checkAdminKey(cfg *config.Config, key string) (bool, error) {
// Check for all permissions
//nolint:bodyclose
_, resp, err := openAIRequest(cfg, "GET", BASE_URL+ORGS_ENDPOINT, key, nil)
if err != nil {
return false, err
}
switch resp.StatusCode {
case 200:
return true, nil
case 403:
return false, nil
default:
return false, err
}
}
func getUserData(cfg *config.Config, key string) (MeJSON, error) {
var meJSON MeJSON
//nolint:bodyclose
me, resp, err := openAIRequest(cfg, "GET", BASE_URL+ME_ENDPOINT, key, nil)
if err != nil {
return meJSON, err
}
if resp.StatusCode != 200 {
return meJSON, fmt.Errorf("invalid OpenAI token")
}
// Marshall me into meJSON struct
if err := json.Unmarshal(me, &meJSON); err != nil {
return meJSON, err
}
return meJSON, nil
}
func printAPIKeyType(apiKey string) {
if strings.Contains(apiKey, "-svcacct-") {
color.Yellow("[i] Service Account API Key\n")
} else if strings.Contains(apiKey, "-admin-") {
color.Yellow("[i] Admin API Key\n")
} else {
color.Yellow("[i] Project/Org API Key\n")
}
}
func printData(meJSON MeJSON) {
if meJSON.Name != "" && meJSON.Email != "" {
userTable := table.NewWriter()
userTable.SetOutputMirror(os.Stdout)
color.Green("[i] User Information")
userTable.AppendHeader(table.Row{"UserID", "User", "Email", "Phone", "MFA Enabled"})
userTable.AppendRow(table.Row{meJSON.ID, meJSON.Name, meJSON.Email, meJSON.Phone, meJSON.MfaEnabled})
userTable.Render()
} else {
color.Yellow("[!] No User Information Available")
}
if len(meJSON.Orgs.Data) > 0 {
orgTable := table.NewWriter()
orgTable.SetOutputMirror(os.Stdout)
color.Green("[i] Organizations Information")
orgTable.AppendHeader(table.Row{"Org ID", "Title", "User", "Default", "Role"})
for _, org := range meJSON.Orgs.Data {
orgTable.AppendRow(table.Row{org.ID, fmt.Sprintf("%s (%s)", org.Title, org.Description), org.User, org.Default, org.Role})
}
orgTable.Render()
} else {
color.Yellow("[!] No Organizations Information Available")
}
}
func stringifyPermissionStatus(scope OpenAIScope) ([]Permission, analyzers.PermissionType) {
readStatus := false
writeStatus := false
errors := false
for _, test := range scope.ReadTests {
if test.Type == analyzers.READ {
readStatus = test.Status.Value
}
if test.Status.IsError {
errors = true
}
}
for _, test := range scope.WriteTests {
if test.Type == analyzers.WRITE {
writeStatus = test.Status.Value
}
if test.Status.IsError {
errors = true
}
}
if errors {
return nil, analyzers.ERROR
}
if readStatus && writeStatus {
return []Permission{scope.ReadPermission, scope.WritePermission}, analyzers.READ_WRITE
} else if readStatus {
return []Permission{scope.ReadPermission}, analyzers.READ
} else if writeStatus {
return []Permission{scope.WritePermission}, analyzers.WRITE
} else {
return nil, analyzers.NONE
}
}
func getPermissions() []permissionData {
var perms []permissionData
for _, scope := range SCOPES {
permissions, status := stringifyPermissionStatus(scope)
perms = append(perms, permissionData{
name: scope.Endpoints[0], // Using the first endpoint as the name for simplicity
endpoints: scope.Endpoints,
status: status,
permissions: permissions,
})
}
return perms
}
func printPermissions(perms []permissionData, showAll bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Endpoints", "Permission"})
for _, perm := range perms {
if showAll || perm.status != analyzers.NONE {
t.AppendRow([]any{perm.name, perm.endpoints[0], perm.status})
for i := 1; i < len(perm.endpoints); i++ {
t.AppendRow([]any{"", perm.endpoints[i], perm.status})
}
}
}
t.Render()
fmt.Print("\n\n")
}