//go:generate generate_permissions classic.yaml classic_permissions.go classic package classic import ( "fmt" "os" "strings" "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" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" ) var SCOPE_ORDER = [][]Permission{ {Repo, RepoStatus, RepoDeployment, PublicRepo, RepoInvite, SecurityEvents}, {Workflow}, {WritePackages, ReadPackages}, {DeletePackages}, {AdminOrg, WriteOrg, ReadOrg, ManageRunnersOrg}, {AdminPublicKey, WritePublicKey, ReadPublicKey}, {AdminRepoHook, WriteRepoHook, ReadRepoHook}, {AdminOrgHook}, {Gist}, {Notifications}, {User, ReadUser, UserEmail, UserFollow}, {DeleteRepo}, {WriteDiscussion, ReadDiscussion}, {AdminEnterprise, ManageRunnersEnterprise, ManageBillingEnterprise, ReadEnterprise}, {AuditLog, ReadAuditLog}, {Codespace, CodespaceSecrets}, {Copilot, ManageBillingCopilot}, {Project, ReadProject}, {AdminGpgKey, WriteGpgKey, ReadGpgKey}, {AdminSshSigningKey, WriteSshSigningKey, ReadSshSigningKey}, } var SCOPE_TO_SUB_SCOPE = map[Permission][]Permission{ Repo: {RepoStatus, RepoDeployment, PublicRepo, RepoInvite, SecurityEvents}, WritePackages: {ReadPackages}, AdminOrg: {WriteOrg, ReadOrg, ManageRunnersOrg}, WriteOrg: {ReadOrg}, AdminPublicKey: {WritePublicKey, ReadPublicKey}, WritePublicKey: {ReadPublicKey}, AdminRepoHook: {WriteRepoHook, ReadRepoHook}, WriteRepoHook: {ReadRepoHook}, User: {ReadUser, UserEmail, UserFollow}, WriteDiscussion: {ReadDiscussion}, AdminEnterprise: {ManageRunnersEnterprise, ManageBillingEnterprise, ReadEnterprise}, ManageBillingEnterprise: {ReadEnterprise}, AuditLog: {ReadAuditLog}, Codespace: {CodespaceSecrets}, Copilot: {ManageBillingCopilot}, Project: {ReadProject}, AdminGpgKey: {WriteGpgKey, ReadGpgKey}, WriteGpgKey: {ReadGpgKey}, AdminSshSigningKey: {WriteSshSigningKey, ReadSshSigningKey}, WriteSshSigningKey: {ReadSshSigningKey}, } func hasPrivateRepoAccess(scopes map[Permission]bool) bool { return scopes[Repo] } func processScopes(headerScopesSlice []analyzers.Permission) map[Permission]bool { allScopes := make(map[Permission]bool) for _, scope := range headerScopesSlice { allScopes[StringToPermission[scope.Value]] = true } for scope := range allScopes { if subScopes, ok := SCOPE_TO_SUB_SCOPE[scope]; ok { for _, subScope := range subScopes { allScopes[subScope] = true } } } return allScopes } func AnalyzeClassicToken(client *gh.Client, meta *common.TokenMetadata) (*common.SecretInfo, error) { // Convert OauthScopes to have hierarchical permissions. meta.OauthScopes = oauthScopesToPermissions(meta.OauthScopes...) scopes := processScopes(meta.OauthScopes) var repos []*gh.Repository if hasPrivateRepoAccess(scopes) { var err error repos, err = common.GetAllReposForUser(client) if err != nil { return nil, fmt.Errorf("enumerating repos for classic PAT: %w", err) } } gists, err := common.GetAllGistsForUser(client) if err != nil { return nil, fmt.Errorf("enumerating gists for classic PAT: %w", err) } return &common.SecretInfo{ Metadata: meta, Repos: repos, Gists: gists, }, nil } func filterPrivateRepoScopes(scopes map[Permission]bool) []Permission { var intersection []Permission privateScopes := []Permission{Repo, RepoStatus, RepoDeployment, RepoInvite, SecurityEvents, AdminRepoHook, WriteRepoHook, ReadRepoHook} for _, privScope := range privateScopes { if scopes[privScope] { intersection = append(intersection, privScope) } } return intersection } func PrintClassicToken(cfg *config.Config, info *common.SecretInfo) { scopes := processScopes(info.Metadata.OauthScopes) if len(scopes) == 0 { color.Red("[x] Classic Token has no scopes") } else { printClassicGHPermissions(scopes, cfg.ShowAll) } privateScopes := filterPrivateRepoScopes(scopes) if hasPrivateRepoAccess(scopes) { color.Green("[!] Token has scope(s) for both public and private repositories. Here's a list of all accessible repositories:") common.PrintGitHubRepos(info.Repos) } else if len(privateScopes) > 0 { color.Yellow("[!] Token has scope(s) useful for accessing both public and private repositories.\n However, without the `repo` scope, we cannot enumerate or access code from private repos.\n Review the permissions associated with the following scopes for more details: %v", joinPermissions(privateScopes)) } else if scopes[PublicRepo] { color.Yellow("[i] Token is scoped to only public repositories. See https://github.com/%v?tab=repositories", *info.Metadata.User.Login) } else { color.Red("[x] Token does not appear scoped to any specific repositories.") } common.PrintGists(info.Gists, cfg.ShowAll) } func joinPermissions(perms []Permission) string { var permStrings []string for _, perm := range perms { permStr, err := perm.ToString() if err != nil { panic(err) } permStrings = append(permStrings, permStr) } return strings.Join(permStrings, ", ") } func scopeFormatter(scope Permission, checked bool, indentation int) (string, string) { scopeStr, err := scope.ToString() if err != nil { panic(err) } if indentation != 0 { scopeStr = strings.Repeat(" ", indentation) + scopeStr } if checked { return color.GreenString(scopeStr), color.GreenString("true") } return scopeStr, "false" } func printClassicGHPermissions(scopes map[Permission]bool, showAll bool) { scopeCount := 0 t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"Scope", "In-Scope"}) filteredScopes := make([][]Permission, 0) for _, scopeSlice := range SCOPE_ORDER { for _, scope := range scopeSlice { if scopes[scope] { filteredScopes = append(filteredScopes, scopeSlice) break } } } var formattedScope, status string var indentation int if !showAll { for _, scopeSlice := range filteredScopes { for ind, scope := range scopeSlice { if ind == 0 { indentation = 0 if scopes[scope] { scopeCount++ formattedScope, status = scopeFormatter(scope, true, indentation) t.AppendRow([]any{formattedScope, status}) } else { scopeStr, err := scope.ToString() if err != nil { panic(err) } t.AppendRow([]any{scopeStr, "----"}) } } else { indentation = 2 if scopes[scope] { scopeCount++ formattedScope, status = scopeFormatter(scope, true, indentation) t.AppendRow([]any{formattedScope, status}) } } } t.AppendSeparator() } } else { for _, scopeSlice := range SCOPE_ORDER { for ind, scope := range scopeSlice { if ind == 0 { indentation = 0 } else { indentation = 2 } if scopes[scope] { scopeCount++ formattedScope, status = scopeFormatter(scope, true, indentation) t.AppendRow([]any{formattedScope, status}) } else { formattedScope, status = scopeFormatter(scope, false, indentation) t.AppendRow([]any{formattedScope, status}) } } t.AppendSeparator() } } if scopeCount == 0 && !showAll { color.Red("No Scopes Found for the GitHub Token above\n\n") return } else if scopeCount == 0 { color.Red("Found No Scopes for the GitHub Token above\n") } else { color.Green(fmt.Sprintf("[!] Found %v Scope(s) for the GitHub Token above\n", scopeCount)) } t.Render() fmt.Print("\n\n") } // oauthScopesToPermissions takes a list of scopes and returns a slice of // permissions for it. If the scope has implied permissions, they are included // as children of the parent scope, and both the parent and children are // returned in the slice. func oauthScopesToPermissions(scopes ...analyzers.Permission) []analyzers.Permission { allPermissions := make([]analyzers.Permission, 0, len(scopes)) for _, scope := range scopes { allPermissions = append(allPermissions, oauthScopeToPermissions(scope.Value)...) } return allPermissions } // oauthScopeToPermissions takes a given scope and returns a slice of // permissions for it. If the scope has implied permissions, they are included // as children of the parent scope, and both the parent and children are // returned in the slice. func oauthScopeToPermissions(scope string) []analyzers.Permission { parent := analyzers.Permission{Value: scope} perms := []analyzers.Permission{parent} subScopes, ok := func() ([]Permission, bool) { id, err := PermissionFromString(scope) if err != nil { return nil, false } subScopes, ok := SCOPE_TO_SUB_SCOPE[id] return subScopes, ok }() if !ok { // No sub-scopes, so the only permission is itself. return perms } // Add all the children to the list of permissions. for _, subScope := range subScopes { subScope, _ := subScope.ToString() perms = append(perms, analyzers.Permission{ Value: subScope, Parent: &parent, }) } return perms }