[analyze] Implement Analyzer interface for github (#3110)

* [analyze] Implement Analyzer interface for github

* Make github repo and user enumeration configurable

* Add AnalysisInfo to github detector

* Use AnalyzeAndPrintPermissions from the CLI
This commit is contained in:
Miccah
2024-07-26 14:47:03 -07:00
committed by GitHub
parent 67073617c1
commit 9d089c2188
8 changed files with 338 additions and 171 deletions
+13
View File
@@ -217,3 +217,16 @@ func (r LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
return resp, nil
}
// BindAllPermissions creates a Binding for each permission to the given
// resource.
func BindAllPermissions(r Resource, perms ...Permission) []Binding {
bindings := make([]Binding, len(perms))
for i, perm := range perms {
bindings[i] = Binding{
Resource: r,
Permission: perm,
}
}
return bindings
}
+91 -63
View File
@@ -1,20 +1,39 @@
package github
import (
"context"
"fmt"
"os"
"slices"
"strings"
"github.com/fatih/color"
gh "github.com/google/go-github/v59/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
// var SCOPE_ORDER = []string{"repo", "repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events", "--", "workflow", "--", "write:packages", "read:packages", "--", "delete:packages", "--", "admin:org", "write:org", "read:org", "manage_runners:org", "--", "admin:public_key", "write:public_key", "read:public_key", "--", "admin:repo_hook", "write:repo_hook", "read:repo_hook", "--", "admin:org_hook", "--", "gist", "--", "notifications", "--", "user", "read:user", "user:email", "user:follow", "--", "delete_repo", "--", "write:discussion", "read:discussion", "--", "admin:enterprise", "manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise", "--", "audit_log", "read:audit_log", "--", "codespace", "codespace:secrets", "--", "copilot", "manage_billing:copilot", "--", "project", "read:project", "--", "admin:gpg_key", "write:gpg_key", "read:gpg_key", "--", "admin:ssh_signing_key", "write:ssh_signing_key", "read:ssh_signing_key"}
var SCOPE_ORDER = [][]string{{"repo", "repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"}, {"workflow"}, {"write:packages", "read:packages"}, {"delete:packages"}, {"admin:org", "write:org", "read:org", "manage_runners:org"}, {"admin:public_key", "write:public_key", "read:public_key"}, {"admin:repo_hook", "write:repo_hook", "read:repo_hook"}, {"admin:org_hook"}, {"gist"}, {"notifications"}, {"user", "read:user", "user:email", "user:follow"}, {"delete_repo"}, {"write:discussion", "read:discussion"}, {"admin:enterprise", "manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise"}, {"audit_log", "read:audit_log"}, {"codespace", "codespace:secrets"}, {"copilot", "manage_billing:copilot"}, {"project", "read:project"}, {"admin:gpg_key", "write:gpg_key", "read:gpg_key"}, {"admin:ssh_signing_key", "write:ssh_signing_key", "read:ssh_signing_key"}}
var SCOPE_ORDER = [][]string{
{"repo", "repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"},
{"workflow"},
{"write:packages", "read:packages"},
{"delete:packages"},
{"admin:org", "write:org", "read:org", "manage_runners:org"},
{"admin:public_key", "write:public_key", "read:public_key"},
{"admin:repo_hook", "write:repo_hook", "read:repo_hook"},
{"admin:org_hook"},
{"gist"},
{"notifications"},
{"user", "read:user", "user:email", "user:follow"},
{"delete_repo"},
{"write:discussion", "read:discussion"},
{"admin:enterprise", "manage_runners:enterprise", "manage_billing:enterprise", "read:enterprise"},
{"audit_log", "read:audit_log"},
{"codespace", "codespace:secrets"},
{"copilot", "manage_billing:copilot"},
{"project", "read:project"},
{"admin:gpg_key", "write:gpg_key", "read:gpg_key"},
{"admin:ssh_signing_key", "write:ssh_signing_key", "read:ssh_signing_key"},
}
var SCOPE_TO_SUB_SCOPE = map[string][]string{
"repo": {"repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"},
@@ -39,21 +58,15 @@ var SCOPE_TO_SUB_SCOPE = map[string][]string{
"write:ssh_signing_key": {"read:ssh_signing_key"},
}
func checkPrivateRepoAccess(scopes map[string]bool) []string {
var currPrivateScopes []string
privateScopes := []string{"repo", "repo:status", "repo_deployment", "repo:invite", "security_events", "admin:repo_hook", "write:repo_hook", "read:repo_hook"}
for _, scope := range privateScopes {
if scopes[scope] {
currPrivateScopes = append(currPrivateScopes, scope)
}
}
return currPrivateScopes
func hasPrivateRepoAccess(scopes map[string]bool) bool {
// privateScopes := []string{"repo", "repo:status", "repo_deployment", "repo:invite", "security_events", "admin:repo_hook", "write:repo_hook", "read:repo_hook"}
return scopes["repo"]
}
func processScopes(headerScopesSlice []string) map[string]bool {
func processScopes(headerScopesSlice []analyzers.Permission) map[string]bool {
allScopes := make(map[string]bool)
for _, scope := range headerScopesSlice {
allScopes[scope] = true
allScopes[scope.Value] = true
}
for scope := range allScopes {
if subScopes, ok := SCOPE_TO_SUB_SCOPE[scope]; ok {
@@ -66,50 +79,65 @@ func processScopes(headerScopesSlice []string) map[string]bool {
}
// The `gists` scope is required to update private gists. Anyone can access a private gist with the link.
// These tokens can seem to list out the private repos, but access will depend on scopes.
// These tokens can seem to list out the private repos, but access will depend on scopes.
func analyzeClassicToken(client *gh.Client, meta *TokenMetadata) (*SecretInfo, error) {
scopes := processScopes(meta.OauthScopes)
func analyzeClassicToken(client *gh.Client, _ string, show_all bool) {
// Issue GET request to /user
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
color.Red("[x] Invalid GitHub Token.")
return
}
// If resp.Header "X-OAuth-Scopes", parse the scopes into a map[string]bool
headerScopes := resp.Header.Get("X-OAuth-Scopes")
var scopes = make(map[string]bool)
if headerScopes == "" {
color.Red("[x] Classic Token has no scopes.")
} else {
// Split string into slice of strings
headerScopesSlice := strings.Split(headerScopes, ", ")
scopes = processScopes(headerScopesSlice)
}
printClassicGHPermissions(scopes, show_all)
// Check if private repo access
privateScopes := checkPrivateRepoAccess(scopes)
if len(privateScopes) > 0 && slices.Contains(privateScopes, "repo") {
color.Green("[!] Token has scope(s) for both public and private repositories. Here's a list of all accessible repositories:")
repos, _ := getAllReposForUser(client)
printGitHubRepos(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", strings.Join(privateScopes, ", "))
} else if scopes["public_repo"] {
color.Yellow("[i] Token is scoped to only public repositories. See https://github.com/%v?tab=repositories", *user.Login)
} else {
color.Red("[x] Token does not appear scoped to any specific repositories.")
var repos []*gh.Repository
if hasPrivateRepoAccess(scopes) {
var err error
repos, err = getAllReposForUser(client)
if err != nil {
return nil, err
}
}
// Get all private gists
gists, _ := getAllGistsForUser(client)
printGists(gists, show_all)
gists, err := getAllGistsForUser(client)
if err != nil {
return nil, err
}
return &SecretInfo{
Metadata: meta,
Repos: repos,
Gists: gists,
}, nil
}
func filterPrivateRepoScopes(scopes map[string]bool) []string {
var intersection []string
privateScopes := []string{"repo", "repo:status", "repo_deployment", "repo:invite", "security_events", "admin:repo_hook", "write:repo_hook", "read:repo_hook"}
for _, privScope := range privateScopes {
if scopes[privScope] {
intersection = append(intersection, privScope)
}
}
return intersection
}
func printClassicToken(cfg *config.Config, info *SecretInfo) {
scopes := processScopes(info.Metadata.OauthScopes)
if len(scopes) == 0 {
color.Red("[x] Classic Token has no scopes")
} else {
printClassicGHPermissions(scopes, cfg.ShowAll)
}
// Check if private repo access
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:")
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", strings.Join(privateScopes, ", "))
} else if scopes["public_repo"] {
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.")
}
printGists(info.Gists, cfg.ShowAll)
}
// Question: can you access private repo with those other permissions? or can we just not list them?
@@ -125,7 +153,7 @@ func scopeFormatter(scope string, checked bool, indentation int) (string, string
}
}
func printClassicGHPermissions(scopes map[string]bool, show_all bool) {
func printClassicGHPermissions(scopes map[string]bool, showAll bool) {
scopeCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
@@ -145,7 +173,7 @@ func printClassicGHPermissions(scopes map[string]bool, show_all bool) {
var formattedScope, status string
var indentation int
if !show_all {
if !showAll {
for _, scopeSlice := range filteredScopes {
for ind, scope := range scopeSlice {
if ind == 0 {
@@ -153,16 +181,16 @@ func printClassicGHPermissions(scopes map[string]bool, show_all bool) {
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
t.AppendRow([]any{formattedScope, status})
} else {
t.AppendRow([]interface{}{scope, "----"})
t.AppendRow([]any{scope, "----"})
}
} else {
indentation = 2
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
t.AppendRow([]any{formattedScope, status})
}
}
}
@@ -179,17 +207,17 @@ func printClassicGHPermissions(scopes map[string]bool, show_all bool) {
if scopes[scope] {
scopeCount++
formattedScope, status = scopeFormatter(scope, true, indentation)
t.AppendRow([]interface{}{formattedScope, status})
t.AppendRow([]any{formattedScope, status})
} else {
formattedScope, status = scopeFormatter(scope, false, indentation)
t.AppendRow([]interface{}{formattedScope, status})
t.AppendRow([]any{formattedScope, status})
}
}
t.AppendSeparator()
}
}
if scopeCount == 0 && !show_all {
if scopeCount == 0 && !showAll {
color.Red("No Scopes Found for the GitHub Token above\n\n")
return
} else if scopeCount == 0 {
+48 -46
View File
@@ -12,6 +12,7 @@ import (
"github.com/fatih/color"
gh "github.com/google/go-github/v59/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
)
const (
@@ -119,7 +120,7 @@ var acctPermFuncMap = map[string]func(client *gh.Client, user *gh.User) (string,
}
// Define your custom formatter function
func permissionFormatter(key interface{}, val interface{}) (string, string) {
func permissionFormatter(key, val any) (string, string) {
if strVal, ok := val.(string); ok {
switch strVal {
case NO_ACCESS:
@@ -970,12 +971,8 @@ func getWebhooksPermission(client *gh.Client, repo *gh.Repository, currentAccess
// Ex: "Code scanning alerts" must be enabled to tell if we have that permission.
func analyzeRepositoryPermissions(client *gh.Client, repos []*gh.Repository, permissionType string) string {
access := ""
var err error
for _, repo := range repos {
access, err = repoPermFuncMap[permissionType](client, repo, access)
if err != nil {
log.Fatal(err)
}
access, _ = repoPermFuncMap[permissionType](client, repo, access)
if access != UNKNOWN && access != ERROR {
return access
}
@@ -1310,61 +1307,66 @@ func analyzeUserPermissions(client *gh.Client, user *gh.User, permissionType str
return access
}
func analyzeFineGrainedToken(client *gh.Client, _ string, show_all bool) {
// Get all private repos
func analyzeFineGrainedToken(client *gh.Client, meta *TokenMetadata, shallowCheck bool) (*SecretInfo, error) {
allRepos, err := getAllReposForUser(client)
if err != nil {
color.Red("Error getting repos.")
return
return nil, err
}
filteredRepos := make([]*gh.Repository, 0)
allGists, err := getAllGistsForUser(client)
if err != nil {
return nil, err
}
accessibleRepos := make([]*gh.Repository, 0)
for _, repo := range allRepos {
if analyzeRepositoryPermissions(client, []*gh.Repository{repo}, METADATA) != NO_ACCESS {
filteredRepos = append(filteredRepos, repo)
accessibleRepos = append(accessibleRepos, repo)
}
}
if len(filteredRepos) == 0 {
repoAccessMap := make(map[string]string)
userAccessMap := make(map[string]string)
if !shallowCheck {
// Check our access
for key := range repoPermFuncMap {
repoAccessMap[key] = analyzeRepositoryPermissions(client, accessibleRepos, key)
}
// Analyze Account's Permissions
for key := range acctPermFuncMap {
userAccessMap[key] = analyzeUserPermissions(client, meta.User, key)
}
}
return &SecretInfo{
Metadata: meta,
Repos: allRepos,
Gists: allGists,
AccessibleRepos: accessibleRepos,
RepoAccessMap: repoAccessMap,
UserAccessMap: userAccessMap,
}, nil
}
func printFineGrainedToken(cfg *config.Config, info *SecretInfo) {
if len(info.AccessibleRepos) == 0 {
// If no repos are accessible, then we only have read access to public repos
color.Red("[!] Repository Access: Public Repositories (read-only)\n")
} else {
// Print out the repos the token can access
color.Green(fmt.Sprintf("Found %v", len(filteredRepos)) + " Accessible Repositor(ies) \n")
printGitHubRepos(filteredRepos)
// Check our access
repoAccessMap := make(map[string]string)
for key := range repoPermFuncMap {
repoAccessMap[key] = analyzeRepositoryPermissions(client, filteredRepos, key)
}
color.Green(fmt.Sprintf("Found %v", len(info.AccessibleRepos)) + " Accessible Repositor(ies) \n")
printGitHubRepos(info.AccessibleRepos)
// Print out the access map
printFineGrainedPermissions(repoAccessMap, show_all, true)
printFineGrainedPermissions(info.RepoAccessMap, cfg.ShowAll, true)
}
// Get this token's user
user, _, err := client.Users.Get(context.Background(), "")
if err != nil {
color.Red("Error getting user.")
return
}
// Analyze Account's Permissions
userAccessMap := make(map[string]string)
for key := range acctPermFuncMap {
userAccessMap[key] = analyzeUserPermissions(client, user, key)
}
printFineGrainedPermissions(userAccessMap, show_all, false)
// Get all private gists
gists, _ := getAllGistsForUser(client)
printGists(gists, show_all)
printFineGrainedPermissions(info.UserAccessMap, cfg.ShowAll, false)
printGists(info.Gists, cfg.ShowAll)
}
func printFineGrainedPermissions(accessMap map[string]string, show_all bool, repo_permissions bool) {
func printFineGrainedPermissions(accessMap map[string]string, showAll bool, repoPermissions bool) {
permissionCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
@@ -1385,20 +1387,20 @@ func printFineGrainedPermissions(accessMap map[string]string, show_all bool, rep
} else {
permissionCount++
}
if !show_all && (value == NO_ACCESS || value == UNKNOWN || value == NOT_IMPLEMENTED) {
if !showAll && (value == NO_ACCESS || value == UNKNOWN || value == NOT_IMPLEMENTED) {
continue
} else {
k, v := permissionFormatter(key, value)
t.AppendRow([]interface{}{k, v})
t.AppendRow([]any{k, v})
}
}
var permissionType string
if repo_permissions {
if repoPermissions {
permissionType = "Repositor(ies)"
} else {
permissionType = "User Account"
}
if permissionCount == 0 && !show_all {
if permissionCount == 0 && !showAll {
color.Red("No Permissions Found for the %v above\n\n", permissionType)
return
} else if permissionCount == 0 {
+178 -58
View File
@@ -1,7 +1,6 @@
package github
import (
"context"
"fmt"
"os"
"strings"
@@ -10,9 +9,115 @@ import (
"github.com/fatih/color"
gh "github.com/google/go-github/v59/github"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_GitHub }
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
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
// Metadata *TokenMetadata
// Type string
// FineGrained bool
// User *gh.User
// Expiration time.Time
// OauthScopes []analyzers.Permission
// Repos []*gh.Repository
// Gists []*gh.Gist
// AccessibleRepos []*gh.Repository
// RepoAccessMap map[string]string
// UserAccessMap map[string]string
result := &analyzers.AnalyzerResult{
Metadata: map[string]any{
"type": info.Metadata.Type,
"fine_grained": info.Metadata.FineGrained,
"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.Type != "Organization" {
continue
}
name := *repo.Owner.Name
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 *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 *SecretInfo) []analyzers.Binding {
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, info.Metadata.OauthScopes...)...)
}
return bindings
}
func secretInfoToGistBindings(info *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 getAllGistsForUser(client *gh.Client) ([]*gh.Gist, error) {
opt := &gh.GistListOptions{ListOptions: gh.ListOptions{PerPage: 100}}
var allGists []*gh.Gist
@@ -76,14 +181,14 @@ func printGitHubRepos(repos []*gh.Repository) {
fmt.Print("\n\n")
}
func printGists(gists []*gh.Gist, show_all bool) {
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 show_all && *gist.Public {
if showAll && *gist.Public {
t.AppendRow([]interface{}{*gist.ID, *gist.HTMLURL, *gist.Description, "false"})
} else if !*gist.Public {
privateCount++
@@ -91,9 +196,9 @@ func printGists(gists []*gh.Gist, show_all bool) {
t.AppendRow([]interface{}{green(*gist.ID), green(*gist.HTMLURL), green(*gist.Description), green("true")})
}
}
if show_all && len(gists) == 0 {
if showAll && len(gists) == 0 {
color.Red("[i] No Gist(s) Found\n")
} else if show_all {
} else if showAll {
color.Yellow("[i] Found %v Total Gist(s) (%v private)\n", len(gists), privateCount)
t.Render()
} else if privateCount == 0 {
@@ -105,100 +210,115 @@ func printGists(gists []*gh.Gist, show_all bool) {
fmt.Print("\n\n")
}
func getRemainingTime(t string) string {
targetTime, err := time.Parse("2006-01-02 15:04:05 MST", t)
if err != nil {
return ""
}
// Get the current time
currentTime := time.Now()
// Calculate the duration until the target time
durationUntilTarget := targetTime.Sub(currentTime)
durationUntilTarget = durationUntilTarget.Truncate(time.Minute)
// Print the duration
return fmt.Sprintf("%v", durationUntilTarget)
type TokenMetadata struct {
Type string
FineGrained bool
User *gh.User
Expiration time.Time
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) (resp *gh.Response, err error) {
func getTokenMetadata(token string, client *gh.Client) (*TokenMetadata, error) {
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
return nil, err
}
color.Yellow("[i] Token User: %v", *user.Login)
expiration, _ := time.Parse("2006-01-02 15:04:05 MST", resp.Header.Get("github-authentication-token-expiration"))
expiry := resp.Header.Get("github-authentication-token-expiration")
timeRemaining := getRemainingTime(expiry)
if timeRemaining == "" {
color.Red("[i] Token Expiration: does not expire")
} else {
color.Yellow("[i] Token Expiration: %v (%v remaining)", expiry, timeRemaining)
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})
}
}
return resp, nil
tokenType, fineGrained := checkFineGrained(token, oauthScopes)
return &TokenMetadata{
Type: tokenType,
FineGrained: fineGrained,
User: user,
Expiration: expiration,
OauthScopes: oauthScopes,
}, nil
}
func checkFineGrained(resp *gh.Response, token string) (bool, error) {
func checkFineGrained(token string, oauthScopes []analyzers.Permission) (string, 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_") {
color.Yellow("[i] Token Type: GitHub User-to-Server Token")
return true, nil
return "GitHub User-to-Server Token", true
}
// Handle github_pat_ tokens
if strings.HasPrefix(token, "github_pat") {
color.Yellow("[i] Token Type: Fine-Grained GitHub Personal Access Token")
return true, nil
return "Fine-Grained GitHub Personal Access Token", true
}
// Handle classic PATs
if strings.HasPrefix(token, "ghp_") {
color.Yellow("[i] Token Type: Classic GitHub Personal Access Token")
return false, nil
return "Classic GitHub Personal Access Token", false
}
// Catch-all for any other types
// If resp.Header "X-OAuth-Scopes" doesn't exist, then we have fine-grained permissions
color.Yellow("[i] Token Type: GitHub Token")
if len(resp.Header.Values("X-Oauth-Scopes")) > 0 {
return false, nil
if len(oauthScopes) > 0 {
return "GitHub Token", false
}
return true, nil
return "GitHub Token", true
}
func AnalyzePermissions(cfg *config.Config, key string) {
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 map[string]string
UserAccessMap map[string]string
}
// ToDo: Add logging for GitHub when rewrite to not use GH client.
if cfg.LoggingEnabled {
color.Red("[x] Logging not supported for GitHub Token Analysis.")
return
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
if cfg == nil {
cfg = &config.Config{}
}
client := gh.NewClient(analyzers.NewAnalyzeClient(cfg)).WithAuthToken(key)
client := gh.NewClient(nil).WithAuthToken(key)
resp, err := getTokenMetadata(key, client)
md, err := getTokenMetadata(key, client)
if err != nil {
color.Red("[x] Invalid GitHub Token.")
return nil, err
}
if md.FineGrained {
return analyzeFineGrainedToken(client, md, cfg.Shallow)
}
return 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
}
// Check if the token is fine-grained or classic
if fineGrained, err := checkFineGrained(resp, key); err != nil {
color.Red("[x] Invalid GitHub Token.")
return
} else if !fineGrained {
fmt.Print("\n\n")
analyzeClassicToken(client, key, cfg.ShowAll)
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 {
fmt.Print("\n\n")
analyzeFineGrainedToken(client, key, cfg.ShowAll)
timeRemaining := time.Until(expiry)
color.Yellow("[i] Token Expiration: %v (%v remaining)", expiry, timeRemaining)
}
color.Yellow("[i] Token Type: %s\n\n", info.Metadata.Type)
if info.Metadata.FineGrained {
printFineGrainedToken(cfg, info)
return
}
printClassicToken(cfg, info)
}
+1 -1
View File
@@ -188,7 +188,7 @@ func Run(cmd string) {
panic("todo")
case githubScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("github")
github.AnalyzePermissions(cfg, *githubKey)
github.AnalyzeAndPrintPermissions(cfg, *githubKey)
case sendgridScan.FullCommand():
cfg.LogFile = analyzers.CreateLogFileName("sendgrid")
sendgrid.AnalyzePermissions(cfg, *sendgridKey)
+2
View File
@@ -5,4 +5,6 @@ type Config struct {
LoggingEnabled bool
LogFile string
ShowAll bool
// Limit API calls when enumerating permissions.
Shallow bool
}
+1
View File
@@ -85,6 +85,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
"version": fmt.Sprintf("%d", s.Version()),
},
AnalysisInfo: map[string]string{"key": token},
}
if verify {
+4 -3
View File
@@ -18,9 +18,9 @@ type Scanner struct {
}
// Ensure the Scanner satisfies the interfaces at compile time.
var _ detectors.Detector = (*v1.Scanner)(nil)
var _ detectors.Versioner = (*v1.Scanner)(nil)
var _ detectors.EndpointCustomizer = (*v1.Scanner)(nil)
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
func (s Scanner) Version() int {
return 2
@@ -66,6 +66,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
"version": fmt.Sprintf("%d", s.Version()),
},
AnalysisInfo: map[string]string{"key": token},
}
if verify {