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  }