From f2dc96e8548d5fab0f6ffb3d47b9918b49fdde0a Mon Sep 17 00:00:00 2001 From: Abdul Basit Date: Tue, 11 Feb 2025 23:35:31 +0500 Subject: [PATCH] [Feat] implementation Notion analyzer (#3869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description: Since `Notion.co` allows to enable [capabilities](https://developers.notion.com/reference/capabilities) against integration token, It is a good candidate to build an analyzer for it. ![Screenshot 2025-02-04 at 6 46 37 PM](https://github.com/user-attachments/assets/2fb711fe-466f-4b78-9b25-56852f08cd3b) This PR implements notion analyzer along with its integration test. ### Checklist: * [ ] Tests passing (`make test-community`)? * [x] Lint passing (`make lint` this requires [golangci-lint](https://golangci-lint.run/welcome/install/#local-installation))? --- pkg/analyzer/analyzers/analyzers.go | 2 + .../analyzers/notion/expected_output.json | 1 + pkg/analyzer/analyzers/notion/notion.go | 389 ++++++++++++++++++ pkg/analyzer/analyzers/notion/notion_test.go | 100 +++++ pkg/analyzer/analyzers/notion/permissions.go | 91 ++++ .../analyzers/notion/permissions.yaml | 8 + pkg/analyzer/analyzers/notion/scopes.json | 47 +++ pkg/analyzer/cli.go | 4 + pkg/detectors/notion/notion.go | 1 + 9 files changed, 643 insertions(+) create mode 100644 pkg/analyzer/analyzers/notion/expected_output.json create mode 100644 pkg/analyzer/analyzers/notion/notion.go create mode 100644 pkg/analyzer/analyzers/notion/notion_test.go create mode 100644 pkg/analyzer/analyzers/notion/permissions.go create mode 100644 pkg/analyzer/analyzers/notion/permissions.yaml create mode 100644 pkg/analyzer/analyzers/notion/scopes.json diff --git a/pkg/analyzer/analyzers/analyzers.go b/pkg/analyzer/analyzers/analyzers.go index 168f9bd83..565118a2f 100644 --- a/pkg/analyzer/analyzers/analyzers.go +++ b/pkg/analyzer/analyzers/analyzers.go @@ -82,6 +82,7 @@ const ( AnalyzerTypeStripe AnalyzerTypeTwilio AnalyzerTypePrivateKey + AnalyzerTypeNotion // Add new items here with AnalyzerType prefix ) @@ -110,6 +111,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{ AnalyzerTypeStripe: "Stripe", AnalyzerTypeTwilio: "Twilio", AnalyzerTypePrivateKey: "PrivateKey", + AnalyzerTypeNotion: "Notion", // Add new mappings here } diff --git a/pkg/analyzer/analyzers/notion/expected_output.json b/pkg/analyzer/analyzers/notion/expected_output.json new file mode 100644 index 000000000..203535ec5 --- /dev/null +++ b/pkg/analyzer/analyzers/notion/expected_output.json @@ -0,0 +1 @@ +{"AnalyzerType":22,"Bindings":[{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"insert_content","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"read_content","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"read_users_with_email","Parent":null}},{"Resource":{"Name":"hooman","FullyQualifiedName":"notion.so/bot/62faeec3-a948-4dd4-90ae-426e0b192902","Type":"bot","Metadata":{"workspace":"hoomanit"},"Parent":null},"Permission":{"Value":"update_content","Parent":null}}],"UnboundedResources":[{"Name":"hooman","FullyQualifiedName":"notion.so/person/3d0600fa-fa18-427d-8abc-58b662f0d209","Type":"person","Metadata":{"email":"rendyplayground@gmail.com"},"Parent":null}],"Metadata":null} \ No newline at end of file diff --git a/pkg/analyzer/analyzers/notion/notion.go b/pkg/analyzer/analyzers/notion/notion.go new file mode 100644 index 000000000..7533114e8 --- /dev/null +++ b/pkg/analyzer/analyzers/notion/notion.go @@ -0,0 +1,389 @@ +//go:generate generate_permissions permissions.yaml permissions.go notion + +package notion + +import ( + "bytes" + _ "embed" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/table" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" + "github.com/trufflesecurity/trufflehog/v3/pkg/context" +) + +var _ analyzers.Analyzer = (*Analyzer)(nil) + +type Analyzer struct { + Cfg *config.Config +} + +func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeNotion } + +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.AnalyzerTypeNotion, + Metadata: nil, + Bindings: make([]analyzers.Binding, len(info.Permissions)), + UnboundedResources: make([]analyzers.Resource, 0, len(info.WorkspaceUsers)), + } + + resource := analyzers.Resource{ + Name: info.Bot.Name, + FullyQualifiedName: "notion.so/bot/" + info.Bot.Id, + Type: info.Bot.Type, + Metadata: map[string]interface{}{ + "workspace": info.Bot.GetWorkspaceName(), + }, + } + + for idx, permission := range info.Permissions { + result.Bindings[idx] = analyzers.Binding{ + Resource: resource, + Permission: analyzers.Permission{ + Value: permission, + }, + } + } + + // We can find list of users in the current workspace + // if the API key has read_user permission, so these can be + // unbounded resources + for _, user := range info.WorkspaceUsers { + if info.Bot.Id == user.Id { + // Skip the bot itself + continue + } + unboundresource := analyzers.Resource{ + Name: user.Name, + FullyQualifiedName: fmt.Sprintf("notion.so/%s/%s", user.Type, user.Id), + Type: user.Type, // person or bot + } + if user.Person.Email != "" { + unboundresource.Metadata = map[string]interface{}{ + "email": user.Person.Email, + } + } + + result.UnboundedResources = append(result.UnboundedResources, unboundresource) + } + + return &result +} + +//go:embed scopes.json +var scopesConfig []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, headers map[string]string) (bool, error) { + // If body data, marshal to JSON + var data io.Reader + if h.Payload != nil { + jsonData, err := json.Marshal(h.Payload) + if err != nil { + return false, err + } + data = bytes.NewBuffer(jsonData) + } + + // Create new HTTP request + client := analyzers.NewAnalyzeClientUnrestricted(cfg) + req, err := http.NewRequest(h.Method, h.Endpoint, data) + if err != nil { + return false, err + } + + // Add custom headers if provided + for key, value := range headers { + req.Header.Set(key, value) + } + + // Execute HTTP Request + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + // Check response status code + switch { + case StatusContains(resp.StatusCode, h.ValidStatuses): + return true, nil + case StatusContains(resp.StatusCode, h.InvalidStatuses): + return false, nil + default: + return false, errors.New("error checking response status code") + } +} + +type Scope struct { + Name string `json:"name"` + HttpTest HttpStatusTest `json:"test"` +} + +func readInScopes() ([]Scope, error) { + var scopes []Scope + if err := json.Unmarshal(scopesConfig, &scopes); err != nil { + return nil, err + } + + return scopes, nil +} + +func getPermissions(cfg *config.Config, key string) ([]string, error) { + scopes, err := readInScopes() + if err != nil { + return nil, fmt.Errorf("reading in scopes: %w", err) + } + + permissions := make([]string, 0, len(scopes)) + for _, scope := range scopes { + status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "Bearer " + key, "Notion-Version": "2022-06-28"}) + if err != nil { + return nil, fmt.Errorf("running test: %w", err) + } + if status { + permissions = append(permissions, scope.Name) + } + } + + return permissions, nil +} + +type SecretInfo struct { + Bot *bot + WorkspaceUsers []user + Permissions []string +} + +type user struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Person struct { + Email string `json:"email"` + } +} + +type bot struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Bot struct { + Owner *struct { + Type string `json:"type"` + } + WorkspaceName string `json:"workspace_name"` + } `json:"bot"` +} + +func (b *bot) GetWorkspaceName() string { + return b.Bot.WorkspaceName +} + +func (b *bot) OwnedBy() string { + if b.Bot.Owner != nil { + return b.Bot.Owner.Type + } + return "N/A" +} + +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 Notion API key\n\n") + + color.Green("[i] Bot: %s (%s)\n", info.Bot.Name, info.Bot.Id) + color.Green("[i] Bot Owned By: %s\n", info.Bot.OwnedBy()) + + if info.Bot.GetWorkspaceName() != "" { + color.Green("[i] Workspace: %s\n\n", info.Bot.GetWorkspaceName()) + } + + printPermissions(info.Permissions) + if len(info.WorkspaceUsers) > 0 { + printUsers(info.WorkspaceUsers) + } + color.Yellow("\n[i] Expires: Never") + +} + +func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) { + permissions := make([]string, 0) + + client := analyzers.NewAnalyzeClient(cfg) + + bot, err := getBotInfo(client, key) + if err != nil { + return nil, err + } + + credPermissions, err := getPermissions(cfg, key) + if err != nil { + return nil, err + } + + permissions = append(permissions, credPermissions...) + + users, err := getWorkspaceUsers(client, key) + if err != nil { + return nil, fmt.Errorf("error getting user permission: %s", err.Error()) + } + + // check if email is returned in users to determine permission + for _, user := range users { + if user.Type == "person" { + if user.Person.Email == "" { + permissions = append(permissions, PermissionStrings[ReadUsersWithoutEmail]) + } else { + permissions = append(permissions, PermissionStrings[ReadUsersWithEmail]) + } + break + } + } + return &SecretInfo{ + Bot: bot, + Permissions: permissions, + WorkspaceUsers: users, + }, nil +} + +func printPermissions(permissions []string) { + color.Yellow("[i] Permissions:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Permission"}) + for _, permission := range permissions { + t.AppendRow(table.Row{color.GreenString(permission)}) + } + t.Render() +} + +func printUsers(users []user) { + color.Yellow("\n[i] Workspace Users:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"ID", "Name", "Type", "Email"}) + for _, user := range users { + t.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Type), color.GreenString(user.Person.Email)}) + } + t.Render() +} + +func getBotInfo(client *http.Client, key string) (*bot, error) { + // Create new HTTP request + req, err := http.NewRequest(http.MethodGet, "https://api.notion.com/v1/users/me", http.NoBody) + if err != nil { + return nil, err + } + + // Add custom headers if provided + req.Header.Set("Authorization", "Bearer "+key) + req.Header.Set("Notion-Version", "2022-06-28") + + // Execute HTTP Request + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + me := &bot{} + err = json.NewDecoder(resp.Body).Decode(me) + if err != nil { + return nil, err + } + return me, nil + case http.StatusUnauthorized: + return nil, errors.New("invalid API key") + default: + return nil, errors.New("error getting bot info") + } +} + +// Decode response body +type usersResponse struct { + Results []user `json:"results"` +} + +func getWorkspaceUsers(client *http.Client, key string) ([]user, error) { + // Create new HTTP request + req, err := http.NewRequest(http.MethodGet, "https://api.notion.com/v1/users", http.NoBody) + if err != nil { + return nil, err + } + + // Add custom headers if provided + req.Header.Set("Authorization", "Bearer "+key) + req.Header.Set("Notion-Version", "2022-06-28") + + // Execute HTTP Request + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + response := &usersResponse{} + err = json.NewDecoder(resp.Body).Decode(response) + if err != nil { + return nil, err + } + return response.Results, nil + case http.StatusUnauthorized: + return nil, errors.New("invalid API key") + case http.StatusForbidden: + return nil, nil // no permission + case http.StatusNotFound: + return nil, errors.New("workspace not found") + default: + return nil, errors.New("error checking user permissions") + } + +} diff --git a/pkg/analyzer/analyzers/notion/notion_test.go b/pkg/analyzer/analyzers/notion/notion_test.go new file mode 100644 index 000000000..f6d92eaab --- /dev/null +++ b/pkg/analyzer/analyzers/notion/notion_test.go @@ -0,0 +1,100 @@ +package notion + +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", "detectors3") + 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 notion key", + key: testSecrets.MustGetField("NOTION_TOKEN"), + 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/notion/permissions.go b/pkg/analyzer/analyzers/notion/permissions.go new file mode 100644 index 000000000..deb025966 --- /dev/null +++ b/pkg/analyzer/analyzers/notion/permissions.go @@ -0,0 +1,91 @@ +// Code generated by go generate; DO NOT EDIT. +package notion + +import "errors" + +type Permission int + +const ( + Invalid Permission = iota + ReadContent Permission = iota + UpdateContent Permission = iota + InsertContent Permission = iota + ReadComments Permission = iota + InsertComments Permission = iota + ReadUsersWithEmail Permission = iota + ReadUsersWithoutEmail Permission = iota +) + +var ( + PermissionStrings = map[Permission]string{ + ReadContent: "read_content", + UpdateContent: "update_content", + InsertContent: "insert_content", + ReadComments: "read_comments", + InsertComments: "insert_comments", + ReadUsersWithEmail: "read_users_with_email", + ReadUsersWithoutEmail: "read_users_without_email", + } + + StringToPermission = map[string]Permission{ + "read_content": ReadContent, + "update_content": UpdateContent, + "insert_content": InsertContent, + "read_comments": ReadComments, + "insert_comments": InsertComments, + "read_users_with_email": ReadUsersWithEmail, + "read_users_without_email": ReadUsersWithoutEmail, + } + + PermissionIDs = map[Permission]int{ + ReadContent: 1, + UpdateContent: 2, + InsertContent: 3, + ReadComments: 4, + InsertComments: 5, + ReadUsersWithEmail: 6, + ReadUsersWithoutEmail: 7, + } + + IdToPermission = map[int]Permission{ + 1: ReadContent, + 2: UpdateContent, + 3: InsertContent, + 4: ReadComments, + 5: InsertComments, + 6: ReadUsersWithEmail, + 7: ReadUsersWithoutEmail, + } +) + +// 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/notion/permissions.yaml b/pkg/analyzer/analyzers/notion/permissions.yaml new file mode 100644 index 000000000..269a1b0f6 --- /dev/null +++ b/pkg/analyzer/analyzers/notion/permissions.yaml @@ -0,0 +1,8 @@ +permissions: + - read_content + - update_content + - insert_content + - read_comments + - insert_comments + - read_users_with_email + - read_users_without_email \ No newline at end of file diff --git a/pkg/analyzer/analyzers/notion/scopes.json b/pkg/analyzer/analyzers/notion/scopes.json new file mode 100644 index 000000000..25f645dc5 --- /dev/null +++ b/pkg/analyzer/analyzers/notion/scopes.json @@ -0,0 +1,47 @@ +[ + { + "name": "read_content", + "test": { + "endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist", + "method": "GET", + "valid_status_code": [400], + "invalid_status_code": [403] + } + }, + { + "name": "update_content", + "test": { + "endpoint": "https://api.notion.com/v1/pages/`nowaythiscanexist", + "method": "PATCH", + "valid_status_code": [400], + "invalid_status_code": [403] + } + }, + { + "name": "insert_content", + "test": { + "endpoint": "https://api.notion.com/v1/pages", + "method": "POST", + "valid_status_code": [400], + "invalid_status_code": [403] + } + }, + { + "name": "read_comments", + "test": { + "endpoint": "https://api.notion.com/v1/comments", + "method": "GET", + "valid_status_code": [400], + "invalid_status_code": [403] + } + }, + { + "name": "insert_comments", + "test": { + "endpoint": "https://api.notion.com/v1/comments", + "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 cc1e12170..f7cbebcef 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/alecthomas/kingpin/v2" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket" @@ -14,6 +15,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailchimp" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mysql" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/notion" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/openai" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres" @@ -85,6 +87,8 @@ func Run(keyType string, secretInfo SecretInfo) { opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) case "privatekey": privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) + case "notion": + notion.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) case "dockerhub": dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"]) } diff --git a/pkg/detectors/notion/notion.go b/pkg/detectors/notion/notion.go index 3d33a4121..614981e86 100644 --- a/pkg/detectors/notion/notion.go +++ b/pkg/detectors/notion/notion.go @@ -60,6 +60,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result // Notion returns 401 for all non-valid keys, thus 403 indicates it has fine-tuned permissions, // /v1/search, /v1/databases/*, etc. may work. s1.Verified = true + s1.AnalysisInfo = map[string]string{"key": resMatch} } } else {