Skip to content

Commit

Permalink
[analyze] Add Analyzer for Sendgrid (#3174)
Browse files Browse the repository at this point in the history
* implement analyzer interface for sendgrid

* add unit test for sendgrid analyzer

* fixed sendgrid detector.
linked analyzer with sendgrid detector.
handling if key not found in map

* category as resource.
if subcategory is present then subcategory will become resource with parent category

* corrected test and remove hardcoded boolean for printing in sendgrid

* incorporate code refactoring and suggestion for FullyQualifiedName for subcategory by Miccah.

* generate permissions for sendgrid analyzer

* [NIT] rather than updating the global scopes variable, ProcessPermission will return new list of categories with Permission and eliminate those which are not in generated one.

---------

Co-authored-by: Abdul Basit <[email protected]>
  • Loading branch information
abmussani and abasit-folio3 authored Sep 4, 2024
1 parent d6e1627 commit 899f59f
Show file tree
Hide file tree
Showing 6 changed files with 1,396 additions and 13 deletions.
991 changes: 991 additions & 0 deletions pkg/analyzer/analyzers/sendgrid/permissions.go

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions pkg/analyzer/analyzers/sendgrid/permissions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
permissions:
- access_settings.activity.read
- access_settings.whitelist.create
- access_settings.whitelist.delete
- access_settings.whitelist.read
- access_settings.whitelist.update
- alerts.create
- alerts.delete
- alerts.read
- alerts.update
- api_keys.create
- api_keys.delete
- api_keys.read
- api_keys.update
- asm.groups.create
- asm.groups.delete
- asm.groups.read
- asm.groups.update
- billing.create
- billing.delete
- billing.read
- billing.update
- browsers.stats.read
- categories.create
- categories.delete
- categories.read
- categories.stats.read
- categories.stats.sums.read
- categories.update
- clients.desktop.stats.read
- clients.phone.stats.read
- clients.stats.read
- clients.tablet.stats.read
- clients.webmail.stats.read
- devices.stats.read
- email_activity.read
- geo.stats.read
- ips.assigned.read
- ips.pools.create
- ips.pools.delete
- ips.pools.ips.create
- ips.pools.ips.delete
- ips.pools.ips.read
- ips.pools.ips.update
- ips.pools.read
- ips.pools.update
- ips.read
- ips.warmup.create
- ips.warmup.delete
- ips.warmup.read
- ips.warmup.update
- mail_settings.address_whitelist.read
- mail_settings.address_whitelist.update
- mail_settings.bounce_purge.read
- mail_settings.bounce_purge.update
- mail_settings.footer.read
- mail_settings.footer.update
- mail_settings.forward_bounce.read
- mail_settings.forward_bounce.update
- mail_settings.forward_spam.read
- mail_settings.forward_spam.update
- mail_settings.plain_content.read
- mail_settings.plain_content.update
- mail_settings.read
- mail_settings.template.read
- mail_settings.template.update
- mail.batch.create
- mail.batch.delete
- mail.batch.read
- mail.batch.update
- mail.send
- mailbox_providers.stats.read
- marketing_campaigns.create
- marketing_campaigns.delete
- marketing_campaigns.read
- marketing_campaigns.update
- partner_settings.new_relic.read
- partner_settings.new_relic.update
- partner_settings.read
- stats.global.read
- stats.read
- subusers.create
- subusers.credits.create
- subusers.credits.delete
- subusers.credits.read
- subusers.credits.remaining.create
- subusers.credits.remaining.delete
- subusers.credits.remaining.read
- subusers.credits.remaining.update
- subusers.credits.update
- subusers.delete
- subusers.monitor.create
- subusers.monitor.delete
- subusers.monitor.read
- subusers.monitor.update
- subusers.read
- subusers.reputations.read
- subusers.stats.monthly.read
- subusers.stats.read
- subusers.stats.sums.read
- subusers.summary.read
- subusers.update
- suppression.blocks.create
- suppression.blocks.delete
- suppression.blocks.read
- suppression.blocks.update
- suppression.bounces.create
- suppression.bounces.delete
- suppression.bounces.read
- suppression.bounces.update
- suppression.create
- suppression.delete
- suppression.invalid_emails.create
- suppression.invalid_emails.delete
- suppression.invalid_emails.read
- suppression.invalid_emails.update
- suppression.read
- suppression.spam_reports.create
- suppression.spam_reports.delete
- suppression.spam_reports.read
- suppression.spam_reports.update
- suppression.unsubscribes.create
- suppression.unsubscribes.delete
- suppression.unsubscribes.read
- suppression.unsubscribes.update
- suppression.update
- teammates.create
- teammates.read
- teammates.update
- teammates.delete
- templates.create
- templates.delete
- templates.read
- templates.update
- templates.versions.activate.create
- templates.versions.activate.delete
- templates.versions.activate.read
- templates.versions.activate.update
- templates.versions.create
- templates.versions.delete
- templates.versions.read
- templates.versions.update
- tracking_settings.click.read
- tracking_settings.click.update
- tracking_settings.google_analytics.read
- tracking_settings.google_analytics.update
- tracking_settings.open.read
- tracking_settings.open.update
- tracking_settings.read
- tracking_settings.subscription.read
- tracking_settings.subscription.update
- user.account.read
- user.credits.read
- user.email.create
- user.email.delete
- user.email.read
- user.email.update
- user.multifactor_authentication.create
- user.multifactor_authentication.delete
- user.multifactor_authentication.read
- user.multifactor_authentication.update
- user.password.read
- user.password.update
- user.profile.read
- user.profile.update
- user.scheduled_sends.create
- user.scheduled_sends.delete
- user.scheduled_sends.read
- user.scheduled_sends.update
- user.settings.enforced_tls.read
- user.settings.enforced_tls.update
- user.timezone.read
- user.username.read
- user.username.update
- user.webhooks.event.settings.read
- user.webhooks.event.settings.update
- user.webhooks.event.test.create
- user.webhooks.event.test.read
- user.webhooks.event.test.update
- user.webhooks.parse.settings.create
- user.webhooks.parse.settings.delete
- user.webhooks.parse.settings.read
- user.webhooks.parse.settings.update
- user.webhooks.parse.stats.read
- whitelabel.create
- whitelabel.delete
- whitelabel.read
- whitelabel.update
130 changes: 118 additions & 12 deletions pkg/analyzer/analyzers/sendgrid/sendgrid.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:generate generate_permissions permissions.yaml permissions.go sendgrid

package sendgrid

import (
Expand All @@ -13,17 +15,106 @@ import (

"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)

var _ analyzers.Analyzer = (*Analyzer)(nil)

type Analyzer struct {
Cfg *config.Config
}

func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Sendgrid }

func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, fmt.Errorf("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}

func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}

var keyType string
if slices.Contains(info.RawScopes, "user.email.read") {
keyType = "full access"
} else if slices.Contains(info.RawScopes, "billing.read") {
keyType = "billing access"
} else {
keyType = "restricted access"
}

result := analyzers.AnalyzerResult{
AnalyzerType: analyzerpb.AnalyzerType_Sendgrid,
Metadata: map[string]any{
"key_type": keyType,
"2fa_required": slices.Contains(info.RawScopes, "2fa_required"),
},
Bindings: []analyzers.Binding{},
UnboundedResources: []analyzers.Resource{},
}

for _, scope := range info.Scopes {
resource := getCategoryResource(scope)

if len(scope.Permissions) == 0 {
result.UnboundedResources = append(result.UnboundedResources, *resource)
continue
}

for _, permission := range scope.Permissions {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *resource,
Permission: analyzers.Permission{
Value: permission,
},
})
}
}

