updated anthropic analyzer to analyze admin keys (#3973)

This commit is contained in:
Kashif Khan
2025-03-13 14:27:34 +05:00
committed by GitHub
parent e162ec2d79
commit 3e6c0a502e
2 changed files with 262 additions and 14 deletions
+38 -10
View File
@@ -4,6 +4,7 @@ package anthropic
import (
"errors"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
@@ -17,7 +18,8 @@ var _ analyzers.Analyzer = (*Analyzer)(nil)
const (
// Key Types
APIKey = "API-Key"
APIKey = "API-Key"
AdminKey = "Admin-Key"
)
type Analyzer struct {
@@ -28,7 +30,6 @@ type Analyzer struct {
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
@@ -39,6 +40,7 @@ type AnthropicResource struct {
ID string
Name string
Type string
Parent *AnthropicResource
Metadata map[string]string
}
@@ -73,7 +75,7 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
}
if info.Valid {
color.Green("[!] Valid Anthropic API key\n\n")
color.Green("[!] Valid Anthropic %s\n\n", info.Type)
// no user information
// print full access permission
printPermission(info.Permissions)
@@ -88,16 +90,23 @@ func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create a HTTP client
client := analyzers.NewAnalyzeClient(cfg)
keyType := getKeyType(key)
var secretInfo = &SecretInfo{
Type: APIKey, // TODO: implement Admin-Key type as well
Type: keyType,
}
if err := listModels(client, key, secretInfo); err != nil {
return nil, err
}
if err := listMessageBatches(client, key, secretInfo); err != nil {
return nil, err
switch keyType {
case APIKey:
if err := captureAPIKeyResources(client, key, secretInfo); err != nil {
return nil, err
}
case AdminKey:
if err := captureAdminKeyResources(client, key, secretInfo); err != nil {
return nil, err
}
default:
return nil, errors.New("unsupported key type")
}
// anthropic key has full access only
@@ -133,6 +142,14 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
},
}
if Anthropicresource.Parent != nil {
binding.Resource.Parent = &analyzers.Resource{
Name: Anthropicresource.Parent.Name,
FullyQualifiedName: Anthropicresource.Parent.ID,
Type: Anthropicresource.Parent.Type,
}
}
for key, value := range Anthropicresource.Metadata {
binding.Resource.Metadata[key] = value
}
@@ -162,3 +179,14 @@ func printAnthropicResources(resources []AnthropicResource) {
}
t.Render()
}
// getKeyType return the type of key
func getKeyType(key string) string {
if strings.Contains(key, "sk-ant-admin01") {
return AdminKey
} else if strings.Contains(key, "sk-ant-api03") {
return APIKey
}
return ""
}
+224 -4
View File
@@ -7,6 +7,18 @@ import (
"net/http"
)
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"`
@@ -25,6 +37,47 @@ type MessageResponse struct {
} `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(client *http.Client, url, key string) ([]byte, int, error) {
// create request
@@ -56,8 +109,38 @@ func makeAnthropicRequest(client *http.Client, url, key string) ([]byte, int, er
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)
// captureAPIKeyResources capture resources associated with api key
func captureAPIKeyResources(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
if err := captureModels(client, apiKey, secretInfo); err != nil {
return err
}
if err := captureMessageBatches(client, apiKey, secretInfo); err != nil {
return err
}
return nil
}
// captureAdminKeyResources capture resources associated with admin key
func captureAdminKeyResources(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
if err := captureOrgUsers(client, adminKey, secretInfo); err != nil {
return err
}
if err := captureWorkspaces(client, adminKey, secretInfo); err != nil {
return err
}
if err := captureAPIKeys(client, adminKey, secretInfo); err != nil {
return err
}
return nil
}
func captureModels(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["models"], apiKey)
if err != nil {
return err
}
@@ -86,8 +169,8 @@ func listModels(client *http.Client, key string, secretInfo *SecretInfo) error {
}
}
func listMessageBatches(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, "https://api.anthropic.com/v1/messages/batches", key)
func captureMessageBatches(client *http.Client, apiKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(client, endpoints["messageBatches"], apiKey)
if err != nil {
return err
}
@@ -119,3 +202,140 @@ func listMessageBatches(client *http.Client, key string, secretInfo *SecretInfo)
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureOrgUsers(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(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 {
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:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureWorkspaces(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(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 {
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(client, adminKey, resource, secretInfo); err != nil {
return err
}
}
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 captureWorkspaceMembers(client *http.Client, key string, parentWorkspace AnthropicResource, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(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 {
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:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}
func captureAPIKeys(client *http.Client, adminKey string, secretInfo *SecretInfo) error {
response, statusCode, err := makeAnthropicRequest(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 {
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:
return fmt.Errorf("invalid/revoked api-key")
default:
return fmt.Errorf("unexpected status code: %d while fetching models", statusCode)
}
}