mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-15 01:17:34 +00:00
290 lines
8.9 KiB
Go
290 lines
8.9 KiB
Go
//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/v66/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, err
|
|
}
|
|
}
|
|
|
|
gists, err := common.GetAllGistsForUser(client)
|
|
if err != nil {
|
|
return nil, 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
|
|
}
|