return &result
}

func getCategoryResource(scope SendgridScope) *analyzers.Resource {
categoryResource := &analyzers.Resource{
Name: scope.Category,
FullyQualifiedName: scope.Category,
Type: "category",
Metadata: nil,
}

if scope.SubCategory != "" {
return &analyzers.Resource{
Name: scope.SubCategory,
FullyQualifiedName: fmt.Sprintf("%s/%s", scope.Category, scope.SubCategory),
Type: "category",
Metadata: nil,
Parent: categoryResource,
}
}

return categoryResource
}

type ScopesJSON struct {
Scopes []string `json:"scopes"`
}

type SecretInfo struct {
RawScopes []string
Scopes []SendgridScope
}

func printPermissions(show_all bool) {
func printPermissions(info *SecretInfo, show_all bool) {
fmt.Print("\n\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
Expand All @@ -33,7 +124,7 @@ func printPermissions(show_all bool) {
t.AppendHeader(table.Row{"Scope", "Sub-Scope", "Access"})
}
// Print the scopes
for _, s := range SCOPES {
for _, s := range info.Scopes {
writer := analyzers.GetWriterFromStatus(s.PermissionType)
if show_all {
t.AppendRow([]interface{}{writer(s.Category), writer(s.SubCategory), writer(s.PermissionType), writer(strings.Join(s.Permissions, "\n"))})
Expand All @@ -49,11 +140,11 @@ func printPermissions(show_all bool) {
// It will return the most specific category possible.
// For example, if the scope is "mail.send.read", it will return "Mail Send", not just "Mail"
// since it's searching "mail.send.read" -> "mail.send" -> "mail"
func getScopeIndex(scope string) int {
func getScopeIndex(categories []SendgridScope, scope string) int {
splitScope := strings.Split(scope, ".")
for i := len(splitScope); i > 0; i-- {
searchScope := strings.Join(splitScope[:i], ".")
for i, s := range SCOPES {
for i, s := range categories {
for _, prefix := range s.Prefixes {
if strings.HasPrefix(searchScope, prefix) {
return i
Expand All @@ -64,24 +155,36 @@ func getScopeIndex(scope string) int {
return -1
}

func processPermissions(rawScopes []string) {
func processPermissions(rawScopes []string) []SendgridScope {
categoryPermissions := make([]SendgridScope, len(SCOPES))

// copy all scope categories to the categoryPermissions slice
copy(categoryPermissions, SCOPES)
for _, scope := range rawScopes {
// Skip these scopes since they are not useful for this analysis
if scope == "2fa_required" || scope == "sender_verification_eligible" {
continue
}
ind := getScopeIndex(scope)

// must be part of generated permissions
if _, ok := StringToPermission[scope]; !ok {
continue
}
ind := getScopeIndex(categoryPermissions, scope)
if ind == -1 {
//color.Red("[!] Scope not found: %v", scope)
continue
}
s := &SCOPES[ind]
s := &categoryPermissions[ind]
s.AddPermission(scope)
}

// Run tests to determine the permission type
for i := range SCOPES {
SCOPES[i].RunTests()
for i := range categoryPermissions {
categoryPermissions[i].RunTests()
}

return categoryPermissions
}

func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
Expand All @@ -105,7 +208,7 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
color.Yellow("[i] 2FA Required for this account")
}

printPermissions(cfg.ShowAll)
printPermissions(info, cfg.ShowAll)
}

func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
Expand Down Expand Up @@ -133,7 +236,10 @@ func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// Now you can access the scopes
rawScopes := jsonScopes.Scopes

processPermissions(rawScopes)
categoryScope := processPermissions(rawScopes)

return &SecretInfo{RawScopes: rawScopes}, nil
return &SecretInfo{
RawScopes: rawScopes,
Scopes: categoryScope,
}, nil
}
Loading

0 comments on commit 899f59f

Please sign in to comment.