anthropic api key analyzer (#3878)
* initial commit * added anthropic api key analyzer * added secret info type * resolved comments
This commit is contained in:
@@ -61,6 +61,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
AnalyzerTypeInvalid AnalyzerType = iota
|
AnalyzerTypeInvalid AnalyzerType = iota
|
||||||
AnalyzerTypeAirbrake
|
AnalyzerTypeAirbrake
|
||||||
|
AnalyzerAnthropic
|
||||||
AnalyzerTypeAsana
|
AnalyzerTypeAsana
|
||||||
AnalyzerTypeBitbucket
|
AnalyzerTypeBitbucket
|
||||||
AnalyzerTypeDockerHub
|
AnalyzerTypeDockerHub
|
||||||
@@ -90,6 +91,7 @@ const (
|
|||||||
var analyzerTypeStrings = map[AnalyzerType]string{
|
var analyzerTypeStrings = map[AnalyzerType]string{
|
||||||
AnalyzerTypeInvalid: "Invalid",
|
AnalyzerTypeInvalid: "Invalid",
|
||||||
AnalyzerTypeAirbrake: "Airbrake",
|
AnalyzerTypeAirbrake: "Airbrake",
|
||||||
|
AnalyzerAnthropic: "Anthropic",
|
||||||
AnalyzerTypeAsana: "Asana",
|
AnalyzerTypeAsana: "Asana",
|
||||||
AnalyzerTypeBitbucket: "Bitbucket",
|
AnalyzerTypeBitbucket: "Bitbucket",
|
||||||
AnalyzerTypeDockerHub: "DockerHub",
|
AnalyzerTypeDockerHub: "DockerHub",
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package anthropic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Key Types
|
||||||
|
APIKey = "API-Key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Analyzer struct {
|
||||||
|
Cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretInfo hold the information about the anthropic key
|
||||||
|
type SecretInfo struct {
|
||||||
|
Valid bool
|
||||||
|
Type string // key type - TODO: Handle Anthropic Admin Keys
|
||||||
|
Reference string
|
||||||
|
AnthropicResources []AnthropicResource
|
||||||
|
Permissions string // always full_access
|
||||||
|
Misc map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnthropicResource is any resource that can be accessed with anthropic key
|
||||||
|
type AnthropicResource struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Analyzer) Type() analyzers.AnalyzerType {
|
||||||
|
return analyzers.AnalyzerAnthropic
|
||||||
|
}
|
||||||
|
|
||||||
|
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] Error : %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if info == nil {
|
||||||
|
color.Red("[x] Error : %s", "No information found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Valid {
|
||||||
|
color.Green("[!] Valid Anthropic API key\n\n")
|
||||||
|
// no user information
|
||||||
|
// print full access permission
|
||||||
|
printPermission(info.Permissions)
|
||||||
|
// print resources
|
||||||
|
printAnthropicResources(info.AnthropicResources)
|
||||||
|
|
||||||
|
color.Yellow("\n[i] Expires: Never")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
||||||
|
// create a HTTP client
|
||||||
|
client := analyzers.NewAnalyzeClient(cfg)
|
||||||
|
|
||||||
|
var secretInfo = &SecretInfo{
|
||||||
|
Type: APIKey, // TODO: implement Admin-Key type as well
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := listModels(client, key, secretInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := listMessageBatches(client, key, secretInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// anthropic key has full access only
|
||||||
|
secretInfo.Permissions = PermissionStrings[FullAccess]
|
||||||
|
secretInfo.Valid = true
|
||||||
|
|
||||||
|
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.AnthropicResources)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract information to create bindings and append to result bindings
|
||||||
|
for _, Anthropicresource := range info.AnthropicResources {
|
||||||
|
binding := analyzers.Binding{
|
||||||
|
Resource: analyzers.Resource{
|
||||||
|
Name: Anthropicresource.Name,
|
||||||
|
FullyQualifiedName: Anthropicresource.ID,
|
||||||
|
Type: Anthropicresource.Type,
|
||||||
|
Metadata: map[string]any{},
|
||||||
|
},
|
||||||
|
Permission: analyzers.Permission{
|
||||||
|
Value: info.Permissions,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range Anthropicresource.Metadata {
|
||||||
|
binding.Resource.Metadata[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Bindings = append(result.Bindings, binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPermission(permission string) {
|
||||||
|
color.Yellow("[i] Permissions:")
|
||||||
|
t := table.NewWriter()
|
||||||
|
t.SetOutputMirror(os.Stdout)
|
||||||
|
t.AppendHeader(table.Row{"Permission"})
|
||||||
|
t.AppendRow(table.Row{color.GreenString(permission)})
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAnthropicResources(resources []AnthropicResource) {
|
||||||
|
color.Green("\n[i] Resources:")
|
||||||
|
t := table.NewWriter()
|
||||||
|
t.SetOutputMirror(os.Stdout)
|
||||||
|
t.AppendHeader(table.Row{"Resource Type", "Resource ID", "Resource Name"})
|
||||||
|
for _, resource := range resources {
|
||||||
|
t.AppendRow(table.Row{color.GreenString(resource.Type), color.GreenString(resource.ID), color.GreenString(resource.Name)})
|
||||||
|
}
|
||||||
|
t.Render()
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package anthropic
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed result_output.json
|
||||||
|
var expectedOutput []byte
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := testSecrets.MustGetField("ANTHROPIC")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
secret string
|
||||||
|
want []byte // JSON string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid anthropic key",
|
||||||
|
secret: secret,
|
||||||
|
want: expectedOutput,
|
||||||
|
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.secret})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the expected JSON string
|
||||||
|
var wantObj analyzers.AnalyzerResult
|
||||||
|
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
|
||||||
|
t.Fatalf("could not unmarshal want JSON string: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the expected result to JSON (to normalize)
|
||||||
|
wantJSON, err := json.Marshal(wantObj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not marshal want to JSON: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the JSON strings
|
||||||
|
if string(gotJSON) != string(wantJSON) {
|
||||||
|
// 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(wantObj, "", " ")
|
||||||
|
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 anthropic
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package anthropic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelsResponse struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageResponse struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ProcessingStatus string `json:"processing_status"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
ResultsURL string `json:"results_url"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeAnthropicRequest send the API request to passed url with passed key as API Key and return response body and status code
|
||||||
|
func makeAnthropicRequest(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("x-api-key", key)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("anthropic-version", "2023-06-01")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func listModels(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||||
|
response, statusCode, err := makeAnthropicRequest(client, "https://api.anthropic.com/v1/models", key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
var models ModelsResponse
|
||||||
|
|
||||||
|
if err := json.Unmarshal(response, &models); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models.Data {
|
||||||
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
|
||||||
|
ID: model.ID,
|
||||||
|
Name: model.DisplayName,
|
||||||
|
Type: model.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case http.StatusNotFound, http.StatusUnauthorized:
|
||||||
|
return fmt.Errorf("invalid/revoked api-key")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMessageBatches(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||||
|
response, statusCode, err := makeAnthropicRequest(client, "https://api.anthropic.com/v1/messages/batches", key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
var messageBatches MessageResponse
|
||||||
|
|
||||||
|
if err := json.Unmarshal(response, &messageBatches); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, messageBatch := range messageBatches.Data {
|
||||||
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
|
||||||
|
ID: messageBatch.ID,
|
||||||
|
Name: "", // no name
|
||||||
|
Type: messageBatch.Type,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"expires_at": messageBatch.ExpiresAt,
|
||||||
|
"results_url": messageBatch.ResultsURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case http.StatusNotFound, http.StatusUnauthorized:
|
||||||
|
return fmt.Errorf("invalid/revoked api-key")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"AnalyzerType": 2,
|
||||||
|
"Bindings": [
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "Claude 3.5 Sonnet (New)",
|
||||||
|
"FullyQualifiedName": "claude-3-5-sonnet-20241022",
|
||||||
|
"Type": "model",
|
||||||
|
"Metadata": {},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "Claude 3.5 Haiku",
|
||||||
|
"FullyQualifiedName": "claude-3-5-haiku-20241022",
|
||||||
|
"Type": "model",
|
||||||
|
"Metadata": {},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "Claude 3.5 Sonnet (Old)",
|
||||||
|
"FullyQualifiedName": "claude-3-5-sonnet-20240620",
|
||||||
|
"Type": "model",
|
||||||
|
"Metadata": {},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "Claude 3 Haiku",
|
||||||
|
"FullyQualifiedName": "claude-3-haiku-20240307",
|
||||||
|
"Type": "model",
|
||||||
|
"Metadata": {},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "Claude 3 Opus",
|
||||||
|
"FullyQualifiedName": "claude-3-opus-20240229",
|
||||||
|
"Type": "model",
|
||||||
|
"Metadata": {},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Resource": {
|
||||||
|
"Name": "",
|
||||||
|
"FullyQualifiedName": "msgbatch_015FDqbx29LDeVvbwwyCe314",
|
||||||
|
"Type": "message_batch",
|
||||||
|
"Metadata": {
|
||||||
|
"expires_at": "2025-02-05T07:36:34.761695+00:00",
|
||||||
|
"results_url": "https://api.anthropic.com/v1/messages/batches/msgbatch_015FDqbx29LDeVvbwwyCe314/results"
|
||||||
|
},
|
||||||
|
"Parent": null
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"Value": "full_access",
|
||||||
|
"Parent": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UnboundedResources": null,
|
||||||
|
"Metadata": {
|
||||||
|
"Valid_Key": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/alecthomas/kingpin/v2"
|
"github.com/alecthomas/kingpin/v2"
|
||||||
|
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/anthropic"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
|
||||||
@@ -91,5 +92,7 @@ func Run(keyType string, secretInfo SecretInfo) {
|
|||||||
notion.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
notion.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
case "dockerhub":
|
case "dockerhub":
|
||||||
dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"])
|
dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"])
|
||||||
|
case "anthropic":
|
||||||
|
anthropic.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package anthropic
|
package anthropic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -56,6 +54,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
isVerified, err := verifyToken(ctx, client, resMatch)
|
isVerified, err := verifyToken(ctx, client, resMatch)
|
||||||
s1.Verified = isVerified
|
s1.Verified = isVerified
|
||||||
s1.SetVerificationError(err, resMatch)
|
s1.SetVerificationError(err, resMatch)
|
||||||
|
if s1.Verified {
|
||||||
|
s1.AnalysisInfo = map[string]string{
|
||||||
|
"key": resMatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, s1)
|
results = append(results, s1)
|
||||||
@@ -64,31 +67,13 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type response struct {
|
|
||||||
Error struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
} `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyToken(ctx context.Context, client *http.Client, apiKey string) (bool, error) {
|
func verifyToken(ctx context.Context, client *http.Client, apiKey string) (bool, error) {
|
||||||
body := map[string]any{
|
// https://docs.anthropic.com/en/api/models-list
|
||||||
"model": "claude-3-opus-20240229",
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.anthropic.com/v1/models", http.NoBody)
|
||||||
"max_tokens": 1024,
|
|
||||||
"messages": []map[string]string{
|
|
||||||
{"role": "user", "content": "Hello, world"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyBytes, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.anthropic.com/claude/reference/messages_post
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.anthropic.com/v1/messages", bytes.NewReader(bodyBytes))
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
req.Header.Set("x-api-key", apiKey)
|
req.Header.Set("x-api-key", apiKey)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("anthropic-version", "2023-06-01")
|
req.Header.Set("anthropic-version", "2023-06-01")
|
||||||
@@ -103,14 +88,8 @@ func verifyToken(ctx context.Context, client *http.Client, apiKey string) (bool,
|
|||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
||||||
case http.StatusBadRequest:
|
case http.StatusNotFound, http.StatusUnauthorized:
|
||||||
var resp response
|
// 404 is returned if api key is disabled or not found
|
||||||
if err = json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
|
||||||
return false, fmt.Errorf("unexpected HTTP response body: %w", err)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
|
|
||||||
case http.StatusUnauthorized:
|
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -104,23 +104,6 @@ func TestAnthropic_FromChunk(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
wantVerificationErr: true,
|
wantVerificationErr: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "found, verified but unexpected api surface",
|
|
||||||
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
data: []byte(fmt.Sprintf("You can find a anthropic secret %s within", secret)),
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
want: []detectors.Result{
|
|
||||||
{
|
|
||||||
DetectorType: detectorspb.DetectorType_Anthropic,
|
|
||||||
Verified: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
wantVerificationErr: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -137,7 +120,7 @@ func TestAnthropic_FromChunk(t *testing.T) {
|
|||||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
|
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
|
||||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||||
t.Errorf("Anthropic.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
t.Errorf("Anthropic.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validPattern = "sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA"
|
validPattern = `
|
||||||
|
System Log - Authentication Token Issued
|
||||||
|
Date: 2025-02-04 14:32:10 UTC
|
||||||
|
Server: api-secure-03.internal
|
||||||
|
Service: Anthropic API Gateway
|
||||||
|
API Key: sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA
|
||||||
|
|
||||||
|
Log Entry:
|
||||||
|
A new API key has been generated for service authentication. Please ensure that this key remains confidential and is not exposed in any public repositories or logs.
|
||||||
|
`
|
||||||
invalidPattern = "sk-ant-api03-abc123xyz-456de-klMnopqrstuvwx-3456yza789bcde-1234fghijklmnopAA"
|
invalidPattern = "sk-ant-api03-abc123xyz-456de-klMnopqrstuvwx-3456yza789bcde-1234fghijklmnopAA"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +36,7 @@ func TestAnthropic_Pattern(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid pattern",
|
name: "valid pattern",
|
||||||
input: validPattern,
|
input: validPattern,
|
||||||
want: []string{validPattern},
|
want: []string{"sk-ant-api03-abc123xyz-456def789ghij-klmnopqrstuvwx-3456yza789bcde-1234fghijklmnopby56aaaogaopaaaabc123xyzAA"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid pattern",
|
name: "invalid pattern",
|
||||||
|
|||||||
Reference in New Issue
Block a user