mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-06-15 13:24:37 +00:00
214 lines
5.4 KiB
Go
214 lines
5.4 KiB
Go
//go:generate generate_permissions permissions.yaml permissions.go sourcegraph
|
|
package sourcegraph
|
|
|
|
// ToDo: Add support for custom domain
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeSourcegraph }
|
|
|
|
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
|
key, ok := credInfo["key"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing key in credInfo")
|
|
}
|
|
info, err := AnalyzePermissions(a.Cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return secretInfoToAnalyzerResult(info), nil
|
|
}
|
|
|
|
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
|
|
permission := PermissionStrings[UserRead]
|
|
if info.IsSiteAdmin {
|
|
permission = PermissionStrings[SiteAdminFull]
|
|
}
|
|
result := analyzers.AnalyzerResult{
|
|
AnalyzerType: analyzers.AnalyzerTypeSourcegraph,
|
|
Metadata: nil,
|
|
Bindings: []analyzers.Binding{
|
|
{
|
|
Resource: analyzers.Resource{
|
|
Name: info.User.Data.CurrentUser.Username,
|
|
FullyQualifiedName: "sourcegraph/" + info.User.Data.CurrentUser.Email,
|
|
Type: "user",
|
|
Metadata: map[string]any{
|
|
"created_at": info.User.Data.CurrentUser.CreatedAt,
|
|
"email": info.User.Data.CurrentUser.Email,
|
|
},
|
|
Parent: nil,
|
|
},
|
|
Permission: analyzers.Permission{
|
|
Value: permission,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
type GraphQLError struct {
|
|
Message string `json:"message"`
|
|
Path []string `json:"path"`
|
|
}
|
|
|
|
type GraphQLResponse struct {
|
|
Errors []GraphQLError `json:"errors"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
type UserInfoJSON struct {
|
|
Data struct {
|
|
CurrentUser struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
SiteAdmin bool `json:"siteAdmin"`
|
|
CreatedAt string `json:"createdAt"`
|
|
} `json:"currentUser"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type SecretInfo struct {
|
|
User UserInfoJSON
|
|
IsSiteAdmin bool
|
|
}
|
|
|
|
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
|
|
var userInfo UserInfoJSON
|
|
|
|
// POST request is considered as non-safe and sourcegraph has graphql APIs. They do not change any state.
|
|
// We are using unrestricted client to avoid error for non-safe API request.
|
|
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
|
|
payload := "{\"query\":\"query { currentUser { username, email, siteAdmin, createdAt } }\"}"
|
|
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(payload))
|
|
if err != nil {
|
|
return userInfo, err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "token "+key)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return userInfo, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(&userInfo)
|
|
if err != nil {
|
|
return userInfo, err
|
|
}
|
|
return userInfo, nil
|
|
}
|
|
|
|
func checkSiteAdmin(cfg *config.Config, key string) (bool, error) {
|
|
query := `
|
|
{
|
|
"query": "query webhooks($first: Int, $after: String, $kind: ExternalServiceKind) { webhooks(first: $first, after: $after, kind: $kind) { totalCount } }",
|
|
"variables": {
|
|
"first": 10,
|
|
"after": "",
|
|
"kind": "GITHUB"
|
|
}
|
|
}`
|
|
|
|
// POST request is considered as non-safe and sourcegraph has graphql APIs. They do not change any state.
|
|
// We are using unrestricted client to avoid error for non-safe API request.
|
|
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
|
|
req, err := http.NewRequest("POST", "https://sourcegraph.com/.api/graphql", strings.NewReader(query))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "token "+key)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var response GraphQLResponse
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(response.Errors) > 0 {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
// ToDo: Add in logging
|
|
if cfg.LoggingEnabled {
|
|
color.Red("[x] Logging is not supported for this analyzer.")
|
|
return
|
|
}
|
|
|
|
info, err := AnalyzePermissions(cfg, key)
|
|
if err != nil {
|
|
color.Red("[x] Error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
color.Green("[!] Valid Sourcegraph Access Token\n\n")
|
|
color.Yellow("[i] Sourcegraph User Information\n")
|
|
color.Green("Username: %s\n", info.User.Data.CurrentUser.Username)
|
|
color.Green("Email: %s\n", info.User.Data.CurrentUser.Email)
|
|
color.Green("Created At: %s\n\n", info.User.Data.CurrentUser.CreatedAt)
|
|
|
|
if info.IsSiteAdmin {
|
|
color.Green("[!] Token Permissions: Site Admin")
|
|
} else {
|
|
// This is the default for all access tokens as of 6/11/24
|
|
color.Yellow("[i] Token Permissions: user:full (default)")
|
|
}
|
|
}
|
|
|
|
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
|
userInfo, err := getUserInfo(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if userInfo.Data.CurrentUser.Username == "" {
|
|
return nil, fmt.Errorf("invalid Sourcegraph Access Token")
|
|
}
|
|
|
|
isSiteAdmin, err := checkSiteAdmin(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SecretInfo{
|
|
User: userInfo,
|
|
IsSiteAdmin: isSiteAdmin,
|
|
}, nil
|
|
}
|