377 lines
12 KiB
Go
377 lines
12 KiB
Go
package anthropic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
var endpoints = map[string]string{
|
|
// api key endpoints
|
|
"models": "https://api.anthropic.com/v1/models",
|
|
"messageBatches": "https://api.anthropic.com/v1/messages/batches",
|
|
|
|
// admin key endpoints
|
|
"orgUsers": "https://api.anthropic.com/v1/organizations/users",
|
|
"workspaces": "https://api.anthropic.com/v1/organizations/workspaces",
|
|
"workspaceMembers": "https://api.anthropic.com/v1/organizations/workspaces/%s/members", // require workspace id
|
|
"apiKeys": "https://api.anthropic.com/v1/organizations/api_keys",
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
type OrgUsersResponse struct {
|
|
Data []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type WorkspacesResponse struct {
|
|
Data []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type WorkspaceMembersResponse struct {
|
|
Data []struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
UserID string `json:"user_id"`
|
|
Type string `json:"type"`
|
|
WorkspaceRole string `json:"workspace_role"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type APIKeysResponse struct {
|
|
Data []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
CreatedBy struct {
|
|
ID string `json:"id"`
|
|
} `json:"created_by"`
|
|
PartialKeyHint string `json:"partial_key_hint"`
|
|
Status string `json:"status"`
|
|
} `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(ctx context.Context, client *http.Client, url, key string) ([]byte, int, error) {
|
|
// create request
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to create anthropic request", "url", url)
|
|
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 {
|
|
ctx.Logger().Error(err, "failed to send anthropic request", "url", url)
|
|
return nil, 0, err
|
|
}
|
|
|
|
defer func() {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
}()
|
|
|
|
responseBodyByte, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to read anthropic response body", "url", url)
|
|
return nil, 0, err
|
|
}
|
|
|
|
return responseBodyByte, resp.StatusCode, nil
|
|
}
|
|
|
|
// captureAPIKeyResources capture resources associated with api key
|
|
func captureAPIKeyResources(ctx context.Context, client *http.Client, apiKey string, secretInfo *SecretInfo) error {
|
|
if err := captureModels(ctx, client, apiKey, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := captureMessageBatches(ctx, client, apiKey, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// captureAdminKeyResources capture resources associated with admin key
|
|
func captureAdminKeyResources(ctx context.Context, client *http.Client, adminKey string, secretInfo *SecretInfo) error {
|
|
if err := captureOrgUsers(ctx, client, adminKey, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := captureWorkspaces(ctx, client, adminKey, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := captureAPIKeys(ctx, client, adminKey, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func captureModels(ctx context.Context, client *http.Client, apiKey string, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, endpoints["models"], apiKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var models ModelsResponse
|
|
|
|
if err := json.Unmarshal(response, &models); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal models response")
|
|
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:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching models", "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching models", "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func captureMessageBatches(ctx context.Context, client *http.Client, apiKey string, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, endpoints["messageBatches"], apiKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var messageBatches MessageResponse
|
|
|
|
if err := json.Unmarshal(response, &messageBatches); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal message batches response")
|
|
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:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching message batches", "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching message batches", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching message batches", "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func captureOrgUsers(ctx context.Context, client *http.Client, adminKey string, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, endpoints["orgUsers"], adminKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var users OrgUsersResponse
|
|
|
|
if err := json.Unmarshal(response, &users); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal org users response")
|
|
return err
|
|
}
|
|
|
|
for _, user := range users.Data {
|
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
|
|
ID: user.ID,
|
|
Name: user.Name,
|
|
Type: user.Type,
|
|
Metadata: map[string]string{
|
|
"Role": user.Role,
|
|
"Email": user.Email,
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
case http.StatusNotFound, http.StatusUnauthorized:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching org users", "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching org users", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching org users", "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func captureWorkspaces(ctx context.Context, client *http.Client, adminKey string, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, endpoints["workspaces"], adminKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var workspaces WorkspacesResponse
|
|
|
|
if err := json.Unmarshal(response, &workspaces); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal workspaces response")
|
|
return err
|
|
}
|
|
|
|
for _, workspace := range workspaces.Data {
|
|
resource := AnthropicResource{
|
|
ID: workspace.ID,
|
|
Name: workspace.Name,
|
|
Type: workspace.Type,
|
|
}
|
|
|
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, resource)
|
|
// capture each workspace members
|
|
if err := captureWorkspaceMembers(ctx, client, adminKey, resource, secretInfo); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
case http.StatusNotFound, http.StatusUnauthorized:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching workspaces", "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching workspaces", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching workspaces", "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func captureWorkspaceMembers(ctx context.Context, client *http.Client, key string, parentWorkspace AnthropicResource, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, fmt.Sprintf(endpoints["workspaceMembers"], parentWorkspace.ID), key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var members WorkspaceMembersResponse
|
|
|
|
if err := json.Unmarshal(response, &members); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal workspace members response", "workspace_id", parentWorkspace.ID)
|
|
return err
|
|
}
|
|
|
|
for _, member := range members.Data {
|
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
|
|
ID: fmt.Sprintf("anthropic/workspace/%s/member/%s", member.WorkspaceID, member.UserID),
|
|
Name: member.UserID,
|
|
Type: member.Type,
|
|
Parent: &parentWorkspace,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
case http.StatusNotFound, http.StatusUnauthorized:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching workspace members", "workspace_id", parentWorkspace.ID, "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching workspace members", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching workspace members", "workspace_id", parentWorkspace.ID, "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func captureAPIKeys(ctx context.Context, client *http.Client, adminKey string, secretInfo *SecretInfo) error {
|
|
response, statusCode, err := makeAnthropicRequest(ctx, client, endpoints["apiKeys"], adminKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var apiKeys APIKeysResponse
|
|
|
|
if err := json.Unmarshal(response, &apiKeys); err != nil {
|
|
ctx.Logger().Error(err, "failed to unmarshal api keys response")
|
|
return err
|
|
}
|
|
|
|
for _, apiKey := range apiKeys.Data {
|
|
secretInfo.AnthropicResources = append(secretInfo.AnthropicResources, AnthropicResource{
|
|
ID: apiKey.ID,
|
|
Name: apiKey.Name,
|
|
Type: apiKey.Type,
|
|
Metadata: map[string]string{
|
|
"WorkspaceID": apiKey.WorkspaceID,
|
|
"CreatedBy": apiKey.CreatedBy.ID,
|
|
"PartialKeyHint": apiKey.PartialKeyHint,
|
|
"Status": apiKey.Status,
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
case http.StatusNotFound, http.StatusUnauthorized:
|
|
err := fmt.Errorf("invalid/revoked api-key")
|
|
ctx.Logger().Error(err, "anthropic key invalid while fetching api keys", "status_code", statusCode)
|
|
return err
|
|
default:
|
|
err := fmt.Errorf("unexpected status code: %d while fetching api keys", statusCode)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching api keys", "status_code", statusCode)
|
|
return err
|
|
}
|
|
}
|