Groq Analyzer (#3962)

* added groq analyzer

* print resource if present
This commit is contained in:
Kashif Khan
2025-03-07 12:23:14 +05:00
committed by GitHub
parent 3259c4bc43
commit 95a2a2a04c
10 changed files with 493 additions and 1 deletions
+2
View File
@@ -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 (
+158
View File
@@ -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()
}
+72
View File
@@ -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
+187
View File
@@ -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)
}
}
+3
View File
@@ -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"])
}
}
+6
View File
@@ -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)