[Feat] implementation Notion analyzer (#3869)
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
### 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.  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))?
This commit is contained in:
@@ -82,6 +82,7 @@ const (
|
|||||||
AnalyzerTypeStripe
|
AnalyzerTypeStripe
|
||||||
AnalyzerTypeTwilio
|
AnalyzerTypeTwilio
|
||||||
AnalyzerTypePrivateKey
|
AnalyzerTypePrivateKey
|
||||||
|
AnalyzerTypeNotion
|
||||||
// Add new items here with AnalyzerType prefix
|
// Add new items here with AnalyzerType prefix
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
|||||||
AnalyzerTypeStripe: "Stripe",
|
AnalyzerTypeStripe: "Stripe",
|
||||||
AnalyzerTypeTwilio: "Twilio",
|
AnalyzerTypeTwilio: "Twilio",
|
||||||
AnalyzerTypePrivateKey: "PrivateKey",
|
AnalyzerTypePrivateKey: "PrivateKey",
|
||||||
|
AnalyzerTypeNotion: "Notion",
|
||||||
// Add new mappings here
|
// Add new mappings here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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":"[email protected]"},"Parent":null}],"Metadata":null}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
permissions:
|
||||||
|
- read_content
|
||||||
|
- update_content
|
||||||
|
- insert_content
|
||||||
|
- read_comments
|
||||||
|
- insert_comments
|
||||||
|
- read_users_with_email
|
||||||
|
- read_users_without_email
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin/v2"
|
"github.com/alecthomas/kingpin/v2"
|
||||||
|
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake"
|
"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/asana"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
|
"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/mailchimp"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun"
|
"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/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/openai"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres"
|
"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"])
|
opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
case "privatekey":
|
case "privatekey":
|
||||||
privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
|
case "notion":
|
||||||
|
notion.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
case "dockerhub":
|
case "dockerhub":
|
||||||
dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"])
|
dockerhub.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["username"], secretInfo.Parts["pat"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
// Notion returns 401 for all non-valid keys, thus 403 indicates it has fine-tuned permissions,
|
||||||
// /v1/search, /v1/databases/*, etc. may work.
|
// /v1/search, /v1/databases/*, etc. may work.
|
||||||
s1.Verified = true
|
s1.Verified = true
|
||||||
|
s1.AnalysisInfo = map[string]string{"key": resMatch}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user