Groq Analyzer (#3962)
* added groq analyzer * print resource if present
This commit is contained in:
@@ -89,6 +89,7 @@ const (
|
||||
AnalyzerTypePlanetScale
|
||||
AnalyzerTypeAirtableOAuth
|
||||
AnalyzerTypeAirtablePat
|
||||
AnalyzerTypeGroq
|
||||
// Add new items here with AnalyzerType prefix
|
||||
)
|
||||
|
||||
@@ -124,6 +125,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
||||
AnalyzerTypePlanetScale: "PlanetScale",
|
||||
AnalyzerTypeAirtableOAuth: "AirtableOAuth",
|
||||
AnalyzerTypeAirtablePat: "AirtablePat",
|
||||
AnalyzerTypeGroq: "Groq",
|
||||
// Add new mappings here
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go anthropic
|
||||
package anthropic
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go elevenlabs
|
||||
//go:generate generate_permissions permissions.yaml permissions.go dockerhub
|
||||
package dockerhub
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go groq
|
||||
package groq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// SecretInfo hold the information about the anthropic key
|
||||
type SecretInfo struct {
|
||||
Valid bool
|
||||
Reference string
|
||||
GroqResources []GroqResource
|
||||
Permissions []string
|
||||
Misc map[string]string
|
||||
}
|
||||
|
||||
// GroqResource is a single groq resource which can be accessed with groq api key
|
||||
type GroqResource struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Permission string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// appendGroqResource append the single groq resource to secretinfo groqresources list
|
||||
func (s *SecretInfo) appendGroqResource(resource GroqResource) {
|
||||
s.GroqResources = append(s.GroqResources, resource)
|
||||
}
|
||||
|
||||
// updateMetadata safely update the metadata of the groq resource
|
||||
func (g GroqResource) updateMetadata(key, value string) {
|
||||
if g.Metadata == nil {
|
||||
g.Metadata = map[string]string{}
|
||||
}
|
||||
|
||||
g.Metadata[key] = value
|
||||
}
|
||||
|
||||
func (a Analyzer) Type() analyzers.AnalyzerType {
|
||||
return analyzers.AnalyzerTypeGroq
|
||||
}
|
||||
|
||||
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||
key, exist := credInfo["key"]
|
||||
if !exist {
|
||||
return nil, errors.New("key not found in credentials info")
|
||||
}
|
||||
|
||||
secretInfo, err := AnalyzePermissions(a.Cfg, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretInfoToAnalyzerResult(secretInfo), nil
|
||||
}
|
||||
|
||||
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
||||
info, err := AnalyzePermissions(cfg, key)
|
||||
if err != nil {
|
||||
// just print the error in cli and continue as a partial success
|
||||
color.Red("[x] Invalid Anthropic API key\n")
|
||||
color.Red("[x] Error : %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
color.Red("[x] Error : %s", "No information found")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("[i] Valid Anthropic API key\n")
|
||||
color.Yellow("\n[i] Permission: Full Access\n")
|
||||
|
||||
if len(info.GroqResources) > 0 {
|
||||
printGroqResources(info.GroqResources)
|
||||
}
|
||||
|
||||
color.Yellow("\n[!] Expires: Never")
|
||||
}
|
||||
|
||||
func AnalyzePermissions(cfg *config.Config, apiKey string) (*SecretInfo, error) {
|
||||
// create a HTTP client
|
||||
client := analyzers.NewAnalyzeClient(cfg)
|
||||
|
||||
var secretInfo = &SecretInfo{Valid: true}
|
||||
|
||||
if err := captureBatches(client, apiKey, secretInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := captureFiles(client, apiKey, secretInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
|
||||
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerAnthropic,
|
||||
Metadata: map[string]any{"Valid_Key": info.Valid},
|
||||
Bindings: make([]analyzers.Binding, len(info.GroqResources)),
|
||||
}
|
||||
|
||||
// extract information to create bindings and append to result bindings
|
||||
for _, groqResource := range info.GroqResources {
|
||||
binding := analyzers.Binding{
|
||||
Resource: analyzers.Resource{
|
||||
Name: groqResource.Name,
|
||||
FullyQualifiedName: groqResource.ID,
|
||||
Type: groqResource.Type,
|
||||
Metadata: map[string]any{},
|
||||
},
|
||||
Permission: analyzers.Permission{
|
||||
Value: groqResource.Permission,
|
||||
},
|
||||
}
|
||||
|
||||
for key, value := range groqResource.Metadata {
|
||||
binding.Resource.Metadata[key] = value
|
||||
}
|
||||
|
||||
result.Bindings = append(result.Bindings, binding)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func printGroqResources(resources []GroqResource) {
|
||||
color.Green("\n[i] Resources:")
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"Name", "Type"})
|
||||
for _, resource := range resources {
|
||||
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
|
||||
}
|
||||
t.Render()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package groq
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
apiKey := testSecrets.MustGetField("GROQ")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
apiKey string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid dockerhub credentials",
|
||||
apiKey: apiKey,
|
||||
want: `{"AnalyzerType":2,"Bindings":[],"UnboundedResources":null,"Metadata":{"Valid_Key":true}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := Analyzer{Cfg: &config.Config{}}
|
||||
got, err := a.Analyze(ctx, map[string]string{"key": tt.apiKey})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// marshal the actual result to JSON
|
||||
gotJSON, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to JSON: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(gotJSON))
|
||||
|
||||
// compare the JSON strings
|
||||
if string(gotJSON) != string(tt.want) {
|
||||
// pretty-print both JSON strings for easier comparison
|
||||
var gotIndented, wantIndented []byte
|
||||
gotIndented, err = json.MarshalIndent(got, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to indented JSON: %s", err)
|
||||
}
|
||||
wantIndented, err = json.MarshalIndent(tt.want, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal want to indented JSON: %s", err)
|
||||
}
|
||||
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
package groq
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
FullAccess Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
FullAccess: "full_access",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"full_access": FullAccess,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
FullAccess: 1,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: FullAccess,
|
||||
}
|
||||
)
|
||||
|
||||
// ToString converts a Permission enum to its string representation
|
||||
func (p Permission) ToString() (string, error) {
|
||||
if str, ok := PermissionStrings[p]; ok {
|
||||
return str, nil
|
||||
}
|
||||
return "", errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// ToID converts a Permission enum to its ID
|
||||
func (p Permission) ToID() (int, error) {
|
||||
if id, ok := PermissionIDs[p]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// PermissionFromString converts a string representation to its Permission enum
|
||||
func PermissionFromString(s string) (Permission, error) {
|
||||
if p, ok := StringToPermission[s]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission string")
|
||||
}
|
||||
|
||||
// PermissionFromID converts an ID to its Permission enum
|
||||
func PermissionFromID(id int) (Permission, error) {
|
||||
if p, ok := IdToPermission[id]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission ID")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
permissions:
|
||||
- full_access # by default groq api key has full access
|
||||
@@ -0,0 +1,187 @@
|
||||
package groq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
permissionErr = "permissions_error"
|
||||
notAvailableForPlan = "not_available_for_plan"
|
||||
)
|
||||
|
||||
// errorResponse is the response from groq APIs in case of any error
|
||||
type errorResponse struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// listBatchesResponse is the response of /v1/batches API
|
||||
type listBatchesResponse struct {
|
||||
Data []batch `json:"data"`
|
||||
}
|
||||
|
||||
// batch represent a single batch inside batches list
|
||||
type batch struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
InputFileID string `json:"input_file_id"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
// listBatchesResponse is the response of /v1/files API
|
||||
type listFilesResponse struct {
|
||||
Data []file `json:"data"`
|
||||
}
|
||||
|
||||
// file represents a single file object inside files list
|
||||
type file struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Filename string `json:"filename"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
|
||||
func isPermissionError(err errorResponse) bool {
|
||||
// has permissions error or not available for the plan subscribed
|
||||
if err.Error.Type == permissionErr && err.Error.Code == notAvailableForPlan {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// makeGroqRequest send the API request to passed url with passed key as API Key and return response body and status code
|
||||
func makeGroqRequest(client *http.Client, url, key string) ([]byte, int, error) {
|
||||
// create request
|
||||
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// add required keys in the header
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
responseBodyByte, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return responseBodyByte, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// docs: https://console.groq.com/docs/api-reference#batches-list
|
||||
func captureBatches(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
response, statusCode, err := makeGroqRequest(client, "https://api.groq.com/openai/v1/batches", key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var batches listBatchesResponse
|
||||
|
||||
if err := json.Unmarshal(response, &batches); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, batch := range batches.Data {
|
||||
resource := GroqResource{
|
||||
ID: batch.ID,
|
||||
Name: batch.ID, // no specific name for batch
|
||||
Type: batch.Object,
|
||||
Permission: PermissionStrings[FullAccess],
|
||||
}
|
||||
|
||||
resource.updateMetadata("status", batch.Status)
|
||||
resource.updateMetadata("endpoint", batch.Endpoint)
|
||||
resource.updateMetadata("input file id", batch.InputFileID)
|
||||
resource.updateMetadata("expires at", time.Unix(batch.ExpiresAt, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
|
||||
|
||||
secretInfo.appendGroqResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
var errResp errorResponse
|
||||
|
||||
if err := json.Unmarshal(response, &errResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPermissionError(errResp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected error: %s", errResp.Error.Message)
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// docs: https://console.groq.com/docs/api-reference#files-list
|
||||
func captureFiles(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
response, statusCode, err := makeGroqRequest(client, "https://api.groq.com/openai/v1/files", key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var files listFilesResponse
|
||||
|
||||
if err := json.Unmarshal(response, &files); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files.Data {
|
||||
resource := GroqResource{
|
||||
ID: file.ID,
|
||||
Name: file.Filename,
|
||||
Type: file.Object,
|
||||
Permission: PermissionStrings[FullAccess],
|
||||
}
|
||||
|
||||
resource.updateMetadata("purpose", file.Purpose)
|
||||
resource.updateMetadata("created at", time.Unix(file.CreatedAt, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
|
||||
|
||||
secretInfo.appendGroqResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
var errResp errorResponse
|
||||
|
||||
if err := json.Unmarshal(response, &errResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPermissionError(errResp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected error: %s", errResp.Error.Message)
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/elevenlabs"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/groq"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/huggingface"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailchimp"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun"
|
||||
@@ -109,5 +110,7 @@ func Run(keyType string, secretInfo SecretInfo) {
|
||||
airtableoauth.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "airtablepat":
|
||||
airtablepat.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "groq":
|
||||
groq.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
s1.Verified = isVerified
|
||||
s1.ExtraData = extraData
|
||||
s1.SetVerificationError(verificationErr, match)
|
||||
|
||||
if isVerified {
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": match,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
|
||||
Reference in New Issue
Block a user