diff --git a/pkg/analyzer/analyzers/analyzers.go b/pkg/analyzer/analyzers/analyzers.go index 21988b9f7..909acd8c8 100644 --- a/pkg/analyzer/analyzers/analyzers.go +++ b/pkg/analyzer/analyzers/analyzers.go @@ -99,6 +99,7 @@ const ( AnalyzerTypeMonday AnalyzerTypeNgrok AnalyzerTypeMux + AnalyzerTypePosthog // Add new items here with AnalyzerType prefix ) @@ -143,6 +144,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{ AnalyzerTypeMonday: "Monday", AnalyzerTypeNgrok: "Ngrok", AnalyzerTypeMux: "Mux", + AnalyzerTypePosthog: "Posthog", // Add new mappings here } diff --git a/pkg/analyzer/analyzers/posthog/expected_output.json b/pkg/analyzer/analyzers/posthog/expected_output.json new file mode 100644 index 000000000..56fc217c4 --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/expected_output.json @@ -0,0 +1 @@ +{"AnalyzerType":39,"Bindings":[{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"action:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"activity_log:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"annotation:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"dashboard:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"event_definition:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"event_definition:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"export:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"group:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"group:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"insight:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"person:read","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"person:write","Parent":null}},{"Resource":{"Name":"Default project","FullyQualifiedName":"150774","Type":"project","Metadata":null,"Parent":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null}},"Permission":{"Value":"query:read","Parent":null}},{"Resource":{"Name":"Truffle Security","FullyQualifiedName":"019666bb-9f8e-0000-8bc2-4ea34ec57752","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"user:read","Parent":null}},{"Resource":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null},"Permission":{"Value":"organization:read","Parent":null}},{"Resource":{"Name":"TruffleSecurity","FullyQualifiedName":"019666bb-9f89-0000-0820-312e5f974324","Type":"organization","Metadata":null,"Parent":null},"Permission":{"Value":"project:read","Parent":null}}],"UnboundedResources":null,"Metadata":null} \ No newline at end of file diff --git a/pkg/analyzer/analyzers/posthog/permissions.go b/pkg/analyzer/analyzers/posthog/permissions.go new file mode 100644 index 000000000..9354fbdce --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/permissions.go @@ -0,0 +1,356 @@ +// Code generated by go generate; DO NOT EDIT. +package posthog + +import "errors" + +type Permission int + +const ( + Invalid Permission = iota + ActionRead Permission = iota + ActionWrite Permission = iota + ActivityLogRead Permission = iota + ActivityLogWrite Permission = iota + AnnotationRead Permission = iota + AnnotationWrite Permission = iota + BatchExportRead Permission = iota + BatchExportWrite Permission = iota + CohortRead Permission = iota + CohortWrite Permission = iota + DashboardRead Permission = iota + DashboardWrite Permission = iota + DashboardTemplateRead Permission = iota + DashboardTemplateWrite Permission = iota + EarlyAccessFeatureRead Permission = iota + EarlyAccessFeatureWrite Permission = iota + EventDefinitionRead Permission = iota + EventDefinitionWrite Permission = iota + ErrorTrackingRead Permission = iota + ErrorTrackingWrite Permission = iota + ExperimentRead Permission = iota + ExperimentWrite Permission = iota + ExportRead Permission = iota + ExportWrite Permission = iota + FeatureFlagRead Permission = iota + FeatureFlagWrite Permission = iota + GroupRead Permission = iota + GroupWrite Permission = iota + HogFunctionRead Permission = iota + HogFunctionWrite Permission = iota + InsightRead Permission = iota + InsightWrite Permission = iota + NotebookRead Permission = iota + NotebookWrite Permission = iota + OrganizationRead Permission = iota + OrganizationWrite Permission = iota + OrganizationMemberRead Permission = iota + OrganizationMemberWrite Permission = iota + PersonRead Permission = iota + PersonWrite Permission = iota + PluginRead Permission = iota + PluginWrite Permission = iota + ProjectRead Permission = iota + ProjectWrite Permission = iota + PropertyDefinitionRead Permission = iota + PropertyDefinitionWrite Permission = iota + QueryRead Permission = iota + SessionRecordingRead Permission = iota + SessionRecordingWrite Permission = iota + SessionRecordingPlaylistRead Permission = iota + SessionRecordingPlaylistWrite Permission = iota + SharingConfigurationRead Permission = iota + SharingConfigurationWrite Permission = iota + SubscriptionRead Permission = iota + SubscriptionWrite Permission = iota + SurveyRead Permission = iota + SurveyWrite Permission = iota + UserRead Permission = iota + WebhookRead Permission = iota + WebhookWrite Permission = iota +) + +var ( + PermissionStrings = map[Permission]string{ + ActionRead: "action:read", + ActionWrite: "action:write", + ActivityLogRead: "activity_log:read", + ActivityLogWrite: "activity_log:write", + AnnotationRead: "annotation:read", + AnnotationWrite: "annotation:write", + BatchExportRead: "batch_export:read", + BatchExportWrite: "batch_export:write", + CohortRead: "cohort:read", + CohortWrite: "cohort:write", + DashboardRead: "dashboard:read", + DashboardWrite: "dashboard:write", + DashboardTemplateRead: "dashboard_template:read", + DashboardTemplateWrite: "dashboard_template:write", + EarlyAccessFeatureRead: "early_access_feature:read", + EarlyAccessFeatureWrite: "early_access_feature:write", + EventDefinitionRead: "event_definition:read", + EventDefinitionWrite: "event_definition:write", + ErrorTrackingRead: "error_tracking:read", + ErrorTrackingWrite: "error_tracking:write", + ExperimentRead: "experiment:read", + ExperimentWrite: "experiment:write", + ExportRead: "export:read", + ExportWrite: "export:write", + FeatureFlagRead: "feature_flag:read", + FeatureFlagWrite: "feature_flag:write", + GroupRead: "group:read", + GroupWrite: "group:write", + HogFunctionRead: "hog_function:read", + HogFunctionWrite: "hog_function:write", + InsightRead: "insight:read", + InsightWrite: "insight:write", + NotebookRead: "notebook:read", + NotebookWrite: "notebook:write", + OrganizationRead: "organization:read", + OrganizationWrite: "organization:write", + OrganizationMemberRead: "organization_member:read", + OrganizationMemberWrite: "organization_member:write", + PersonRead: "person:read", + PersonWrite: "person:write", + PluginRead: "plugin:read", + PluginWrite: "plugin:write", + ProjectRead: "project:read", + ProjectWrite: "project:write", + PropertyDefinitionRead: "property_definition:read", + PropertyDefinitionWrite: "property_definition:write", + QueryRead: "query:read", + SessionRecordingRead: "session_recording:read", + SessionRecordingWrite: "session_recording:write", + SessionRecordingPlaylistRead: "session_recording_playlist:read", + SessionRecordingPlaylistWrite: "session_recording_playlist:write", + SharingConfigurationRead: "sharing_configuration:read", + SharingConfigurationWrite: "sharing_configuration:write", + SubscriptionRead: "subscription:read", + SubscriptionWrite: "subscription:write", + SurveyRead: "survey:read", + SurveyWrite: "survey:write", + UserRead: "user:read", + WebhookRead: "webhook:read", + WebhookWrite: "webhook:write", + } + + StringToPermission = map[string]Permission{ + "action:read": ActionRead, + "action:write": ActionWrite, + "activity_log:read": ActivityLogRead, + "activity_log:write": ActivityLogWrite, + "annotation:read": AnnotationRead, + "annotation:write": AnnotationWrite, + "batch_export:read": BatchExportRead, + "batch_export:write": BatchExportWrite, + "cohort:read": CohortRead, + "cohort:write": CohortWrite, + "dashboard:read": DashboardRead, + "dashboard:write": DashboardWrite, + "dashboard_template:read": DashboardTemplateRead, + "dashboard_template:write": DashboardTemplateWrite, + "early_access_feature:read": EarlyAccessFeatureRead, + "early_access_feature:write": EarlyAccessFeatureWrite, + "event_definition:read": EventDefinitionRead, + "event_definition:write": EventDefinitionWrite, + "error_tracking:read": ErrorTrackingRead, + "error_tracking:write": ErrorTrackingWrite, + "experiment:read": ExperimentRead, + "experiment:write": ExperimentWrite, + "export:read": ExportRead, + "export:write": ExportWrite, + "feature_flag:read": FeatureFlagRead, + "feature_flag:write": FeatureFlagWrite, + "group:read": GroupRead, + "group:write": GroupWrite, + "hog_function:read": HogFunctionRead, + "hog_function:write": HogFunctionWrite, + "insight:read": InsightRead, + "insight:write": InsightWrite, + "notebook:read": NotebookRead, + "notebook:write": NotebookWrite, + "organization:read": OrganizationRead, + "organization:write": OrganizationWrite, + "organization_member:read": OrganizationMemberRead, + "organization_member:write": OrganizationMemberWrite, + "person:read": PersonRead, + "person:write": PersonWrite, + "plugin:read": PluginRead, + "plugin:write": PluginWrite, + "project:read": ProjectRead, + "project:write": ProjectWrite, + "property_definition:read": PropertyDefinitionRead, + "property_definition:write": PropertyDefinitionWrite, + "query:read": QueryRead, + "session_recording:read": SessionRecordingRead, + "session_recording:write": SessionRecordingWrite, + "session_recording_playlist:read": SessionRecordingPlaylistRead, + "session_recording_playlist:write": SessionRecordingPlaylistWrite, + "sharing_configuration:read": SharingConfigurationRead, + "sharing_configuration:write": SharingConfigurationWrite, + "subscription:read": SubscriptionRead, + "subscription:write": SubscriptionWrite, + "survey:read": SurveyRead, + "survey:write": SurveyWrite, + "user:read": UserRead, + "webhook:read": WebhookRead, + "webhook:write": WebhookWrite, + } + + PermissionIDs = map[Permission]int{ + ActionRead: 1, + ActionWrite: 2, + ActivityLogRead: 3, + ActivityLogWrite: 4, + AnnotationRead: 5, + AnnotationWrite: 6, + BatchExportRead: 7, + BatchExportWrite: 8, + CohortRead: 9, + CohortWrite: 10, + DashboardRead: 11, + DashboardWrite: 12, + DashboardTemplateRead: 13, + DashboardTemplateWrite: 14, + EarlyAccessFeatureRead: 15, + EarlyAccessFeatureWrite: 16, + EventDefinitionRead: 17, + EventDefinitionWrite: 18, + ErrorTrackingRead: 19, + ErrorTrackingWrite: 20, + ExperimentRead: 21, + ExperimentWrite: 22, + ExportRead: 23, + ExportWrite: 24, + FeatureFlagRead: 25, + FeatureFlagWrite: 26, + GroupRead: 27, + GroupWrite: 28, + HogFunctionRead: 29, + HogFunctionWrite: 30, + InsightRead: 31, + InsightWrite: 32, + NotebookRead: 33, + NotebookWrite: 34, + OrganizationRead: 35, + OrganizationWrite: 36, + OrganizationMemberRead: 37, + OrganizationMemberWrite: 38, + PersonRead: 39, + PersonWrite: 40, + PluginRead: 41, + PluginWrite: 42, + ProjectRead: 43, + ProjectWrite: 44, + PropertyDefinitionRead: 45, + PropertyDefinitionWrite: 46, + QueryRead: 47, + SessionRecordingRead: 48, + SessionRecordingWrite: 49, + SessionRecordingPlaylistRead: 50, + SessionRecordingPlaylistWrite: 51, + SharingConfigurationRead: 52, + SharingConfigurationWrite: 53, + SubscriptionRead: 54, + SubscriptionWrite: 55, + SurveyRead: 56, + SurveyWrite: 57, + UserRead: 58, + WebhookRead: 59, + WebhookWrite: 60, + } + + IdToPermission = map[int]Permission{ + 1: ActionRead, + 2: ActionWrite, + 3: ActivityLogRead, + 4: ActivityLogWrite, + 5: AnnotationRead, + 6: AnnotationWrite, + 7: BatchExportRead, + 8: BatchExportWrite, + 9: CohortRead, + 10: CohortWrite, + 11: DashboardRead, + 12: DashboardWrite, + 13: DashboardTemplateRead, + 14: DashboardTemplateWrite, + 15: EarlyAccessFeatureRead, + 16: EarlyAccessFeatureWrite, + 17: EventDefinitionRead, + 18: EventDefinitionWrite, + 19: ErrorTrackingRead, + 20: ErrorTrackingWrite, + 21: ExperimentRead, + 22: ExperimentWrite, + 23: ExportRead, + 24: ExportWrite, + 25: FeatureFlagRead, + 26: FeatureFlagWrite, + 27: GroupRead, + 28: GroupWrite, + 29: HogFunctionRead, + 30: HogFunctionWrite, + 31: InsightRead, + 32: InsightWrite, + 33: NotebookRead, + 34: NotebookWrite, + 35: OrganizationRead, + 36: OrganizationWrite, + 37: OrganizationMemberRead, + 38: OrganizationMemberWrite, + 39: PersonRead, + 40: PersonWrite, + 41: PluginRead, + 42: PluginWrite, + 43: ProjectRead, + 44: ProjectWrite, + 45: PropertyDefinitionRead, + 46: PropertyDefinitionWrite, + 47: QueryRead, + 48: SessionRecordingRead, + 49: SessionRecordingWrite, + 50: SessionRecordingPlaylistRead, + 51: SessionRecordingPlaylistWrite, + 52: SharingConfigurationRead, + 53: SharingConfigurationWrite, + 54: SubscriptionRead, + 55: SubscriptionWrite, + 56: SurveyRead, + 57: SurveyWrite, + 58: UserRead, + 59: WebhookRead, + 60: WebhookWrite, + } +) + +// 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/posthog/permissions.yaml b/pkg/analyzer/analyzers/posthog/permissions.yaml new file mode 100644 index 000000000..cdea227b5 --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/permissions.yaml @@ -0,0 +1,61 @@ +permissions: + - action:read + - action:write + - activity_log:read + - activity_log:write + - annotation:read + - annotation:write + - batch_export:read + - batch_export:write + - cohort:read + - cohort:write + - dashboard:read + - dashboard:write + - dashboard_template:read + - dashboard_template:write + - early_access_feature:read + - early_access_feature:write + - event_definition:read + - event_definition:write + - error_tracking:read + - error_tracking:write + - experiment:read + - experiment:write + - export:read + - export:write + - feature_flag:read + - feature_flag:write + - group:read + - group:write + - hog_function:read + - hog_function:write + - insight:read + - insight:write + - notebook:read + - notebook:write + - organization:read + - organization:write + - organization_member:read + - organization_member:write + - person:read + - person:write + - plugin:read + - plugin:write + - project:read + - project:write + - property_definition:read + - property_definition:write + - query:read + - session_recording:read + - session_recording:write + - session_recording_playlist:read + - session_recording_playlist:write + - sharing_configuration:read + - sharing_configuration:write + - subscription:read + - subscription:write + - survey:read + - survey:write + - user:read + - webhook:read + - webhook:write diff --git a/pkg/analyzer/analyzers/posthog/posthog.go b/pkg/analyzer/analyzers/posthog/posthog.go new file mode 100644 index 000000000..65c534dde --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/posthog.go @@ -0,0 +1,545 @@ +//go:generate generate_permissions permissions.yaml permissions.go posthog + +package posthog + +import ( + "bytes" + _ "embed" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" + "github.com/trufflesecurity/trufflehog/v3/pkg/context" +) + +var _ analyzers.Analyzer = (*Analyzer)(nil) + +const ( + USDomain = "https://us.posthog.com" + EUDomain = "https://eu.posthog.com" +) + +type Analyzer struct { + Cfg *config.Config +} + +func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePosthog } + +func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) { + key, ok := credInfo["key"] + if !ok { + return nil, errors.New("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 + } + result := analyzers.AnalyzerResult{ + AnalyzerType: analyzers.AnalyzerTypePosthog, + Metadata: nil, + Bindings: make([]analyzers.Binding, 0), + } + + if info.orgPermissions == nil { + // no permissions to check + return &result + } + + if info.user != nil { + // for user resource + userResource := analyzers.Resource{ + Name: info.user.FirstName + " " + info.user.LastName, + FullyQualifiedName: info.user.UUID, + Type: "user", + } + analyzerPermission := analyzers.Permission{ + Value: PermissionStrings[UserRead], + } + result.Bindings = append(result.Bindings, analyzers.Binding{ + Resource: userResource, + Permission: analyzerPermission, + }) + } + + // for organization permissions, we need to bind the permissions to the organization resource + organizationResource := analyzers.Resource{ + Name: info.organization.Name, + FullyQualifiedName: info.organization.ID, + Type: "organization", + } + for _, permission := range info.orgPermissions { + if value, ok := PermissionStrings[permission]; ok { + analyzerPermission := analyzers.Permission{ + Value: value, + } + result.Bindings = append(result.Bindings, analyzers.Binding{ + Resource: organizationResource, + Permission: analyzerPermission, + }) + } + } + + // for project permissions, we need to bind the permissions to the project resource and organization as the parent resource + for _, projectPermission := range info.projectPermissions { + projectResource := analyzers.Resource{ + Name: projectPermission.Project.Name, + FullyQualifiedName: strconv.FormatInt(projectPermission.Project.ID, 10), + Type: "project", + Parent: &organizationResource, + } + for _, permission := range projectPermission.Permissions { + permissionStr, _ := permission.ToString() + analyzerPermission := analyzers.Permission{ + Value: permissionStr, + } + result.Bindings = append(result.Bindings, analyzers.Binding{ + Resource: projectResource, + Permission: analyzerPermission, + }) + } + } + + return &result +} + +//go:embed scopes.json +var scopesConfigBytes []byte + +type HttpStatusTest struct { + Endpoint string `json:"endpoint"` + Method string `json:"method"` + Payload interface{} `json:"payload"` + ValidStatuses []int `json:"valid_status_code"` + InvalidStatuses []int `json:"invalid_status_code"` +} + +func StatusContains(status int, vals []int) bool { + for _, v := range vals { + if status == v { + return true + } + } + return false +} + +func (h *HttpStatusTest) RunTest(cfg *config.Config, client *http.Client, domain string, headers map[string]string, args ...any) (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) + } + + req, err := http.NewRequest(h.Method, fmt.Sprintf(domain+h.Endpoint, args...), 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.Method, h.Endpoint) + return false, errors.New("error checking response status code") + } +} + +type ScopesConfig struct { + GeneralScopes []Scope `json:"general_scopes"` + OrganizationScopes []Scope `json:"organization_scopes"` + ProjectScopes []Scope `json:"project_scopes"` +} + +type Scope struct { + Name string `json:"name"` + Test ScopeTest `json:"test"` +} + +type ScopeTest struct { + Read *HttpStatusTest `json:"read"` + Write *HttpStatusTest `json:"write"` +} + +func readInScopesConfig() (*ScopesConfig, error) { + var scopesConfig ScopesConfig + if err := json.Unmarshal(scopesConfigBytes, &scopesConfig); err != nil { + return nil, err + } + + return &scopesConfig, nil +} + +func checkPermissions(cfg *config.Config, client *http.Client, domain string, key string, scopes []Scope, args ...any) ([]Permission, error) { + + permissions := make([]Permission, 0) + headers := map[string]string{"Authorization": "Bearer " + key} + for _, scope := range scopes { + var status bool + var err error + if scope.Test.Write != nil { + status, err = scope.Test.Write.RunTest(cfg, client, domain, headers, args...) + if err != nil { + return nil, fmt.Errorf("running test: %w", err) + } + } + if status { + if permission, ok := StringToPermission[scope.Name+":write"]; ok { + permissions = append(permissions, permission) + } + // if write exists, read also exists + if permission, ok := StringToPermission[scope.Name+":read"]; ok { + permissions = append(permissions, permission) + } + } else { + status, err = scope.Test.Read.RunTest(cfg, client, domain, headers, args...) + if err != nil { + return nil, fmt.Errorf("running test: %w", err) + } + if status { + if permission, ok := StringToPermission[scope.Name+":read"]; ok { + permissions = append(permissions, permission) + } + } + } + + } + + return permissions, nil +} + +type ProjectPermissions struct { + Project *Project + Permissions []Permission +} + +type SecretInfo struct { + user *User + organization *Organization + orgPermissions []Permission + projectPermissions []ProjectPermissions + // generalPermissions []Permission + unverifiedPermissions map[Permission]struct{} +} + +func AnalyzeAndPrintPermissions(cfg *config.Config, key string) { + info, err := AnalyzePermissions(cfg, key) + if err != nil { + color.Red("[x] Error : %s", err.Error()) + return + } + + color.Green("[!] Valid Posthog API key") + color.Yellow("[i] Expires: Never") + if info.user != nil { + printUser(*info.user) + } + + if info.organization == nil { + color.Yellow("\n[i] No permissions were verified for this key because the key does not have one of the necessary permissions (user:read or organization:read) required to verifiy other permissions.") + } + + if info.orgPermissions != nil { + printOrganizationPermissions(*info.organization, info.orgPermissions) + } + if len(info.projectPermissions) > 0 { + printProjectPermissions(info.projectPermissions) + } + printUnverifiedPermissions(info.unverifiedPermissions) + +} + +func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) { + var info = &SecretInfo{} + + // These are permissions that cannot be verified due to no endpoint available + info.unverifiedPermissions = map[Permission]struct{}{ + ErrorTrackingRead: {}, + ErrorTrackingWrite: {}, + SharingConfigurationRead: {}, + SharingConfigurationWrite: {}, + WebhookRead: {}, + WebhookWrite: {}, + } + + client := analyzers.NewAnalyzeClient(cfg) + + // we need to determine if the key is for US or EU domain + domain, user, err := resolveDomainAndUser(cfg, client, key) + if err != nil { + return nil, fmt.Errorf("Invalid API Key: %w", err) + } + + info.user = user + + // Most posthog API scopes are bound to projects and organization, so to determine the scopes we need to first get the organization and projects. + // If the key has user:read scope, we will get the user above which contains the organizations and projects. + // If the key does not have user:read scope, we can call the /organizations/@current endpoint to get the + // organization and projects. If the key does not have organization:read scope as well, we cannot determine any scope. + var org *Organization + if user == nil { + org, err = getOrganization(cfg, client, domain, key) + if err != nil { + return nil, err + } + if org == nil { + // can't determine any scopes + for permission := range PermissionStrings { + info.unverifiedPermissions[permission] = struct{}{} + } + return info, nil + } + } else { + org = &user.Organization + } + + // set the organization in the info struct + info.organization = org + + // read in scopes + scopesConfig, err := readInScopesConfig() + if err != nil { + return nil, err + } + + // check organization permissions + organizationPermissions, err := checkOrganizationPermissions(cfg, client, domain, key, scopesConfig, org) + if err != nil { + return nil, err + } + + // check general permissions + generalOrganizationPermissions, err := checkGeneralPermissions(cfg, client, domain, key, scopesConfig) + if err != nil { + return nil, err + } + + // merge general permissions with organization permissions + info.orgPermissions = organizationPermissions + info.orgPermissions = append(info.orgPermissions, generalOrganizationPermissions...) + + // check project permissions + projectPermissions, err := checkProjectPermissions(cfg, client, domain, key, scopesConfig, org) + if err != nil { + return nil, err + } + info.projectPermissions = projectPermissions + + return info, nil +} + +func checkGeneralPermissions(cfg *config.Config, client *http.Client, domain, key string, scopesConfig *ScopesConfig) ([]Permission, error) { + return checkPermissions(cfg, client, domain, key, scopesConfig.GeneralScopes) +} + +func checkOrganizationPermissions( + cfg *config.Config, + client *http.Client, + domain, + key string, + scopesConfig *ScopesConfig, + org *Organization, +) ([]Permission, error) { + return checkPermissions(cfg, client, domain, key, scopesConfig.OrganizationScopes, org.ID) +} + +func checkProjectPermissions( + cfg *config.Config, + client *http.Client, + domain, + key string, + scopesConfig *ScopesConfig, + org *Organization, +) ([]ProjectPermissions, error) { + projectPermissions := make([]ProjectPermissions, 0) + for _, project := range org.Projects { + projectPermission := ProjectPermissions{ + Project: &project, + } + permissions, err := checkPermissions(cfg, client, domain, key, scopesConfig.ProjectScopes, project.ID) + if err != nil { + return nil, err + } + projectPermission.Permissions = permissions + projectPermissions = append(projectPermissions, projectPermission) + } + return projectPermissions, nil +} + +type User struct { + UUID string `json:"uuid"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Organization Organization `json:"organization"` +} + +type Organization struct { + ID string `json:"id"` + Name string `json:"name"` + Projects []Project `json:"projects"` +} + +type Project struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +// resolves the domain and user (if permission exists) by calling the /users/@me method for both US and EU domains +// if the response is 200 OK, it means the domain is valid and user:read permission is also there +// if the response is 403 Forbidden, it means the domain is valid but user:read permission is not there +// if the response is 401 Unauthorized, it means the domain is invalid +func resolveDomainAndUser(cfg *config.Config, client *http.Client, key string) (string, *User, error) { + + domains := []string{USDomain, EUDomain} + for _, domain := range domains { + req, err := http.NewRequest(http.MethodGet, domain+"/api/users/@me/", nil) + if err != nil { + return "", nil, err + } + req.Header.Set("Authorization", "Bearer "+key) + + // Execute HTTP Request + resp, err := client.Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + // domain is valid and user permission also exists + var userInfo User + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + return "", nil, err + } + return domain, &userInfo, nil + case http.StatusForbidden: + // domain is valid but user permission does not exist + return domain, nil, nil + case http.StatusUnauthorized: + // Key might not be valid of this domain + // Try the other domain + continue + default: + // unexpected status code + return "", nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + } + return "", nil, fmt.Errorf("invalid Posthog API key") +} + +func getOrganization(cfg *config.Config, client *http.Client, domain string, key string) (*Organization, error) { + req, err := http.NewRequest(http.MethodGet, domain+"/api/organizations/@current/", nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+key) + + // Execute HTTP Request + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + var org Organization + if err := json.NewDecoder(resp.Body).Decode(&org); err != nil { + return nil, err + } + return &org, nil + case http.StatusForbidden: + return nil, nil + default: + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} + +func printUser(user User) { + color.Yellow("\n[i] User Info:") + color.Green("[i] Name: %s %s", user.FirstName, user.LastName) + color.Green("[i] Email: %s", user.Email) + color.Green("[i] ID: %s", user.UUID) +} + +func printOrganizationPermissions(organization Organization, permissions []Permission) { + color.Yellow("\n[i] Organization Permissions:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Organization", "Permission"}) + permissionsString := make([]string, len(permissions)) + for i, permission := range permissions { + permissionsString[i], _ = permission.ToString() + } + t.AppendRow(table.Row{ + color.GreenString(organization.Name), + color.GreenString(strings.Join(permissionsString, "\n")), + }) + t.Render() +} + +func printProjectPermissions(projectPermissions []ProjectPermissions) { + color.Yellow("\n[i] Project Permissions:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Project", "Permission"}) + for _, projectPermission := range projectPermissions { + permissionsString := make([]string, len(projectPermission.Permissions)) + for i, permission := range projectPermission.Permissions { + permissionsString[i], _ = permission.ToString() + + } + t.AppendRow(table.Row{ + color.GreenString(projectPermission.Project.Name), + color.GreenString(strings.Join(permissionsString, "\n")), + }) + } + t.Render() +} + +func printUnverifiedPermissions(permissions map[Permission]struct{}) { + color.Yellow("\n[i] Unverified Permissions:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Permission"}) + for permission := range permissions { + permissionStr, _ := permission.ToString() + t.AppendRow(table.Row{color.YellowString(permissionStr)}) + } + t.Render() +} diff --git a/pkg/analyzer/analyzers/posthog/posthog_test.go b/pkg/analyzer/analyzers/posthog/posthog_test.go new file mode 100644 index 000000000..b74368986 --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/posthog_test.go @@ -0,0 +1,100 @@ +package posthog + +import ( + _ "embed" + "encoding/json" + "sort" + "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*15) + defer cancel() + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") + if err != nil { + t.Fatalf("could not get test secrets from GCP: %s", err) + } + + tests := []struct { + name string + key string + want string // JSON string + wantErr bool + }{ + { + name: "valid posthog api key", + key: testSecrets.MustGetField("POSTHOG_API_KEY"), + 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}) + if (err != nil) != tt.wantErr { + t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // bindings need to be in the same order to be comparable + sortBindings(got.Bindings) + + // 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) + } + + // bindings need to be in the same order to be comparable + sortBindings(wantObj.Bindings) + + // 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) + } + }) + } +} + +// Helper function to sort bindings +func sortBindings(bindings []analyzers.Binding) { + sort.SliceStable(bindings, func(i, j int) bool { + if bindings[i].Resource.Name == bindings[j].Resource.Name { + return bindings[i].Permission.Value < bindings[j].Permission.Value + } + return bindings[i].Resource.Name < bindings[j].Resource.Name + }) +} diff --git a/pkg/analyzer/analyzers/posthog/scopes.json b/pkg/analyzer/analyzers/posthog/scopes.json new file mode 100644 index 000000000..ee20081a2 --- /dev/null +++ b/pkg/analyzer/analyzers/posthog/scopes.json @@ -0,0 +1,668 @@ +{ + "general_scopes": [ + { + "name": "organization", + "test": { + "read": { + "endpoint": "/api/organizations", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/organizations", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + } + ], + "organization_scopes": [ + { + "name": "batch_export", + "test": { + "read": { + "endpoint": "/api/organizations/%s/batch_exports", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/organizations/%s/batch_exports", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "organization_member", + "test": { + "read": { + "endpoint": "/api/organizations/%s/members", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/organizations/%s/members/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "project", + "test": { + "read": { + "endpoint": "/api/organizations/%s/projects", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/organizations/%s/projects/`nowaythiscanexist", + "method": "DELETE", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + } + ], + "project_scopes": [ + { + "name": "action", + "test": { + "read": { + "endpoint": "/api/projects/%d/actions", + "method": "GET", + "valid_status_code": [200], + "invalid_status_code": [403] + }, + "write": { + "endpoint": "/api/projects/%d/actions", + "method": "POST", + "valid_status_code": [500], + "invalid_status_code": [403] + } + } + }, + { + "name": "activity_log", + "test": { + "read": { + "endpoint": "/api/projects/%d/activity_log", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/activity_log", + "method": "POST", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "annotation", + "test": { + "read": { + "endpoint": "/api/projects/%d/annotations", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/annotations/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "cohort", + "test": { + "read": { + "endpoint": "/api/projects/%d/cohorts", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/cohorts/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "dashboard", + "test": { + "read": { + "endpoint": "/api/projects/%d/dashboards", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/dashboards/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "dashboard_template", + "test": { + "read": { + "endpoint": "/api/projects/%d/dashboard_templates", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/dashboard_templates/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "early_access_feature", + "test": { + "read": { + "endpoint": "/api/projects/%d/early_access_feature", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/early_access_feature", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "event_definition", + "test": { + "read": { + "endpoint": "/api/projects/%d/event_definitions", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/event_definitions/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "experiment", + "test": { + "read": { + "endpoint": "/api/projects/%d/experiments", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/experiments", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "export", + "test": { + "read": { + "endpoint": "/api/projects/%d/exports", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/exports", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "feature_flag", + "test": { + "read": { + "endpoint": "/api/projects/%d/feature_flags", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/feature_flags", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "group", + "test": { + "read": { + "endpoint": "/api/projects/%d/groups", + "method": "GET", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/groups/update_property", + "method": "POST", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "hog_function", + "test": { + "read": { + "endpoint": "/api/projects/%d/hog_functions", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/hog_functions/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "insight", + "test": { + "read": { + "endpoint": "/api/projects/%d/insights", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/insights/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "notebook", + "test": { + "read": { + "endpoint": "/api/projects/%d/notebooks", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/notebooks/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "person", + "test": { + "read": { + "endpoint": "/api/projects/%d/persons", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/persons/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "plugin", + "test": { + "read": { + "endpoint": "/api/projects/%d/plugin_configs", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/plugin_configs", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "property_definition", + "test": { + "read": { + "endpoint": "/api/projects/%d/property_definitions", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/property_definitions/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 500 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "query", + "test": { + "read": { + "endpoint": "/api/projects/%d/query/`nowaythiscanexist", + "method": "GET", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "session_recording", + "test": { + "read": { + "endpoint": "/api/projects/%d/session_recordings", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/session_recordings/`nowaythisexists", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "session_recording_playlist", + "test": { + "read": { + "endpoint": "/api/projects/%d/session_recording_playlists", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/session_recording_playlists/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "subscription", + "test": { + "read": { + "endpoint": "/api/projects/%d/subscriptions", + "method": "GET", + "valid_status_code": [ + 200, + 402 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/subscriptions/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [ + 402, + 404 + ], + "invalid_status_code": [ + 403 + ] + } + } + }, + { + "name": "survey", + "test": { + "read": { + "endpoint": "/api/projects/%d/surveys", + "method": "GET", + "valid_status_code": [ + 200 + ], + "invalid_status_code": [ + 403 + ] + }, + "write": { + "endpoint": "/api/projects/%d/surveys", + "method": "POST", + "valid_status_code": [ + 400 + ], + "invalid_status_code": [ + 403 + ] + } + } + } + ] + +} \ No newline at end of file diff --git a/pkg/analyzer/cli.go b/pkg/analyzer/cli.go index 00699453c..717352c75 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -34,6 +34,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/plaid" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/planetscale" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/posthog" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postman" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/privatekey" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sendgrid" @@ -136,5 +137,7 @@ func Run(keyType string, secretInfo SecretInfo) { ngrok.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) case "mux": mux.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["secret"]) + case "posthog": + posthog.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) } } diff --git a/pkg/detectors/posthog/posthog.go b/pkg/detectors/posthog/posthog.go index a26c350a1..275984a8f 100644 --- a/pkg/detectors/posthog/posthog.go +++ b/pkg/detectors/posthog/posthog.go @@ -2,10 +2,11 @@ package posthog import ( "context" - 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" @@ -58,6 +59,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result defer res.Body.Close() if res.StatusCode >= 200 && res.StatusCode < 300 { s1.Verified = true + s1.AnalysisInfo = map[string]string{ + "key": resMatch, + } } else if res.StatusCode == 401 { // Try EU Endpoint only if other one fails. res, err := client.Do(reqEU) @@ -65,6 +69,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result defer res.Body.Close() if res.StatusCode >= 200 && res.StatusCode < 300 { s1.Verified = true + s1.AnalysisInfo = map[string]string{ + "key": resMatch, + } } } }