Anthropic detector: surface API error detail on non-2xx verification (#5180)

This commit is contained in:
Ahsan-Sarbaz
2026-08-12 17:07:23 +05:00
committed by GitHub
parent 58cb799387
commit 353950021f
2 changed files with 132 additions and 14 deletions
+46 -14
View File
@@ -2,8 +2,9 @@ package anthropic
import (
"context"
"errors"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
@@ -59,22 +60,21 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
client = defaultClient
}
isAdminKey := isAdminKey(keyMatch)
var isVerified bool
var err error
var (
isVerified bool
verificationErr error
)
if isAdminKey {
isVerified, err = verifyAnthropicKey(ctx, client, adminKeyEndpoint, keyMatch)
if isAdminKey(keyMatch) {
s1.ExtraData["Type"] = "Admin Key"
} else if !isAdminKey {
isVerified, err = verifyAnthropicKey(ctx, client, apiKeyEndpoint, keyMatch)
s1.ExtraData["Type"] = "API Key"
isVerified, verificationErr = verifyAnthropicKey(ctx, client, adminKeyEndpoint, keyMatch)
} else {
return nil, errors.New("unknown key type detected for anthropic")
s1.ExtraData["Type"] = "API Key"
isVerified, verificationErr = verifyAnthropicKey(ctx, client, apiKeyEndpoint, keyMatch)
}
s1.Verified = isVerified
s1.SetVerificationError(err, keyMatch)
s1.SetVerificationError(verificationErr, keyMatch)
}
results = append(results, s1)
@@ -95,7 +95,7 @@ Endpoints:
func verifyAnthropicKey(ctx context.Context, client *http.Client, endpoint, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil {
return false, nil
return false, err
}
req.Header.Set("x-api-key", key)
@@ -106,7 +106,10 @@ func verifyAnthropicKey(ctx context.Context, client *http.Client, endpoint, key
if err != nil {
return false, err
}
defer func() { _ = res.Body.Close() }()
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
@@ -117,10 +120,39 @@ func verifyAnthropicKey(ctx context.Context, client *http.Client, endpoint, key
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
// A 400 is invalid_request_error, never a bad-key signal (that's always 401),
// so it must stay indeterminate rather than count as not-live.
return false, apiError(res)
}
}
// anthropicErrorResponse is the Anthropic API's error envelope for non-2xx responses.
// See https://platform.claude.com/docs/en/api/errors.
type anthropicErrorResponse struct {
Error struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error"`
}
const maxErrorBodySize = 4 << 10 // cap how much of the error body we bother parsing
// apiError enriches an unexpected status with the API's own error type/message when present.
func apiError(res *http.Response) error {
body, err := io.ReadAll(io.LimitReader(res.Body, maxErrorBodySize))
if err != nil {
return fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
var apiErr anthropicErrorResponse
if err := json.Unmarshal(body, &apiErr); err != nil || apiErr.Error.Type == "" {
return fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
return fmt.Errorf("unexpected HTTP response status %d (%s: %s)",
res.StatusCode, apiErr.Error.Type, apiErr.Error.Message)
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_Anthropic
}
+86
View File
@@ -2,15 +2,101 @@ package anthropic
import (
"context"
"net/http"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
func TestAnthropic_VerifyMatch(t *testing.T) {
const key = "sk-ant-api03-Dtjm9IZ_rYhS_ihHLZmPXhjJ6PN8UPp7vNO7qO3735RRDpf8xbWGinsch0McONXznUm-4KWoA7WU2otvvwHBR5QRjiLakAA"
tests := []struct {
name string
statusCode int
body string
wantVerified bool
wantErr bool
// wantErrContains, when set, must appear in the verification error. It pins that
// the API's own explanation survives into the error rather than being discarded.
wantErrContains string
}{
{
name: "200 is verified",
statusCode: http.StatusOK,
body: `{"data":[]}`,
wantVerified: true,
},
{
name: "401 is determinate not-live",
statusCode: http.StatusUnauthorized,
body: `{"type":"error","error":{"type":"authentication_error","message":"API key is invalid."}}`,
},
{
name: "404 is determinate not-live",
statusCode: http.StatusNotFound,
body: `{"type":"error","error":{"type":"not_found_error","message":"Not found"}}`,
},
{
// 400 is invalid_request_error: the API returns it for a malformed request,
// never for the state of the key (bad keys return 401). It must stay
// indeterminate, otherwise a request mangled in transit silently marks a live
// key as not-verified.
name: "400 stays indeterminate and keeps the API message",
statusCode: http.StatusBadRequest,
body: `{"type":"error","error":{"type":"invalid_request_error","message":"anthropic-version: header is required"}}`,
wantErr: true,
wantErrContains: "anthropic-version: header is required",
},
{
name: "429 stays indeterminate",
statusCode: http.StatusTooManyRequests,
body: `{"type":"error","error":{"type":"rate_limit_error","message":"Number of requests has exceeded your rate limit"}}`,
wantErr: true,
wantErrContains: "rate_limit_error",
},
{
name: "500 stays indeterminate",
statusCode: http.StatusInternalServerError,
body: `{"type":"error","error":{"type":"api_error","message":"Internal server error"}}`,
wantErr: true,
},
{
// A gateway that returns a non-JSON body must still degrade to a plain
// status-code error rather than panicking or losing the error entirely.
name: "unparseable body still yields an error",
statusCode: http.StatusBadRequest,
body: `<html>502 Bad Gateway</html>`,
wantErr: true,
wantErrContains: "unexpected HTTP response status 400",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := common.ConstantResponseHttpClient(test.statusCode, test.body)
verified, err := verifyAnthropicKey(context.Background(), client, apiKeyEndpoint, key)
if test.wantErr {
require.Error(t, err)
if test.wantErrContains != "" {
assert.Contains(t, err.Error(), test.wantErrContains)
}
} else {
require.NoError(t, err)
}
assert.Equal(t, test.wantVerified, verified)
})
}
}
func TestAnthropic_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})