github.com/google/osv-scalibr@v0.4.1/enricher/huggingfacemeta/huggingfacemeta.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package huggingfacemeta contains an Enricher that adds additional metadata 16 // to each Huggingface keys based on the API response 17 package huggingfacemeta 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 26 "github.com/google/osv-scalibr/enricher" 27 "github.com/google/osv-scalibr/inventory" 28 "github.com/google/osv-scalibr/plugin" 29 "github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey" 30 ) 31 32 const ( 33 // Name is the unique name of this Enricher. 34 Name = "huggingfacemeta/velesvalidate" 35 36 version = 1 37 defaultBaseURL = "https://huggingface.co" 38 ) 39 40 var _ enricher.Enricher = &Enricher{} 41 42 // Enricher uses a Veles ValidationEngine to validate Secrets found by Veles. 43 type Enricher struct { 44 baseURL string 45 httpClient *http.Client 46 } 47 48 // New creates a new Enricher using the default Veles Validators. 49 func New() enricher.Enricher { 50 return &Enricher{ 51 baseURL: defaultBaseURL, 52 httpClient: http.DefaultClient, 53 } 54 } 55 56 // NewWithBaseURL creates a new Enricher that uses the provided base URL for the Hugging Face API. 57 // Useful for tests with a httptest.Server. 58 func NewWithBaseURL(baseURL string) enricher.Enricher { 59 return &Enricher{ 60 baseURL: baseURL, 61 httpClient: http.DefaultClient, 62 } 63 } 64 65 // Name of the Enricher. 66 func (Enricher) Name() string { 67 return Name 68 } 69 70 // Version of the Enricher. 71 func (Enricher) Version() int { 72 return version 73 } 74 75 // Requirements of the Enricher. 76 // Needs network access so it can validate Secrets. 77 func (Enricher) Requirements() *plugin.Capabilities { 78 return &plugin.Capabilities{ 79 Network: plugin.NetworkOnline, 80 } 81 } 82 83 // RequiredPlugins returns the plugins that are required to be enabled for this 84 // Enricher to run. While it works on the results of the filesystem/secrets 85 // Extractor, the Enricher itself can run independently. 86 func (Enricher) RequiredPlugins() []string { 87 return []string{} 88 } 89 90 // huggingfaceResponse represents the minimal structure needed from the Hugging Face API response 91 type huggingfaceResponse struct { 92 Auth struct { 93 AccessToken struct { 94 Role string `json:"role"` 95 FineGrained struct { 96 Scoped []struct { 97 Permissions []string `json:"permissions"` 98 } `json:"scoped"` 99 } `json:"fineGrained"` 100 } `json:"accessToken"` 101 } `json:"auth"` 102 } 103 104 // Enrich validates all the Secrets from the Inventory using a Veles 105 // ValidationEngine. 106 // Each individual Secret maintains its own error in case the validation failed. 107 func (e *Enricher) Enrich(ctx context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error { 108 for _, s := range inv.Secrets { 109 if err := ctx.Err(); err != nil { 110 return err 111 } 112 if huggingSecret, ok := s.Secret.(huggingfaceapikey.HuggingfaceAPIKey); ok { 113 url := e.baseURL + "/api/whoami-v2" 114 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 115 if err != nil { 116 return fmt.Errorf("creating HTTP GET request failed: %w", err) 117 } 118 req.Header.Set("Authorization", "Bearer "+huggingSecret.Key) 119 res, err := e.httpClient.Do(req) 120 if err != nil { 121 return fmt.Errorf("HTTP GET failed: %w", err) 122 } 123 defer res.Body.Close() 124 125 if res.StatusCode == http.StatusOK { 126 body, err := io.ReadAll(res.Body) 127 if err != nil { 128 return fmt.Errorf("reading response body failed: %w", err) 129 } 130 131 var apiResponse huggingfaceResponse 132 if err := json.Unmarshal(body, &apiResponse); err != nil { 133 return fmt.Errorf("parsing JSON response failed: %w", err) 134 } 135 136 // Extract all permissions from scoped entities 137 var permissions []string 138 for _, scopedItem := range apiResponse.Auth.AccessToken.FineGrained.Scoped { 139 permissions = append(permissions, scopedItem.Permissions...) 140 } 141 142 // Update the secret with the actual values from the response 143 s.Secret = huggingfaceapikey.HuggingfaceAPIKey{ 144 Key: huggingSecret.Key, 145 Role: apiResponse.Auth.AccessToken.Role, 146 FineGrainedScope: permissions, 147 } 148 } 149 } 150 } 151 return nil 152 }