Skip to content

Commit 7185b31

Browse files
anthropic api key analyzer (#3878)
* initial commit * added anthropic api key analyzer * added secret info type * resolved comments
1 parent ec42f44 commit 7185b31

File tree

11 files changed

+548
-50
lines changed

11 files changed

+548
-50
lines changed

pkg/analyzer/analyzers/analyzers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
const (
6262
AnalyzerTypeInvalid AnalyzerType = iota
6363
AnalyzerTypeAirbrake
64+
AnalyzerAnthropic
6465
AnalyzerTypeAsana
6566
AnalyzerTypeBitbucket
6667
AnalyzerTypeDockerHub
@@ -90,6 +91,7 @@ const (
9091
var analyzerTypeStrings = map[AnalyzerType]string{
9192
AnalyzerTypeInvalid: "Invalid",
9293
AnalyzerTypeAirbrake: "Airbrake",
94+
AnalyzerAnthropic: "Anthropic",
9395
AnalyzerTypeAsana: "Asana",
9496
AnalyzerTypeBitbucket: "Bitbucket",
9597
AnalyzerTypeDockerHub: "DockerHub",
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package anthropic
2+
3+
import (
4+
"errors"
5+
"os"
6+
7+
"github.com/fatih/color"
8+
"github.com/jedib0t/go-pretty/table"
9+
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
)
14+
15+
var _ analyzers.Analyzer = (*Analyzer)(nil)
16+
17+
const (
18+
// Key Types
19+
APIKey = "API-Key"
20+
)
21+
22+
type Analyzer struct {
23+
Cfg *config.Config
24+
}
25+
26+
// SecretInfo hold the information about the anthropic key
27+
type SecretInfo struct {
28+
Valid bool
29+
Type string // key type - TODO: Handle Anthropic Admin Keys
30+
Reference string
31+
AnthropicResources []AnthropicResource
32+
Permissions string // always full_access
33+
Misc map[string]string
34+
}
35+
36+
// AnthropicResource is any resource that can be accessed with anthropic key
37+
type AnthropicResource struct {
38+
ID string
39+
Name string
40+
Type string
41+
Metadata map[string]string
42+
}
43+
44+
func (a Analyzer) Type() analyzers.AnalyzerType {
45+
return analyzers.AnalyzerAnthropic
46+
}
47+
48+
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
49+
key, exist := credInfo["key"]
50+
if !exist {
51+
return nil, errors.New("key not found in credentials info")
52+
}
53+
54+
secretInfo, err := AnalyzePermissions(a.Cfg, key)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
return secretInfoToAnalyzerResult(secretInfo), nil
60+
}
61+
62+
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
63+
info, err := AnalyzePermissions(cfg, key)
64+
if err != nil {
65+
// just print the error in cli and continue as a partial success
66+
color.Red("[x] Error : %s", err.Error())
67+
}
68+
69+
if info == nil {
70+
color.Red("[x] Error : %s", "No information found")
71+
return
72+
}
73+
74+
if info.Valid {
75+
color.Green("[!] Valid Anthropic API key\n\n")
76+
// no user information
77+
// print full access permission
78+
printPermission(info.Permissions)
79+
// print resources
80+
printAnthropicResources(info.AnthropicResources)
81+
82+
color.Yellow("\n[i] Expires: Never")
83+
}
84+
}
85+
86+
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
87+
// create a HTTP client
88+
client := analyzers.NewAnalyzeClient(cfg)
89+
90+
var secretInfo = &SecretInfo{
91+
Type: APIKey, // TODO: implement Admin-Key type as well
92+
}
93+
94+
if err := listModels(client, key, secretInfo); err != nil {
95+
return nil, err
96+
}
97+
98+
if err := listMessageBatches(client, key, secretInfo); err != nil {
99+
return nil, err
100+
}
101+
102+
// anthropic key has full access only
103+
secretInfo.Permissions = PermissionStrings[FullAccess]
104+
secretInfo.Valid = true
105+
106+
return secretInfo, nil
107+
}
108+
109+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
110+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
111+
if info == nil {
112+
return nil
113+
}
114+
115+
result := analyzers.AnalyzerResult{
116+
AnalyzerType: analyzers.AnalyzerAnthropic,
117+
Metadata: map[string]any{"Valid_Key": info.Valid},
118+
Bindings: make([]analyzers.Binding, len(info.AnthropicResources)),
119+
}
120+
121+
// extract information to create bindings and append to result bindings
122+
for _, Anthropicresource := range info.AnthropicResources {
123+
binding := analyzers.Binding{
124+
Resource: analyzers.Resource{
125+
Name: Anthropicresource.Name,
126+
FullyQualifiedName: Anthropicresource.ID,
127+
Type: Anthropicresource.Type,
128+
Metadata: map[string]any{},
129+
},
130+
Permission: analyzers.Permission{
131+
Value: info.Permissions,
132+
},
133+
}
134+
135+
for key, value := range Anthropicresource.Metadata {
136+
binding.Resource.Metadata[key] = value
137+
}
138+
139+
result.Bindings = append(result.Bindings, binding)
140+
}
141+
142+
return &result
143+
}
144+
145+
func printPermission(permission string) {
146+
color.Yellow("[i] Permissions:")
147+
t := table.NewWriter()
148+
t.SetOutputMirror(os.Stdout)
149+
t.AppendHeader(table.Row{"Permission"})
150+
t.AppendRow(table.Row{color.GreenString(permission)})
151+
t.Render()
152+
}
153+
154+
func printAnthropicResources(resources []AnthropicResource) {
155+
color.Green("\n[i] Resources:")
156+
t := table.NewWriter()
157+
t.SetOutputMirror(os.Stdout)
158+
t.AppendHeader(table.Row{"Resource Type", "Resource ID", "Resource Name"})
159+
for _, resource := range resources {
160+
t.AppendRow(table.Row{color.GreenString(resource.Type), color.GreenString(resource.ID), color.GreenString(resource.Name)})
161+
}
162+
t.Render()
163+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package anthropic
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"testing"
7+
"time"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
)
14+
15+
//go:embed result_output.json
16+
var expectedOutput []byte
17+
18+
func TestAnalyzer_Analyze(t *testing.T) {
19+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
20+
defer cancel()
21+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
22+
if err != nil {
23+
t.Fatalf("could not get test secrets from GCP: %s", err)
24+
}
25+
26+
secret := testSecrets.MustGetField("ANTHROPIC")
27+
28+
tests := []struct {
29+
name string
30+
secret string
31+
want []byte // JSON string
32+
wantErr bool
33+
}{
34+
{
35+
name: "valid anthropic key",
36+
secret: secret,
37+
want: expectedOutput,
38+
wantErr: false,
39+
},
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
a := Analyzer{Cfg: &config.Config{}}
45+
got, err := a.Analyze(ctx, map[string]string{"key": tt.secret})
46+
if (err != nil) != tt.wantErr {
47+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
48+
return
49+
}
50+
51+
// Marshal the actual result to JSON
52+
gotJSON, err := json.Marshal(got)
53+
if err != nil {
54+
t.Fatalf("could not marshal got to JSON: %s", err)
55+
}
56+
57+
// Parse the expected JSON string
58+
var wantObj analyzers.AnalyzerResult
59+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
60+
t.Fatalf("could not unmarshal want JSON string: %s", err)
61+
}
62+
63+
// Marshal the expected result to JSON (to normalize)
64+
wantJSON, err := json.Marshal(wantObj)
65+
if err != nil {
66+
t.Fatalf("could not marshal want to JSON: %s", err)
67+
}
68+
69+
// Compare the JSON strings
70+
if string(gotJSON) != string(wantJSON) {
71+
// Pretty-print both JSON strings for easier comparison
72+
var gotIndented, wantIndented []byte
73+
gotIndented, err = json.MarshalIndent(got, "", " ")
74+
if err != nil {
75+
t.Fatalf("could not marshal got to indented JSON: %s", err)
76+
}
77+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
78+
if err != nil {
79+
t.Fatalf("could not marshal want to indented JSON: %s", err)
80+
}
81+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
82+
}
83+
})
84+
}
85+
}

pkg/analyzer/analyzers/anthropic/permissions.go

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
permissions:
2+
- full_access

0 commit comments

Comments
 (0)