[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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -5,4 +5,6 @@ type Config struct {
|
||||
LoggingEnabled bool
|
||||
LogFile string
|
||||
ShowAll bool
|
||||
// Limit API calls when enumerating permissions.
|
||||
Shallow bool
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user