package common import ( "context" "fmt" "os" "strings" "time" "github.com/fatih/color" gh "github.com/google/go-github/v67/github" "github.com/jedib0t/go-pretty/v6/table" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" ) type TokenType string const ( TokenTypeFineGrainedPAT TokenType = "Fine-Grained GitHub Personal Access Token" TokenTypeClassicPAT TokenType = "Classic GitHub Personal Access Token" TokenTypeUserToServer TokenType = "GitHub User-to-Server Token" TokenTypeGitHubToken TokenType = "GitHub Token" ) func checkFineGrained(token string, oauthScopes []analyzers.Permission) (TokenType, bool) { // For details on token prefixes, see: // https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ // Special case for ghu_ prefix tokens (ex: in a codespace) that don't have the X-OAuth-Scopes header if strings.HasPrefix(token, "ghu_") { return TokenTypeUserToServer, true } // Handle github_pat_ tokens if strings.HasPrefix(token, "github_pat") { return TokenTypeFineGrainedPAT, true } // Handle classic PATs if strings.HasPrefix(token, "ghp_") { return TokenTypeClassicPAT, false } // Catch-all for any other types // If resp.Header "X-OAuth-Scopes" doesn't exist, then we have fine-grained permissions if len(oauthScopes) > 0 { return TokenTypeGitHubToken, false } return TokenTypeGitHubToken, true } type Permission int type SecretInfo struct { Metadata *TokenMetadata Repos []*gh.Repository Gists []*gh.Gist // AccessibleRepos, RepoAccessMap, and UserAccessMap are only set if // the token has fine-grained access. AccessibleRepos []*gh.Repository RepoAccessMap any UserAccessMap any } type TokenMetadata struct { Type TokenType FineGrained bool User *gh.User Expiration time.Time // OauthScopes is only set for classic tokens. OauthScopes []analyzers.Permission } // GetTokenMetadata gets the username, expiration date, and x-oauth-scopes headers for a given token // by sending a GET request to the /user endpoint // Returns a response object for usage in the checkFineGrained function func GetTokenMetadata(token string, client *gh.Client) (*TokenMetadata, error) { user, resp, err := client.Users.Get(context.Background(), "") if err != nil { return nil, err } var oauthScopes []analyzers.Permission for _, scope := range resp.Header.Values("X-OAuth-Scopes") { for _, scope := range strings.Split(scope, ", ") { oauthScopes = append(oauthScopes, analyzers.Permission{Value: scope}) } } tokenType, fineGrained := checkFineGrained(token, oauthScopes) var expiration time.Time if tokenType == TokenTypeClassicPAT { // for classic tokens, github return token expiration time in header in UTC format. expiration, _ = time.Parse("2006-01-02 15:04:05 UTC", resp.Header.Get("github-authentication-token-expiration")) } else { expiration, _ = time.Parse("2006-01-02 15:04:05 -0700", resp.Header.Get("github-authentication-token-expiration")) } return &TokenMetadata{ Type: tokenType, FineGrained: fineGrained, User: user, Expiration: expiration, OauthScopes: oauthScopes, }, nil } func GetAllGistsForUser(client *gh.Client) ([]*gh.Gist, error) { opt := &gh.GistListOptions{ListOptions: gh.ListOptions{PerPage: 100}} var allGists []*gh.Gist page := 1 for { opt.Page = page gists, resp, err := client.Gists.List(context.Background(), "", opt) if err != nil { color.Red("Error getting gists.") return nil, err } allGists = append(allGists, gists...) linkHeader := resp.Header.Get("link") if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) { break } page++ } return allGists, nil } func GetAllReposForUser(client *gh.Client) ([]*gh.Repository, error) { opt := &gh.RepositoryListByAuthenticatedUserOptions{ListOptions: gh.ListOptions{PerPage: 100}} var allRepos []*gh.Repository page := 1 for { opt.Page = page repos, resp, err := client.Repositories.ListByAuthenticatedUser(context.Background(), opt) if err != nil { color.Red("Error getting repos.") return nil, err } allRepos = append(allRepos, repos...) linkHeader := resp.Header.Get("link") if linkHeader == "" || !strings.Contains(linkHeader, `rel="next"`) { break } page++ } return allRepos, nil } func PrintGitHubRepos(repos []*gh.Repository) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"Repo Name", "Owner", "Repo Link", "Private"}) for _, repo := range repos { if *repo.Private { green := color.New(color.FgGreen).SprintFunc() t.AppendRow([]interface{}{green(*repo.Name), green(*repo.Owner.Login), green(*repo.HTMLURL), green("true")}) } else { t.AppendRow([]interface{}{*repo.Name, *repo.Owner.Login, *repo.HTMLURL, *repo.Private}) } } t.Render() fmt.Print("\n\n") } func PrintGists(gists []*gh.Gist, showAll bool) { privateCount := 0 t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"Gist ID", "Gist Link", "Description", "Private"}) for _, gist := range gists { if gist == nil { continue } gistID := gist.GetID() gistLink := gist.GetHTMLURL() gistDescription := gist.GetDescription() isPublic := gist.GetPublic() if showAll && isPublic { t.AppendRow([]any{gistID, gistLink, gistDescription, "false"}) } else if !isPublic { privateCount++ green := color.New(color.FgGreen).SprintFunc() t.AppendRow([]any{green(gistID), green(gistLink), green(gistDescription), green("true")}) } } if showAll && len(gists) == 0 { color.Red("[i] No Gist(s) Found\n") } else if showAll { color.Yellow("[i] Found %v Total Gist(s) (%v private)\n", len(gists), privateCount) t.Render() } else if privateCount == 0 { color.Red("[i] No Private Gist(s) Found\n") } else { color.Green(fmt.Sprintf("[!] Found %v Private Gist(s)\n", privateCount)) t.Render() } fmt.Print("\n\n") }