Files
trufflehog/pkg/analyzer/analyzers/stripe/stripe.go
Bill Rich abecab0d8f
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Snifftest / Run Snifftest (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Remove proto (#3489)
Only used for tracking analyzer types
2024-10-21 17:16:26 -07:00

385 lines
9.3 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go stripe
package stripe
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"sort"
"strings"
"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"
"gopkg.in/yaml.v2"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeStripe }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := &analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeStripe,
Metadata: map[string]any{
"key_type": info.KeyType,
"key_env": info.KeyEnv,
},
}
// create list of bindings using permissions, with category being the parent and unbounded resource
result.Bindings = []analyzers.Binding{}
result.UnboundedResources = []analyzers.Resource{}
for _, permissionCategory := range info.Permissions {
parentResource := &analyzers.Resource{
Name: permissionCategory.Name,
FullyQualifiedName: permissionCategory.Name,
Type: "category",
Metadata: nil,
Parent: nil,
}
if len(permissionCategory.Permissions) == 0 {
result.UnboundedResources = append(result.UnboundedResources, *parentResource)
} else {
for _, permission := range permissionCategory.Permissions {
if _, ok := StringToPermission[*permission.Value]; !ok { // skip unknown scopes/permission
continue
}
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *parentResource,
Permission: analyzers.Permission{
Value: fmt.Sprintf("%s:%s", permission.Name, *permission.Value),
},
})
}
}
}
return result
}
const (
SECRET_PREFIX = "sk_"
PUBLISHABLE_PREFIX = "pk_"
RESTRICTED_PREFIX = "rk_"
LIVE_PREFIX = "live_"
TEST_PREFIX = "test_"
SECRET = "Secret"
PUBLISHABLE = "Publishable"
RESTRICTED = "Restricted"
LIVE = "Live"
TEST = "Test"
)
//go:embed restricted.yaml
var restrictedConfig []byte
type PermissionStruct struct {
Name string
Value *string
}
type PermissionsCategory struct {
Name string
Permissions []PermissionStruct
}
type HttpStatusTest struct {
Endpoint string `yaml:"Endpoint"`
Method string `yaml:"Method"`
Payload interface{} `yaml:"Payload"`
ValidStatuses []int `yaml:"Valid"`
InvalidStatuses []int `yaml:"Invalid"`
}
type Category map[string]map[string]HttpStatusTest
type Config struct {
Categories map[string]Category `yaml:"categories"`
}
type SecretInfo struct {
KeyType string
KeyEnv string
Valid bool
Permissions []PermissionsCategory
}
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
// If body data, marshal to JSON
var data io.Reader
if h.Payload != nil {
jsonData, err := json.Marshal(h.Payload)
if err != nil {
return false, err
}
data = bytes.NewBuffer(jsonData)
}
// Create new HTTP request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest(h.Method, h.Endpoint, data)
if err != nil {
return false, err
}
// Add custom headers if provided
for key, value := range headers {
req.Header.Set(key, value)
}
// Execute HTTP Request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check response status code
switch {
case StatusContains(resp.StatusCode, h.ValidStatuses):
return true, nil
case StatusContains(resp.StatusCode, h.InvalidStatuses):
return false, nil
default:
fmt.Println(h)
fmt.Println(resp.Body)
fmt.Println(resp.StatusCode)
return false, errors.New("error checking response status code")
}
}
func StatusContains(status int, vals []int) bool {
for _, v := range vals {
if status == v {
return true
}
}
return false
}
func checkKeyType(key string) (string, error) {
if strings.HasPrefix(key, SECRET_PREFIX) {
return SECRET, nil
} else if strings.HasPrefix(key, PUBLISHABLE_PREFIX) {
return PUBLISHABLE, nil
} else if strings.HasPrefix(key, RESTRICTED_PREFIX) {
return RESTRICTED, nil
}
return "", errors.New("Invalid Stripe key format")
}
func checkKeyEnv(key string) (string, error) {
//remove first 3 characters
key = key[3:]
if strings.HasPrefix(key, LIVE_PREFIX) {
return LIVE, nil
}
if strings.HasPrefix(key, TEST_PREFIX) {
return TEST, nil
}
return "", errors.New("invalid Stripe key format")
}
func checkValidity(cfg *config.Config, key string) (bool, error) {
// Create a new request
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.stripe.com/v1/charges", nil)
if err != nil {
return false, err
}
// Add Authorization header
req.Header.Add("Authorization", "Bearer "+key)
// Send the request
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check the response. Valid is 200 (secret/restricted) or 403 (restricted)
if resp.StatusCode == 200 || resp.StatusCode == 403 {
return true, nil
}
return false, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// Check if secret, publishable, or restricted key
var keyType, keyEnv string
keyType, err := checkKeyType(key)
if err != nil {
return nil, err
}
// Check if live or test key
keyEnv, err = checkKeyEnv(key)
if err != nil {
return nil, err
}
// Check if key is valid
valid, err := checkValidity(cfg, key)
if err != nil {
return nil, err
}
permissions, err := getRestrictedPermissions(cfg, key)
if err != nil {
return nil, err
}
// Additional details
// get total customers
// get total charges
return &SecretInfo{
KeyType: keyType,
KeyEnv: keyEnv,
Valid: valid,
Permissions: permissions,
}, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
if info.KeyType == PUBLISHABLE {
color.Red("[x] This is a publishable Stripe key. It is not considered secret.")
return
}
if !info.Valid {
color.Red("[x] Invalid Stripe API Key\n")
return
}
color.Green("[!] Valid Stripe API Key\n\n")
if info.KeyType == SECRET {
color.Green("[i] Key Type: %s", info.KeyType)
} else if info.KeyType == RESTRICTED {
color.Yellow("[i] Key Type: %s", info.KeyType)
}
if info.KeyEnv == LIVE {
color.Green("[i] Key Environment: %s", info.KeyEnv)
} else if info.KeyEnv == TEST {
color.Red("[i] Key Environment: %s", info.KeyEnv)
}
fmt.Println("")
if info.KeyType == SECRET {
color.Green("[i] Permissions: Full Access")
return
}
printRestrictedPermissions(info.Permissions, cfg.ShowAll)
}
func getRestrictedPermissions(cfg *config.Config, key string) ([]PermissionsCategory, error) {
var config Config
if err := yaml.Unmarshal(restrictedConfig, &config); err != nil {
fmt.Println("Error unmarshalling YAML:", err)
return nil, err
}
output := make([]PermissionsCategory, 0)
for category, scopes := range config.Categories {
permissions := make([]PermissionStruct, 0)
for name, scope := range scopes {
value := ""
testCount := 0
for typ, test := range scope {
if test.Endpoint == "" {
continue
}
testCount++
status, err := test.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key})
if err != nil {
color.Red("[x] Error running test: %s", err.Error())
return nil, err
}
if status {
value = typ
}
if value == "Write" {
break
}
}
if testCount > 0 {
permissions = append(permissions, PermissionStruct{Name: name, Value: &value})
}
}
output = append(output, PermissionsCategory{Name: category, Permissions: permissions})
}
// sort the output
order := []string{"Core", "Checkout", "Billing", "Connect", "Orders", "Issuing", "Reporting", "Identity", "Webhook", "Stripe CLI", "Payment Links", "Terminal", "Tax", "Radar", "Climate"}
// ToDo: order the permissions within each category
// Create a map for quick lookup of the order
orderMap := make(map[string]int)
for i, name := range order {
orderMap[name] = i
}
// Sort the categories according to the desired order
sort.Slice(output, func(i, j int) bool {
return orderMap[output[i].Name] < orderMap[output[j].Name]
})
return output, nil
}
func printRestrictedPermissions(permissions []PermissionsCategory, show_all bool) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission", "Access"})
for _, category := range permissions {
for _, permission := range category.Permissions {
if *permission.Value != "" || show_all {
t.AppendRow([]interface{}{category.Name, permission.Name, *permission.Value})
}
}
}
t.Render()
}