mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 07:04:24 +00:00
[analyze] Add analyzer interface for Shopify (#3226)
* implement analyzer interface for shopify * fixed shopify analyzer according to new code and generated permissions * shopify analyzer test added * [chore] - key validations - linked analyzer with detectors * [chore] - moved redundant initialize to global. * [chore] moved expected output of test in json file to neat the code. * [Fixes] - Fixed permission and category resource issue in shopify analyzer - corrected test for shopify analyzer --------- Co-authored-by: Abdul Basit <abasit@folio3.com>
This commit is contained in:
parent
57e58123f1
commit
dc9c9a30b3
7 changed files with 441 additions and 3 deletions
177
pkg/analyzer/analyzers/shopify/expected_output.json
Normal file
177
pkg/analyzer/analyzers/shopify/expected_output.json
Normal file
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"AnalyzerType": 15,
|
||||
"Bindings": [
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Analytics",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Analytics",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "read",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Applications",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Applications",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "read",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Assigned fulfillment orders",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Assigned fulfillment orders",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Customers",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Customers",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Discovery",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Discovery",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Merchant-managed fulfillment orders",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Merchant-managed fulfillment orders",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Reports",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/Reports",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "cart_transforms",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com/cart_transforms",
|
||||
"Type": "category",
|
||||
"Metadata": null,
|
||||
"Parent": {
|
||||
"Name": "My Store",
|
||||
"FullyQualifiedName": "727f01-d6.myshopify.com/detectors@trufflesec.com",
|
||||
"Type": "shop",
|
||||
"Metadata": {
|
||||
"created_at": "2024-08-16T17:16:17+05:00"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "full_access",
|
||||
"Parent": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"UnboundedResources": null,
|
||||
"Metadata": {
|
||||
"status_code": 200
|
||||
}
|
||||
}
|
71
pkg/analyzer/analyzers/shopify/permissions.go
Normal file
71
pkg/analyzer/analyzers/shopify/permissions.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
package shopify
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Read Permission = iota
|
||||
Write Permission = iota
|
||||
FullAccess Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
Read: "read",
|
||||
Write: "write",
|
||||
FullAccess: "full_access",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"read": Read,
|
||||
"write": Write,
|
||||
"full_access": FullAccess,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
Read: 1,
|
||||
Write: 2,
|
||||
FullAccess: 3,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: Read,
|
||||
2: Write,
|
||||
3: FullAccess,
|
||||
}
|
||||
)
|
||||
|
||||
// ToString converts a Permission enum to its string representation
|
||||
func (p Permission) ToString() (string, error) {
|
||||
if str, ok := PermissionStrings[p]; ok {
|
||||
return str, nil
|
||||
}
|
||||
return "", errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// ToID converts a Permission enum to its ID
|
||||
func (p Permission) ToID() (int, error) {
|
||||
if id, ok := PermissionIDs[p]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// PermissionFromString converts a string representation to its Permission enum
|
||||
func PermissionFromString(s string) (Permission, error) {
|
||||
if p, ok := StringToPermission[s]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission string")
|
||||
}
|
||||
|
||||
// PermissionFromID converts an ID to its Permission enum
|
||||
func PermissionFromID(id int) (Permission, error) {
|
||||
if p, ok := IdToPermission[id]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission ID")
|
||||
}
|
4
pkg/analyzer/analyzers/shopify/permissions.yaml
Normal file
4
pkg/analyzer/analyzers/shopify/permissions.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
permissions:
|
||||
- read
|
||||
- write
|
||||
- full_access
|
|
@ -1,8 +1,11 @@
|
|||
//go:generate generate_permissions permissions.yaml permissions.go shopify
|
||||
|
||||
package shopify
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -12,8 +15,100 @@ import (
|
|||
"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/analyzer/pb/analyzerpb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
||||
|
||||
type Analyzer struct {
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
var (
|
||||
// order the categories
|
||||
categoryOrder = []string{"Analytics", "Applications", "Assigned fulfillment orders", "Browsing behavior", "Custom pixels", "Customers", "Discounts", "Discovery", "Draft orders", "Files", "Fulfillment services", "Gift cards", "Inventory", "Legal policies", "Locations", "Marketing events", "Merchant-managed fulfillment orders", "Metaobject definitions", "Metaobject entries", "Online Store navigation", "Online Store pages", "Order editing", "Orders", "Packing slip management", "Payment customizations", "Payment terms", "Pixels", "Price rules", "Product feeds", "Product listings", "Products", "Publications", "Purchase options", "Reports", "Resource feedback", "Returns", "Sales channels", "Script tags", "Shipping", "Shop locales", "Shopify Markets", "Shopify Payments accounts", "Shopify Payments bank accounts", "Shopify Payments disputes", "Shopify Payments payouts", "Store content", "Store credit account transactions", "Store credit accounts", "Themes", "Third-party fulfillment orders", "Translations", "all_cart_transforms", "all_checkout_completion_target_customizations", "cart_transforms", "cash_tracking", "companies", "custom_fulfillment_services", "customer_data_erasure", "customer_merge", "delivery_customizations", "delivery_option_generators", "discounts_allocator_functions", "fulfillment_constraint_rules", "gates", "order_submission_rules", "privacy_settings", "shopify_payments_provider_accounts_sensitive", "validations"}
|
||||
)
|
||||
|
||||
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Shopify }
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
storeUrl, ok := credInfo["store_url"]
|
||||
if !ok {
|
||||
return nil, errors.New("store_url not found in credentialInfo")
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(a.Cfg, key, storeUrl)
|
||||
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: analyzerpb.AnalyzerType_Shopify,
|
||||
Metadata: map[string]any{
|
||||
"status_code": info.StatusCode,
|
||||
},
|
||||
}
|
||||
|
||||
resource := &analyzers.Resource{
|
||||
Name: info.ShopInfo.Shop.Name,
|
||||
FullyQualifiedName: info.ShopInfo.Shop.Domain + "/" + info.ShopInfo.Shop.Email,
|
||||
Type: "shop",
|
||||
Metadata: map[string]any{
|
||||
"created_at": info.ShopInfo.Shop.CreatedAt,
|
||||
},
|
||||
Parent: nil,
|
||||
}
|
||||
result.Bindings = make([]analyzers.Binding, 0)
|
||||
|
||||
for _, category := range categoryOrder {
|
||||
if val, ok := info.Scopes[category]; ok {
|
||||
cateogryResource := &analyzers.Resource{
|
||||
Name: category,
|
||||
FullyQualifiedName: resource.FullyQualifiedName + "/" + category, // shop.domain/shop.email/category
|
||||
Type: "category",
|
||||
Parent: resource,
|
||||
}
|
||||
|
||||
if sliceContains(val.Scopes, "Read") && sliceContains(val.Scopes, "Write") {
|
||||
result.Bindings = append(result.Bindings, analyzers.Binding{
|
||||
Resource: *cateogryResource,
|
||||
Permission: analyzers.Permission{
|
||||
Value: PermissionStrings[FullAccess],
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
for _, scope := range val.Scopes {
|
||||
lowerScope := strings.ToLower(scope)
|
||||
if _, ok := StringToPermission[lowerScope]; !ok { // skip unknown scopes/permission
|
||||
continue
|
||||
}
|
||||
result.Bindings = append(result.Bindings, analyzers.Binding{
|
||||
Resource: *cateogryResource,
|
||||
Permission: analyzers.Permission{
|
||||
Value: lowerScope,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
//go:embed scopes.json
|
||||
var scopesConfig []byte
|
||||
|
||||
|
@ -90,6 +185,7 @@ func determineScopes(data ScopeDataJSON, input string) map[string]OutputScopes {
|
|||
|
||||
type ShopInfoJSON struct {
|
||||
Shop struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
|
@ -224,9 +320,6 @@ func printAccessScopes(accessScopes map[string]OutputScopes) {
|
|||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"Scope", "Description", "Access"})
|
||||
|
||||
// order the categories
|
||||
categoryOrder := []string{"Analytics", "Applications", "Assigned fulfillment orders", "Browsing behavior", "Custom pixels", "Customers", "Discounts", "Discovery", "Draft orders", "Files", "Fulfillment services", "Gift cards", "Inventory", "Legal policies", "Locations", "Marketing events", "Merchant-managed fulfillment orders", "Metaobject definitions", "Metaobject entries", "Online Store navigation", "Online Store pages", "Order editing", "Orders", "Packing slip management", "Payment customizations", "Payment terms", "Pixels", "Price rules", "Product feeds", "Product listings", "Products", "Publications", "Purchase options", "Reports", "Resource feedback", "Returns", "Sales channels", "Script tags", "Shipping", "Shop locales", "Shopify Markets", "Shopify Payments accounts", "Shopify Payments bank accounts", "Shopify Payments disputes", "Shopify Payments payouts", "Store content", "Store credit account transactions", "Store credit accounts", "Themes", "Third-party fulfillment orders", "Translations", "all_cart_transforms", "all_checkout_completion_target_customizations", "cart_transforms", "cash_tracking", "companies", "custom_fulfillment_services", "customer_data_erasure", "customer_merge", "delivery_customizations", "delivery_option_generators", "discounts_allocator_functions", "fulfillment_constraint_rules", "gates", "order_submission_rules", "privacy_settings", "shopify_payments_provider_accounts_sensitive", "validations"}
|
||||
|
||||
for _, category := range categoryOrder {
|
||||
if val, ok := accessScopes[category]; ok {
|
||||
t.AppendRow([]interface{}{color.GreenString(category), color.GreenString(val.Description), color.GreenString(val.PrintScopes())})
|
||||
|
|
88
pkg/analyzer/analyzers/shopify/shopify_test.go
Normal file
88
pkg/analyzer/analyzers/shopify/shopify_test.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package shopify
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
//go:embed expected_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
secret := testSecrets.MustGetField("SHOPIFY_ADMIN_SECRET")
|
||||
domain := testSecrets.MustGetField("SHOPIFY_DOMAIN")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
storeUrl string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid Shopify key",
|
||||
key: secret,
|
||||
storeUrl: domain,
|
||||
want: string(expectedOutput),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := Analyzer{Cfg: &config.Config{}}
|
||||
got, err := a.Analyze(ctx, map[string]string{"key": tt.key, "store_url": tt.storeUrl})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal the actual result to JSON
|
||||
gotJSON, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to JSON: %s", err)
|
||||
}
|
||||
|
||||
// Parse the expected JSON string
|
||||
var wantObj analyzers.AnalyzerResult
|
||||
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
|
||||
t.Fatalf("could not unmarshal want JSON string: %s", err)
|
||||
}
|
||||
|
||||
// Marshal the expected result to JSON (to normalize)
|
||||
wantJSON, err := json.Marshal(wantObj)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal want to JSON: %s", err)
|
||||
}
|
||||
|
||||
// Compare the JSON strings
|
||||
if string(gotJSON) != string(wantJSON) {
|
||||
// Pretty-print both JSON strings for easier comparison
|
||||
var gotIndented, wantIndented []byte
|
||||
gotIndented, err = json.MarshalIndent(got, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to indented JSON: %s", err)
|
||||
}
|
||||
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal want to indented JSON: %s", err)
|
||||
}
|
||||
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -74,6 +74,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||
s1.ExtraData = map[string]string{
|
||||
"access_scopes": strings.Join(handleArray, ","),
|
||||
}
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": key,
|
||||
"store_url": domainRes,
|
||||
}
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@ func TestShopify_FromChunk(t *testing.T) {
|
|||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("Shopify.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
|
|
Loading…
Reference in a new issue