Files
Amaan Ullah 87a80345c4 Issue - 3697 - GitHub analyzer panic (#4113)
* bug: add safeGetString and safeGetBool functions for nil pointer handling

* refactor: use package methods instead of custom methods
2025-05-05 10:34:56 -05:00

208 lines
5.8 KiB
Go

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")
}