From a43d451c4d9f52ea47819029ac23e6cdf6c19849 Mon Sep 17 00:00:00 2001 From: Abdul Basit Date: Sat, 7 Sep 2024 00:40:47 +0500 Subject: [PATCH] [analyze] Add Analyzer for SourceGraph (#3173) * implement analyzer interface for sourcegraph * created permission for sourcegraph test for sourcegraph. added email in resource metadata. * handling of missing keys in map * linked sourcegraph detector to analyzer * update the fullyqualidied name of resource to make it unique. updated the test. * add current user email in metadata --------- Co-authored-by: Abdul Basit --- .../analyzers/sourcegraph/permissions.go | 66 +++++++++++ .../analyzers/sourcegraph/permissions.yaml | 3 + .../analyzers/sourcegraph/sourcegraph.go | 57 ++++++++++ .../analyzers/sourcegraph/sourcegraph_test.go | 103 ++++++++++++++++++ pkg/detectors/sourcegraph/sourcegraph.go | 4 +- pkg/detectors/sourcegraph/sourcegraph_test.go | 1 + 6 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 pkg/analyzer/analyzers/sourcegraph/permissions.go create mode 100644 pkg/analyzer/analyzers/sourcegraph/permissions.yaml create mode 100644 pkg/analyzer/analyzers/sourcegraph/sourcegraph_test.go diff --git a/pkg/analyzer/analyzers/sourcegraph/permissions.go b/pkg/analyzer/analyzers/sourcegraph/permissions.go new file mode 100644 index 000000000..fe395efb2 --- /dev/null +++ b/pkg/analyzer/analyzers/sourcegraph/permissions.go @@ -0,0 +1,66 @@ +// Code generated by go generate; DO NOT EDIT. +package sourcegraph + +import "errors" + +type Permission int + +const ( + NoAccess Permission = iota + UserRead Permission = iota + SiteAdminFull Permission = iota +) + +var ( + PermissionStrings = map[Permission]string{ + UserRead: "user:read", + SiteAdminFull: "site_admin:full", + } + + StringToPermission = map[string]Permission{ + "user:read": UserRead, + "site_admin:full": SiteAdminFull, + } + + PermissionIDs = map[Permission]int{ + UserRead: 0, + SiteAdminFull: 1, + } + + IdToPermission = map[int]Permission{ + 0: UserRead, + 1: SiteAdminFull, + } +) + +// 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") +} diff --git a/pkg/analyzer/analyzers/sourcegraph/permissions.yaml b/pkg/analyzer/analyzers/sourcegraph/permissions.yaml new file mode 100644 index 000000000..67cf1d738 --- /dev/null +++ b/pkg/analyzer/analyzers/sourcegraph/permissions.yaml @@ -0,0 +1,3 @@ +permissions: +- user:read +- site_admin:full diff --git a/pkg/analyzer/analyzers/sourcegraph/sourcegraph.go b/pkg/analyzer/analyzers/sourcegraph/sourcegraph.go index fefbd9916..71f3d05af 100644 --- a/pkg/analyzer/analyzers/sourcegraph/sourcegraph.go +++ b/pkg/analyzer/analyzers/sourcegraph/sourcegraph.go @@ -1,3 +1,4 @@ +//go:generate generate_permissions permissions.yaml permissions.go sourcegraph package sourcegraph // ToDo: Add suport for custom domain @@ -11,8 +12,64 @@ import ( "github.com/fatih/color" "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 +} + +func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Sourcegraph } + +func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) { + key, ok := credInfo["key"] + if !ok { + return nil, fmt.Errorf("missing key in credInfo") + } + 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 + } + + permission := PermissionStrings[UserRead] + if info.IsSiteAdmin { + permission = PermissionStrings[SiteAdminFull] + } + result := analyzers.AnalyzerResult{ + AnalyzerType: analyzerpb.AnalyzerType_Sourcegraph, + Metadata: nil, + Bindings: []analyzers.Binding{ + { + Resource: analyzers.Resource{ + Name: info.User.Data.CurrentUser.Username, + FullyQualifiedName: "sourcegraph/" + info.User.Data.CurrentUser.Email, + Type: "user", + Metadata: map[string]any{ + "created_at": info.User.Data.CurrentUser.CreatedAt, + "email": info.User.Data.CurrentUser.Email, + }, + Parent: nil, + }, + Permission: analyzers.Permission{ + Value: permission, + }, + }, + }, + } + + return &result +} + type GraphQLError struct { Message string `json:"message"` Path []string `json:"path"` diff --git a/pkg/analyzer/analyzers/sourcegraph/sourcegraph_test.go b/pkg/analyzer/analyzers/sourcegraph/sourcegraph_test.go new file mode 100644 index 000000000..31e948d24 --- /dev/null +++ b/pkg/analyzer/analyzers/sourcegraph/sourcegraph_test.go @@ -0,0 +1,103 @@ +package sourcegraph + +import ( + "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" +) + +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("SOURCEGRAPH") + + tests := []struct { + name string + key string + want string // JSON string + wantErr bool + }{ + { + name: "valid SourceGraph key", + key: secret, + want: `{ + "AnalyzerType": 17, + "Bindings": [ + { + "Resource": { + "Name": "ahrav", + "FullyQualifiedName": "sourcegraph/ahravdutta02@gmail.com", + "Type": "user", + "Metadata": { + "created_at": "2023-07-23T04:16:31Z", + "email": "ahravdutta02@gmail.com" + }, + "Parent": null + }, + "Permission": { + "Value": "user:read", + "Parent": null + } + } + ], + "UnboundedResources": null, + "Metadata": null + }`, + 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}) + 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) + } + }) + } +} diff --git a/pkg/detectors/sourcegraph/sourcegraph.go b/pkg/detectors/sourcegraph/sourcegraph.go index 08c542f6c..81cbd2213 100644 --- a/pkg/detectors/sourcegraph/sourcegraph.go +++ b/pkg/detectors/sourcegraph/sourcegraph.go @@ -3,10 +3,11 @@ package sourcegraph import ( "context" "fmt" - regexp "github.com/wasilibs/go-re2" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -80,6 +81,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } else { s1.SetVerificationError(err, resMatch) } + s1.AnalysisInfo = map[string]string{"key": resMatch} } results = append(results, s1) diff --git a/pkg/detectors/sourcegraph/sourcegraph_test.go b/pkg/detectors/sourcegraph/sourcegraph_test.go index 29d4be8b8..c15c09527 100644 --- a/pkg/detectors/sourcegraph/sourcegraph_test.go +++ b/pkg/detectors/sourcegraph/sourcegraph_test.go @@ -227,6 +227,7 @@ func TestSourcegraph_FromChunk(t *testing.T) { if (got[i].VerificationError() != nil) != tt.wantVerificationErr { t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError()) } + got[i].AnalysisInfo = nil } ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "VerificationError", "ExtraData") if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {