Files
trufflehog/pkg/analyzer/analyzers/huggingface/huggingface.go
Bill Rich abecab0d8f
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Snifftest / Run Snifftest (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Remove proto (#3489)
Only used for tracking analyzer types
2024-10-21 17:16:26 -07:00

668 lines
19 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go huggingface
package huggingface
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
const (
FINEGRAINED = "fineGrained"
WRITE = "write"
READ = "read"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeHuggingFace }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok || key == "" {
return nil, fmt.Errorf("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func bakeUnboundedResources(tokenJSON HFTokenJSON) []analyzers.Resource {
unboundedResources := make([]analyzers.Resource, len(tokenJSON.Orgs))
for idx, org := range tokenJSON.Orgs {
unboundedResources[idx] = analyzers.Resource{
Name: org.Name,
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username + "/organization/" + org.Name,
Type: "organization",
Metadata: map[string]interface{}{
"role": org.Role,
"is_enterprise": org.IsEnterprise,
},
}
}
return unboundedResources
}
func bakeUnfineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) []analyzers.Binding {
bindings := make([]analyzers.Binding, len(allModels))
for idx, model := range allModels {
// Add Read Privs to All Models
modelResource := analyzers.Resource{
Name: model.Name,
FullyQualifiedName: "huggingface.com/model/" + model.ID,
Type: "model",
Metadata: map[string]interface{}{
"private": model.Private,
},
}
// means both read & write permission for the model
accessLevel := string(analyzers.READ)
if tokenJSON.Auth.AccessToken.Type == WRITE {
accessLevel = string(analyzers.WRITE)
}
bindings[idx] = analyzers.Binding{
Resource: modelResource,
Permission: analyzers.Permission{
Value: string(accessLevel),
},
}
}
return bindings
}
// finegrained scopes are grouped by org, user or model.
func bakefineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) []analyzers.Binding {
// this section will extract the relevant permissions for each entity and store them in a map
var nameToPermissions = make(map[string]analyzers.Permission)
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
privs := analyzers.Permission{
Value: string(analyzers.NONE),
}
for _, perm := range permission.Permissions {
if perm == "repo.content.read" {
privs.Value = string(analyzers.READ)
} else if perm == "repo.write" {
privs.Value = string(analyzers.WRITE)
}
}
if permission.Entity.Type == "user" || permission.Entity.Type == "org" {
nameToPermissions[permission.Entity.Name] = privs
} else if permission.Entity.Type == "model" {
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = privs
}
}
bindings := make([]analyzers.Binding, len(allModels))
for idx, model := range allModels {
// Add Read Privs to All Models
modelResource := analyzers.Resource{
Name: model.Name,
FullyQualifiedName: "huggingface.com/model/" + model.ID,
Type: "model",
Metadata: map[string]interface{}{
"private": model.Private,
},
}
var perm analyzers.Permission
// get username/orgname for each model and apply those permissions
modelUsername := strings.Split(model.Name, "/")[0]
if permissions, ok := nameToPermissions[modelUsername]; ok {
perm = permissions
}
// override model permissions with repo-specific permissions
if permissions, ok := nameToPermissions[model.Name]; ok {
perm = permissions
}
bindings[idx] = analyzers.Binding{
Resource: modelResource,
Permission: perm,
}
}
return bindings
}
func bakeOrganizationBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
// check if there are any org permissions
// if so, save them as a map. Only need to do this once
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
// for all orgs referenced on an access token
orgPermissions := map[string]struct{}{}
var orgResource *analyzers.Resource = nil
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "org" {
orgResource = &analyzers.Resource{
Name: permission.Entity.Name,
FullyQualifiedName: "hugggingface.com/organization/" + permission.Entity.ID,
Type: "organization",
}
for _, perm := range permission.Permissions {
orgPermissions[perm] = struct{}{}
}
break
}
}
bindings := make([]analyzers.Binding, 0)
// check if there are any org permissions
if orgResource == nil {
return bindings
}
for _, permission := range org_scopes_order {
for key, value := range org_scopes[permission] {
if _, ok := orgPermissions[key]; ok {
bindings = append(bindings, analyzers.Binding{
Resource: *orgResource,
Permission: analyzers.Permission{
Value: value,
},
})
}
}
}
return bindings
}
func bakeUserBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
bindings := make([]analyzers.Binding, 0)
// build a map of all user permissions
users := map[string]struct{}{}
userPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "user" {
users[permission.Entity.Name] = struct{}{}
for _, perm := range permission.Permissions {
userPermissions[perm] = struct{}{}
}
}
}
// global permissions only apply to user tokens as of 6/6/24
// but there would be a naming collision in the scopes document
// so we prepend "global." to the key and then add to the map
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
userPermissions["global."+permission] = struct{}{}
}
// check if there are any user permissions
if len(userPermissions) == 0 {
return bindings
}
userResource := analyzers.Resource{
Name: tokenJSON.Name,
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username,
Type: "user",
}
for _, permission := range user_scopes_order {
for key, value := range user_scopes[permission] {
if _, ok := userPermissions[key]; ok {
bindings = append(bindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: value,
},
})
}
}
}
return bindings
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeHuggingFace,
Metadata: map[string]interface{}{
"username": info.Token.Username,
"name": info.Token.Name,
"token_name": info.Token.Auth.AccessToken.Name,
"token_type": info.Token.Auth.AccessToken.Type,
},
}
if len(info.Token.Orgs) > 0 {
result.UnboundedResources = bakeUnboundedResources(info.Token)
}
result.Bindings = make([]analyzers.Binding, 0)
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
result.Bindings = append(result.Bindings, bakefineGrainedBindings(info.Models, info.Token)...)
result.Bindings = append(result.Bindings, bakeOrganizationBindings(info.Token)...)
result.Bindings = append(result.Bindings, bakeUserBindings(info.Token)...)
} else {
result.Bindings = append(result.Bindings, bakeUnfineGrainedBindings(info.Models, info.Token)...)
}
return &result
}
// HFTokenJSON is the struct for the HF /whoami-v2 API JSON response
type HFTokenJSON struct {
Username string `json:"name"`
Name string `json:"fullname"`
Orgs []struct {
Name string `json:"name"`
Role string `json:"roleInOrg"`
IsEnterprise bool `json:"isEnterprise"`
} `json:"orgs"`
Auth struct {
AccessToken struct {
Name string `json:"displayName"`
Type string `json:"role"`
CreatedAt string `json:"createdAt"`
FineGrained struct {
Global []string `json:"global"`
Scoped []struct {
Entity struct {
Type string `json:"type"`
Name string `json:"name"`
ID string `json:"_id"`
} `json:"entity"`
Permissions []string `json:"permissions"`
} `json:"scoped"`
} `json:"fineGrained"`
}
} `json:"auth"`
}
type Permissions struct {
Read bool
Write bool
}
type Model struct {
Name string `json:"id"`
ID string `json:"_id"`
Private bool `json:"private"`
Permissions Permissions
}
// getModelsByAuthor calls the HF API /models endpoint with the author query param
// returns a list of models and an error
func getModelsByAuthor(cfg *config.Config, key string, author string) ([]Model, error) {
var modelsJSON []Model
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/models", nil)
if err != nil {
return modelsJSON, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// Add author param
q := req.URL.Query()
q.Add("author", author)
req.URL.RawQuery = q.Encode()
// send the request
resp, err := client.Do(req)
if err != nil {
return modelsJSON, err
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&modelsJSON); err != nil {
return modelsJSON, err
}
return modelsJSON, nil
}
// getTokenInfo calls the HF API /whoami-v2 endpoint to get the token info
// returns the token info, a boolean indicating token validity, and an error
func getTokenInfo(cfg *config.Config, key string) (HFTokenJSON, bool, error) {
var tokenJSON HFTokenJSON
// create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://huggingface.co/api/whoami-v2", nil)
if err != nil {
return tokenJSON, false, err
}
// Add bearer token
req.Header.Add("Authorization", "Bearer "+key)
// send the request
resp, err := client.Do(req)
if err != nil {
return tokenJSON, false, err
}
// check if the response is 200
if resp.StatusCode != 200 {
return tokenJSON, false, nil
}
// defer the response body closing
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&tokenJSON); err != nil {
return tokenJSON, true, err
}
return tokenJSON, true, nil
}
type SecretInfo struct {
Token HFTokenJSON
Models []Model
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// get token info
token, success, err := getTokenInfo(cfg, key)
if err != nil {
return nil, err
}
if !success {
return nil, fmt.Errorf("Invalid HuggingFace Access Token")
}
// get all models by username
var allModels []Model
userModels, err := getModelsByAuthor(cfg, key, token.Username)
if err != nil {
return nil, err
}
allModels = append(allModels, userModels...)
// get all models from all orgs
for _, org := range token.Orgs {
orgModels, err := getModelsByAuthor(cfg, key, org.Name)
if err != nil {
return nil, err
}
allModels = append(allModels, orgModels...)
}
return &SecretInfo{
Token: token,
Models: allModels,
}, nil
}
// AnalyzePermissions prints the permissions of a HuggingFace API key
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid HuggingFace Access Token\n\n")
// print user info
color.Yellow("[i] Username: " + info.Token.Username)
color.Yellow("[i] Name: " + info.Token.Name)
color.Yellow("[i] Token Name: " + info.Token.Auth.AccessToken.Name)
color.Yellow("[i] Token Type: " + info.Token.Auth.AccessToken.Type)
// print org info
printOrgs(info.Token)
// print accessible models
printAccessibleModels(info.Models, info.Token)
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
// print org permissions
printOrgPermissions(info.Token)
// print user permissions
printUserPermissions(info.Token)
}
}
// printUserPermissions prints the user permissions
// only applies to fine-grained tokens
func printUserPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] User Permissions:")
// build a map of all user permissions
userPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "user" {
for _, perm := range permission.Permissions {
userPermissions[perm] = struct{}{}
}
}
}
// global permissions only apply to user tokens as of 6/6/24
// but there would be a naming collision in the scopes document
// so we prepend "global." to the key and then add to the map
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
userPermissions["global."+permission] = struct{}{}
}
// check if there are any user permissions
if len(userPermissions) == 0 {
color.Red("\tNo user permissions scoped.")
return
}
// print the user permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range user_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range user_scopes[permission] {
if _, ok := userPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgPermissions prints the organization permissions
// only applies to fine-grained tokens
func printOrgPermissions(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organization Permissions:")
// check if there are any org permissions
// if so, save them as a map. Only need to do this once
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
// for all orgs referenced on an access token
orgScoped := false
orgPermissions := map[string]struct{}{}
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
if permission.Entity.Type == "org" {
orgScoped = true
for _, perm := range permission.Permissions {
orgPermissions[perm] = struct{}{}
}
break
}
}
// check if there are any org permissions
if !orgScoped {
color.Red("\tNo organization permissions scoped.")
return
}
// print the org permissions
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
for _, permission := range org_scopes_order {
t.AppendRow([]interface{}{permission, "---", "---"})
for key, value := range org_scopes[permission] {
if _, ok := orgPermissions[key]; ok {
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
} else {
t.AppendRow([]interface{}{"", value, "False"})
}
}
}
t.Render()
}
// printOrgs prints the organizations the user is a member of
func printOrgs(tokenJSON HFTokenJSON) {
color.Green("\n[i] Organizations:")
if len(tokenJSON.Orgs) == 0 {
color.Yellow("\tNo organizations found.")
return
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Role", "Is Enterprise"})
for _, org := range tokenJSON.Orgs {
enterprise := ""
role := ""
if org.IsEnterprise {
enterprise = color.New(color.FgGreen).Sprint("True")
} else {
enterprise = "False"
}
if org.Role == "admin" {
role = color.New(color.FgGreen).Sprint("Admin")
} else {
role = org.Role
}
t.AppendRow([]interface{}{color.GreenString(org.Name), role, enterprise})
}
t.Render()
}
// modelNameLookup is a helper function to lookup model name by _id
func modelNameLookup(models []Model, id string) string {
for _, model := range models {
if model.ID == id {
return model.Name
}
}
return ""
}
// printAccessibleModels adds permissions as needed to each model
//
// and then calls the printModelsTable function
func printAccessibleModels(allModels []Model, tokenJSON HFTokenJSON) {
color.Green("\n[i] Accessible Models:")
if tokenJSON.Auth.AccessToken.Type != FINEGRAINED {
// Add Read Privs to All Models
for idx := range allModels {
allModels[idx].Permissions.Read = true
}
// Add Write Privs to All Models if Write Access
if tokenJSON.Auth.AccessToken.Type == WRITE {
for idx := range allModels {
allModels[idx].Permissions.Write = true
}
}
// Print Models Table
printModelsTable(allModels)
return
}
// finegrained scopes are grouped by org, user or model.
// this section will extract the relevant permissions for each entity and store them in a map
var nameToPermissions = make(map[string]Permissions)
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
read := false
write := false
for _, perm := range permission.Permissions {
if perm == "repo.content.read" {
read = true
} else if perm == "repo.write" {
write = true
}
}
if permission.Entity.Type == "user" || permission.Entity.Type == "org" {
nameToPermissions[permission.Entity.Name] = Permissions{Read: read, Write: write}
} else if permission.Entity.Type == "model" {
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = Permissions{Read: read, Write: write}
}
}
// apply permissions to all models
for idx := range allModels {
// get username/orgname for each model and apply those permissions
modelUsername := strings.Split(allModels[idx].Name, "/")[0]
if permissions, ok := nameToPermissions[modelUsername]; ok {
allModels[idx].Permissions = permissions
}
// override model permissions with repo-specific permissions
if permissions, ok := nameToPermissions[allModels[idx].Name]; ok {
allModels[idx].Permissions = permissions
}
}
// Print Models Table
printModelsTable(allModels)
}
// printModelsTable prints the models table
func printModelsTable(models []Model) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Model", "Private", "Read", "Write"})
for _, model := range models {
var name, read, write, private string
if model.Permissions.Read {
read = color.New(color.FgGreen).Sprint("True")
} else {
read = "False"
}
if model.Permissions.Write {
write = color.New(color.FgGreen).Sprint("True")
} else {
write = "False"
}
if model.Private {
private = color.New(color.FgGreen).Sprint("True")
name = color.New(color.FgGreen).Sprint(model.Name)
} else {
private = "False"
name = model.Name
}
t.AppendRow([]interface{}{name, private, read, write})
}
t.Render()
}