[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:
Abdul Basit 2024-09-12 23:53:42 +05:00 committed by GitHub
parent 57e58123f1
commit dc9c9a30b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 441 additions and 3 deletions

View 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
}
}

View 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")
}

View file

@ -0,0 +1,4 @@
permissions:
- read
- write
- full_access

View file

@ -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())})

View 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)
}
})
}
}

View file

@ -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()
}

View file

@ -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)