mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-06-15 13:24:37 +00:00
Return an explicit error when AnalyzePermissions yields nil info instead of passing nil to secretInfoToAnalyzerResult. Wrap classic PAT repo/gist enumeration errors with context for easier debugging.
218 lines
6.6 KiB
Go
218 lines
6.6 KiB
Go
package github
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
gh "github.com/google/go-github/v67/github"
|
|
"golang.org/x/time/rate"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/classic"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/finegrained"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
// According to GitHub's rate limiting documentation, the default rate limit for
|
|
// authenticated requests (PAT) is 5000 requests per hour. This equates to roughly 1.39
|
|
// requests per second. To provide some buffer, we set the rate limit to 1.25
|
|
// requests per second with a burst of 10.
|
|
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28
|
|
var rateLimiter = rate.NewLimiter(rate.Limit(1.25), 10)
|
|
|
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeGitHub }
|
|
|
|
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
|
info, err := AnalyzePermissions(a.Cfg, credInfo["key"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info == nil {
|
|
return nil, fmt.Errorf("GitHub analyzer returned no data for token")
|
|
}
|
|
return secretInfoToAnalyzerResult(info), nil
|
|
}
|
|
|
|
func secretInfoToAnalyzerResult(info *common.SecretInfo) *analyzers.AnalyzerResult {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
result := &analyzers.AnalyzerResult{
|
|
AnalyzerType: analyzers.AnalyzerTypeGitHub,
|
|
Metadata: map[string]any{
|
|
"owner": info.Metadata.User.Login,
|
|
"type": info.Metadata.Type,
|
|
"expiration": info.Metadata.Expiration,
|
|
},
|
|
}
|
|
result.Bindings = append(result.Bindings, secretInfoToUserBindings(info)...)
|
|
result.Bindings = append(result.Bindings, secretInfoToRepoBindings(info)...)
|
|
result.Bindings = append(result.Bindings, secretInfoToGistBindings(info)...)
|
|
for _, repo := range append(info.Repos, info.AccessibleRepos...) {
|
|
if repo.Owner.GetType() != "Organization" {
|
|
continue
|
|
}
|
|
name := repo.Owner.GetName()
|
|
if name == "" {
|
|
continue
|
|
}
|
|
result.UnboundedResources = append(result.UnboundedResources, analyzers.Resource{
|
|
Name: name,
|
|
FullyQualifiedName: fmt.Sprintf("github.com/%s", name),
|
|
Type: "organization",
|
|
})
|
|
}
|
|
// TODO: Unbound resources
|
|
// - Repo owners
|
|
// - Gist owners
|
|
return result
|
|
}
|
|
|
|
func secretInfoToUserBindings(info *common.SecretInfo) []analyzers.Binding {
|
|
return analyzers.BindAllPermissions(*userToResource(info.Metadata.User), info.Metadata.OauthScopes...)
|
|
}
|
|
|
|
func userToResource(user *gh.User) *analyzers.Resource {
|
|
name := *user.Login
|
|
return &analyzers.Resource{
|
|
Name: name,
|
|
FullyQualifiedName: fmt.Sprintf("github.com/%s", name),
|
|
Type: strings.ToLower(*user.Type), // "user" or "organization"
|
|
}
|
|
}
|
|
|
|
func secretInfoToRepoBindings(info *common.SecretInfo) []analyzers.Binding {
|
|
var perms []analyzers.Permission
|
|
switch info.Metadata.Type {
|
|
case common.TokenTypeClassicPAT:
|
|
perms = info.Metadata.OauthScopes
|
|
case common.TokenTypeFineGrainedPAT:
|
|
fineGrainedPermissions := info.RepoAccessMap.([]finegrained.Permission)
|
|
for _, perm := range fineGrainedPermissions {
|
|
permName, _ := perm.ToString()
|
|
perms = append(perms, analyzers.Permission{Value: permName})
|
|
}
|
|
default:
|
|
if len(info.Metadata.OauthScopes) > 0 {
|
|
perms = info.Metadata.OauthScopes
|
|
}
|
|
}
|
|
|
|
repos := info.Repos
|
|
if len(info.AccessibleRepos) > 0 {
|
|
repos = info.AccessibleRepos
|
|
}
|
|
var bindings []analyzers.Binding
|
|
for _, repo := range repos {
|
|
resource := analyzers.Resource{
|
|
Name: *repo.Name,
|
|
FullyQualifiedName: fmt.Sprintf("github.com/%s", *repo.FullName),
|
|
Type: "repository",
|
|
Parent: userToResource(repo.Owner),
|
|
}
|
|
bindings = append(bindings, analyzers.BindAllPermissions(resource, perms...)...)
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
func secretInfoToGistBindings(info *common.SecretInfo) []analyzers.Binding {
|
|
var bindings []analyzers.Binding
|
|
for _, gist := range info.Gists {
|
|
resource := analyzers.Resource{
|
|
Name: *gist.Description,
|
|
FullyQualifiedName: fmt.Sprintf("gist.github.com/%s/%s", *gist.Owner.Login, *gist.ID),
|
|
Type: "gist",
|
|
Parent: userToResource(gist.Owner),
|
|
}
|
|
bindings = append(bindings, analyzers.BindAllPermissions(resource, info.Metadata.OauthScopes...)...)
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
func AnalyzePermissions(cfg *config.Config, key string) (*common.SecretInfo, error) {
|
|
if cfg == nil {
|
|
cfg = &config.Config{}
|
|
}
|
|
client := gh.NewClient(analyzers.NewAnalyzeClient(cfg, analyzers.WithRateLimiter(rateLimiter))).WithAuthToken(key)
|
|
|
|
md, err := common.GetTokenMetadata(key, client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if md.FineGrained {
|
|
return finegrained.AnalyzeFineGrainedToken(client, md, cfg.Shallow)
|
|
} else {
|
|
return classic.AnalyzeClassicToken(client, md)
|
|
}
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
info, err := AnalyzePermissions(cfg, key)
|
|
if err != nil {
|
|
color.Red("[x] %s", err.Error())
|
|
return
|
|
}
|
|
|
|
color.Yellow("[i] Token User: %v", *info.Metadata.User.Login)
|
|
if expiry := info.Metadata.Expiration; expiry.IsZero() {
|
|
color.Red("[i] Token Expiration: does not expire")
|
|
} else {
|
|
timeRemaining := time.Until(expiry)
|
|
color.Yellow("[i] Token Expiration: %v (%s remaining)", expiry, roughHumanReadableDuration(timeRemaining))
|
|
}
|
|
color.Yellow("[i] Token Type: %s\n\n", info.Metadata.Type)
|
|
|
|
if info.Metadata.FineGrained {
|
|
finegrained.PrintFineGrainedToken(cfg, info)
|
|
return
|
|
}
|
|
classic.PrintClassicToken(cfg, info)
|
|
}
|
|
|
|
// roughHumanReadableDuration converts a duration into a rough estimate for
|
|
// human consumption. The larger the duration, the larger granularity is
|
|
// returned.
|
|
func roughHumanReadableDuration(d time.Duration) string {
|
|
var gran time.Duration
|
|
var unit string
|
|
switch {
|
|
case d < 1*time.Minute:
|
|
gran = time.Second
|
|
unit = "second"
|
|
case d < 1*time.Hour:
|
|
gran = time.Minute
|
|
unit = "minute"
|
|
case d < 24*time.Hour:
|
|
gran = time.Hour
|
|
unit = "hour"
|
|
case d < 4*7*24*time.Hour:
|
|
gran = 24 * time.Hour
|
|
unit = "day"
|
|
case d < 3*4*7*24*time.Hour:
|
|
gran = 7 * 24 * time.Hour
|
|
unit = "week"
|
|
case d < 5*365*24*time.Hour:
|
|
gran = 365 * 24 * time.Hour
|
|
unit = "month"
|
|
default:
|
|
gran = 365 * 24 * time.Hour
|
|
unit = "year"
|
|
}
|
|
num := d.Round(gran) / gran
|
|
if num != 1 {
|
|
unit += "s"
|
|
}
|
|
return fmt.Sprintf("%d %s", num, unit)
|
|
}
|