Add permissions lookup tables (#3125)

* OpenAI LUT

* github LUT

* cleanup

* add test

* update

* update

* update openai

* update

* Add Analyze interface to Twilio (#3128)

* Add Analyze interface to Twilio

* add readme
This commit is contained in:
Dustin Decker 2024-07-31 13:01:29 -07:00 committed by GitHub
parent 6fccac7f3d
commit 25b01019b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2544 additions and 876 deletions

View file

@ -1,4 +1,3 @@
name: Scan for secrets
on:
@ -15,13 +14,13 @@ jobs:
if: github.repository == 'trufflesecurity/trufflehog'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Dogfood
uses: ./
id: dogfood
with:
extra_args: --results=verified,unknown
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Dogfood
uses: ./
id: dogfood
with:
extra_args: --results=verified

15
pkg/analyzer/README.md Normal file
View file

@ -0,0 +1,15 @@
# Implementing Analyzers
## Defining the Permissions
Permissions are defined in lower snake case as `permission_name:access_level`.
The Permissions are initially defined as a [yaml file](analyzers/twilio/permissions.yaml).
At the top of the [analyzer implementation](analyzers/twilio/twilio.go) you specify the go generate command.
You can install the generator with `go install github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/generate_permissions`.
Then you can run `go generate ./...` to generate the Permission types for the analyzer.
The generated Permission types are to be used in the `AnalyzerResult` struct when defining the `Permissions` and in your code.

View file

@ -38,9 +38,8 @@ type (
}
Permission struct {
Value string
AccessLevel string
Parent *Permission
Value string
Parent *Permission
}
Binding struct {

View file

@ -0,0 +1,49 @@
permissions:
- 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

View file

@ -0,0 +1,296 @@
// Code generated by go generate; DO NOT EDIT.
package classic
import "errors"
type Permission int
const (
NoAccess Permission = iota
Repo Permission = iota
RepoStatus Permission = iota
RepoDeployment Permission = iota
PublicRepo Permission = iota
RepoInvite Permission = iota
SecurityEvents Permission = iota
Workflow Permission = iota
WritePackages Permission = iota
ReadPackages Permission = iota
DeletePackages Permission = iota
AdminOrg Permission = iota
WriteOrg Permission = iota
ReadOrg Permission = iota
ManageRunnersOrg Permission = iota
AdminPublicKey Permission = iota
WritePublicKey Permission = iota
ReadPublicKey Permission = iota
AdminRepoHook Permission = iota
WriteRepoHook Permission = iota
ReadRepoHook Permission = iota
AdminOrgHook Permission = iota
Gist Permission = iota
Notifications Permission = iota
User Permission = iota
ReadUser Permission = iota
UserEmail Permission = iota
UserFollow Permission = iota
DeleteRepo Permission = iota
WriteDiscussion Permission = iota
ReadDiscussion Permission = iota
AdminEnterprise Permission = iota
ManageRunnersEnterprise Permission = iota
ManageBillingEnterprise Permission = iota
ReadEnterprise Permission = iota
AuditLog Permission = iota
ReadAuditLog Permission = iota
Codespace Permission = iota
CodespaceSecrets Permission = iota
Copilot Permission = iota
ManageBillingCopilot Permission = iota
Project Permission = iota
ReadProject Permission = iota
AdminGpgKey Permission = iota
WriteGpgKey Permission = iota
ReadGpgKey Permission = iota
AdminSshSigningKey Permission = iota
WriteSshSigningKey Permission = iota
ReadSshSigningKey Permission = iota
)
var (
permissionStrings = map[Permission]string{
Repo: "repo",
RepoStatus: "repo:status",
RepoDeployment: "repo_deployment",
PublicRepo: "public_repo",
RepoInvite: "repo:invite",
SecurityEvents: "security_events",
Workflow: "workflow",
WritePackages: "write:packages",
ReadPackages: "read:packages",
DeletePackages: "delete:packages",
AdminOrg: "admin:org",
WriteOrg: "write:org",
ReadOrg: "read:org",
ManageRunnersOrg: "manage_runners:org",
AdminPublicKey: "admin:public_key",
WritePublicKey: "write:public_key",
ReadPublicKey: "read:public_key",
AdminRepoHook: "admin:repo_hook",
WriteRepoHook: "write:repo_hook",
ReadRepoHook: "read:repo_hook",
AdminOrgHook: "admin:org_hook",
Gist: "gist",
Notifications: "notifications",
User: "user",
ReadUser: "read:user",
UserEmail: "user:email",
UserFollow: "user:follow",
DeleteRepo: "delete_repo",
WriteDiscussion: "write:discussion",
ReadDiscussion: "read:discussion",
AdminEnterprise: "admin:enterprise",
ManageRunnersEnterprise: "manage_runners:enterprise",
ManageBillingEnterprise: "manage_billing:enterprise",
ReadEnterprise: "read:enterprise",
AuditLog: "audit_log",
ReadAuditLog: "read:audit_log",
Codespace: "codespace",
CodespaceSecrets: "codespace:secrets",
Copilot: "copilot",
ManageBillingCopilot: "manage_billing:copilot",
Project: "project",
ReadProject: "read:project",
AdminGpgKey: "admin:gpg_key",
WriteGpgKey: "write:gpg_key",
ReadGpgKey: "read:gpg_key",
AdminSshSigningKey: "admin:ssh_signing_key",
WriteSshSigningKey: "write:ssh_signing_key",
ReadSshSigningKey: "read:ssh_signing_key",
}
stringToPermission = map[string]Permission{
"repo": Repo,
"repo:status": RepoStatus,
"repo_deployment": RepoDeployment,
"public_repo": PublicRepo,
"repo:invite": RepoInvite,
"security_events": SecurityEvents,
"workflow": Workflow,
"write:packages": WritePackages,
"read:packages": ReadPackages,
"delete:packages": DeletePackages,
"admin:org": AdminOrg,
"write:org": WriteOrg,
"read:org": ReadOrg,
"manage_runners:org": ManageRunnersOrg,
"admin:public_key": AdminPublicKey,
"write:public_key": WritePublicKey,
"read:public_key": ReadPublicKey,
"admin:repo_hook": AdminRepoHook,
"write:repo_hook": WriteRepoHook,
"read:repo_hook": ReadRepoHook,
"admin:org_hook": AdminOrgHook,
"gist": Gist,
"notifications": Notifications,
"user": User,
"read:user": ReadUser,
"user:email": UserEmail,
"user:follow": UserFollow,
"delete_repo": DeleteRepo,
"write:discussion": WriteDiscussion,
"read:discussion": ReadDiscussion,
"admin:enterprise": AdminEnterprise,
"manage_runners:enterprise": ManageRunnersEnterprise,
"manage_billing:enterprise": ManageBillingEnterprise,
"read:enterprise": ReadEnterprise,
"audit_log": AuditLog,
"read:audit_log": ReadAuditLog,
"codespace": Codespace,
"codespace:secrets": CodespaceSecrets,
"copilot": Copilot,
"manage_billing:copilot": ManageBillingCopilot,
"project": Project,
"read:project": ReadProject,
"admin:gpg_key": AdminGpgKey,
"write:gpg_key": WriteGpgKey,
"read:gpg_key": ReadGpgKey,
"admin:ssh_signing_key": AdminSshSigningKey,
"write:ssh_signing_key": WriteSshSigningKey,
"read:ssh_signing_key": ReadSshSigningKey,
}
permissionIDs = map[Permission]int{
Repo: 0,
RepoStatus: 1,
RepoDeployment: 2,
PublicRepo: 3,
RepoInvite: 4,
SecurityEvents: 5,
Workflow: 6,
WritePackages: 7,
ReadPackages: 8,
DeletePackages: 9,
AdminOrg: 10,
WriteOrg: 11,
ReadOrg: 12,
ManageRunnersOrg: 13,
AdminPublicKey: 14,
WritePublicKey: 15,
ReadPublicKey: 16,
AdminRepoHook: 17,
WriteRepoHook: 18,
ReadRepoHook: 19,
AdminOrgHook: 20,
Gist: 21,
Notifications: 22,
User: 23,
ReadUser: 24,
UserEmail: 25,
UserFollow: 26,
DeleteRepo: 27,
WriteDiscussion: 28,
ReadDiscussion: 29,
AdminEnterprise: 30,
ManageRunnersEnterprise: 31,
ManageBillingEnterprise: 32,
ReadEnterprise: 33,
AuditLog: 34,
ReadAuditLog: 35,
Codespace: 36,
CodespaceSecrets: 37,
Copilot: 38,
ManageBillingCopilot: 39,
Project: 40,
ReadProject: 41,
AdminGpgKey: 42,
WriteGpgKey: 43,
ReadGpgKey: 44,
AdminSshSigningKey: 45,
WriteSshSigningKey: 46,
ReadSshSigningKey: 47,
}
idToPermission = map[int]Permission{
0: Repo,
1: RepoStatus,
2: RepoDeployment,
3: PublicRepo,
4: RepoInvite,
5: SecurityEvents,
6: Workflow,
7: WritePackages,
8: ReadPackages,
9: DeletePackages,
10: AdminOrg,
11: WriteOrg,
12: ReadOrg,
13: ManageRunnersOrg,
14: AdminPublicKey,
15: WritePublicKey,
16: ReadPublicKey,
17: AdminRepoHook,
18: WriteRepoHook,
19: ReadRepoHook,
20: AdminOrgHook,
21: Gist,
22: Notifications,
23: User,
24: ReadUser,
25: UserEmail,
26: UserFollow,
27: DeleteRepo,
28: WriteDiscussion,
29: ReadDiscussion,
30: AdminEnterprise,
31: ManageRunnersEnterprise,
32: ManageBillingEnterprise,
33: ReadEnterprise,
34: AuditLog,
35: ReadAuditLog,
36: Codespace,
37: CodespaceSecrets,
38: Copilot,
39: ManageBillingCopilot,
40: Project,
41: ReadProject,
42: AdminGpgKey,
43: WriteGpgKey,
44: ReadGpgKey,
45: AdminSshSigningKey,
46: WriteSshSigningKey,
47: ReadSshSigningKey,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := permissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := permissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := stringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := idToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}

View file

@ -0,0 +1,246 @@
//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/v63/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) {
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")
}

View file

@ -1,230 +0,0 @@
package github
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
gh "github.com/google/go-github/v63/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_TO_SUB_SCOPE = map[string][]string{
"repo": {"repo:status", "repo_deployment", "public_repo", "repo:invite", "security_events"},
"write:pakages": {"read:packages"},
"admin:org": {"write:org", "read:org", "manage_runners:org"},
"write:org": {"read:org"},
"admin:public_key": {"write:public_key", "read:public_key"},
"write:public_key": {"read:public_key"},
"admin:repo_hook": {"write:repo_hook", "read:repo_hook"},
"write:repo_hook": {"read:repo_hook"},
"user": {"read:user", "user:email", "user:follow"},
"write:discussion": {"read:discussion"},
"admin:enterprise": {"manage_runners:enterprise", "manage_billing:enterprise", "read: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"},
"write:gpg_key": {"read:gpg_key"},
"admin:ssh_signing_key": {"write:ssh_signing_key", "read:ssh_signing_key"},
"write:ssh_signing_key": {"read:ssh_signing_key"},
}
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 []analyzers.Permission) map[string]bool {
allScopes := make(map[string]bool)
for _, scope := range headerScopesSlice {
allScopes[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
}
// 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.
func analyzeClassicToken(client *gh.Client, meta *TokenMetadata) (*SecretInfo, error) {
scopes := processScopes(meta.OauthScopes)
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, 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?
func scopeFormatter(scope string, checked bool, indentation int) (string, string) {
if indentation != 0 {
scope = strings.Repeat(" ", indentation) + scope
}
if checked {
return color.GreenString(scope), color.GreenString("true")
} else {
return scope, "false"
}
}
func printClassicGHPermissions(scopes map[string]bool, showAll bool) {
scopeCount := 0
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "In-Scope" /* Add more column headers if needed */})
filteredScopes := make([][]string, 0)
for _, scopeSlice := range SCOPE_ORDER {
for _, scope := range scopeSlice {
if scopes[scope] {
filteredScopes = append(filteredScopes, scopeSlice)
break
}
}
}
// For ease of reading, divide the scopes into sections, just like the GH UI
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 {
t.AppendRow([]any{scope, "----"})
}
} 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")
}

View file

@ -0,0 +1,182 @@
package common
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
gh "github.com/google/go-github/v63/github"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
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_") {
return "GitHub User-to-Server Token", true
}
// Handle github_pat_ tokens
if strings.HasPrefix(token, "github_pat") {
return "Fine-Grained GitHub Personal Access Token", true
}
// Handle classic PATs
if strings.HasPrefix(token, "ghp_") {
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
if len(oauthScopes) > 0 {
return "GitHub Token", false
}
return "GitHub Token", 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 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) (*TokenMetadata, error) {
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
return nil, err
}
expiration, _ := time.Parse("2006-01-02 15:04:05 MST", resp.Header.Get("github-authentication-token-expiration"))
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)
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 showAll && *gist.Public {
t.AppendRow([]interface{}{*gist.ID, *gist.HTMLURL, *gist.Description, "false"})
} else if !*gist.Public {
privateCount++
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]interface{}{green(*gist.ID), green(*gist.HTMLURL), green(*gist.Description), 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")
}

View file

@ -0,0 +1,80 @@
# Please generate a yaml list of all of the strings permission_name:access_level for all of the permissions and access levels that can be emitted from the test functions. The strings should be lower snake case with a colon joining the permission name and access level. The only access levels I want are "read" and "write"
permissions:
- actions:read
- actions:write
- administration:read
- administration:write
- code_scanning_alerts:read
- code_scanning_alerts:write
- codespaces:read
- codespaces:write
- codespaces_lifecycle:read
- codespaces_lifecycle:write
- codespaces_metadata:read
- codespaces_metadata:write
- codespaces_secrets:read
- codespaces_secrets:write
- commit_statuses:read
- commit_statuses:write
- contents:read
- contents:write
- custom_properties:read
- custom_properties:write
- dependabot_alerts:read
- dependabot_alerts:write
- dependabot_secrets:read
- dependabot_secrets:write
- deployments:read
- deployments:write
- environments:read
- environments:write
- issues:read
- issues:write
- merge_queues:read
- merge_queues:write
- metadata:read
- metadata:write
- pages:read
- pages:write
- pull_requests:read
- pull_requests:write
- repo_security:read
- repo_security:write
- secret_scanning:read
- secret_scanning:write
- secrets:read
- secrets:write
- variables:read
- variables:write
- webhooks:read
- webhooks:write
- workflows:read
- workflows:write
- block_user:read
- block_user:write
- codespace_user_secrets:read
- codespace_user_secrets:write
- email:read
- email:write
- followers:read
- followers:write
- gpg_keys:read
- gpg_keys:write
- gists:read
- gists:write
- git_keys:read
- git_keys:write
- limits:read
- limits:write
- plan:read
- plan:write
- private_invites:read
- private_invites:write
- profile:read
- profile:write
- signing_keys:read
- signing_keys:write
- starring:read
- starring:write
- watching:read
- watching:write

View file

@ -0,0 +1,446 @@
// Code generated by go generate; DO NOT EDIT.
package finegrained
import "errors"
type Permission int
const (
NoAccess Permission = iota
ActionsRead Permission = iota
ActionsWrite Permission = iota
AdministrationRead Permission = iota
AdministrationWrite Permission = iota
CodeScanningAlertsRead Permission = iota
CodeScanningAlertsWrite Permission = iota
CodespacesRead Permission = iota
CodespacesWrite Permission = iota
CodespacesLifecycleRead Permission = iota
CodespacesLifecycleWrite Permission = iota
CodespacesMetadataRead Permission = iota
CodespacesMetadataWrite Permission = iota
CodespacesSecretsRead Permission = iota
CodespacesSecretsWrite Permission = iota
CommitStatusesRead Permission = iota
CommitStatusesWrite Permission = iota
ContentsRead Permission = iota
ContentsWrite Permission = iota
CustomPropertiesRead Permission = iota
CustomPropertiesWrite Permission = iota
DependabotAlertsRead Permission = iota
DependabotAlertsWrite Permission = iota
DependabotSecretsRead Permission = iota
DependabotSecretsWrite Permission = iota
DeploymentsRead Permission = iota
DeploymentsWrite Permission = iota
EnvironmentsRead Permission = iota
EnvironmentsWrite Permission = iota
IssuesRead Permission = iota
IssuesWrite Permission = iota
MergeQueuesRead Permission = iota
MergeQueuesWrite Permission = iota
MetadataRead Permission = iota
MetadataWrite Permission = iota
PagesRead Permission = iota
PagesWrite Permission = iota
PullRequestsRead Permission = iota
PullRequestsWrite Permission = iota
RepoSecurityRead Permission = iota
RepoSecurityWrite Permission = iota
SecretScanningRead Permission = iota
SecretScanningWrite Permission = iota
SecretsRead Permission = iota
SecretsWrite Permission = iota
VariablesRead Permission = iota
VariablesWrite Permission = iota
WebhooksRead Permission = iota
WebhooksWrite Permission = iota
WorkflowsRead Permission = iota
WorkflowsWrite Permission = iota
BlockUserRead Permission = iota
BlockUserWrite Permission = iota
CodespaceUserSecretsRead Permission = iota
CodespaceUserSecretsWrite Permission = iota
EmailRead Permission = iota
EmailWrite Permission = iota
FollowersRead Permission = iota
FollowersWrite Permission = iota
GpgKeysRead Permission = iota
GpgKeysWrite Permission = iota
GistsRead Permission = iota
GistsWrite Permission = iota
GitKeysRead Permission = iota
GitKeysWrite Permission = iota
LimitsRead Permission = iota
LimitsWrite Permission = iota
PlanRead Permission = iota
PlanWrite Permission = iota
PrivateInvitesRead Permission = iota
PrivateInvitesWrite Permission = iota
ProfileRead Permission = iota
ProfileWrite Permission = iota
SigningKeysRead Permission = iota
SigningKeysWrite Permission = iota
StarringRead Permission = iota
StarringWrite Permission = iota
WatchingRead Permission = iota
WatchingWrite Permission = iota
)
var (
permissionStrings = map[Permission]string{
ActionsRead: "actions:read",
ActionsWrite: "actions:write",
AdministrationRead: "administration:read",
AdministrationWrite: "administration:write",
CodeScanningAlertsRead: "code_scanning_alerts:read",
CodeScanningAlertsWrite: "code_scanning_alerts:write",
CodespacesRead: "codespaces:read",
CodespacesWrite: "codespaces:write",
CodespacesLifecycleRead: "codespaces_lifecycle:read",
CodespacesLifecycleWrite: "codespaces_lifecycle:write",
CodespacesMetadataRead: "codespaces_metadata:read",
CodespacesMetadataWrite: "codespaces_metadata:write",
CodespacesSecretsRead: "codespaces_secrets:read",
CodespacesSecretsWrite: "codespaces_secrets:write",
CommitStatusesRead: "commit_statuses:read",
CommitStatusesWrite: "commit_statuses:write",
ContentsRead: "contents:read",
ContentsWrite: "contents:write",
CustomPropertiesRead: "custom_properties:read",
CustomPropertiesWrite: "custom_properties:write",
DependabotAlertsRead: "dependabot_alerts:read",
DependabotAlertsWrite: "dependabot_alerts:write",
DependabotSecretsRead: "dependabot_secrets:read",
DependabotSecretsWrite: "dependabot_secrets:write",
DeploymentsRead: "deployments:read",
DeploymentsWrite: "deployments:write",
EnvironmentsRead: "environments:read",
EnvironmentsWrite: "environments:write",
IssuesRead: "issues:read",
IssuesWrite: "issues:write",
MergeQueuesRead: "merge_queues:read",
MergeQueuesWrite: "merge_queues:write",
MetadataRead: "metadata:read",
MetadataWrite: "metadata:write",
PagesRead: "pages:read",
PagesWrite: "pages:write",
PullRequestsRead: "pull_requests:read",
PullRequestsWrite: "pull_requests:write",
RepoSecurityRead: "repo_security:read",
RepoSecurityWrite: "repo_security:write",
SecretScanningRead: "secret_scanning:read",
SecretScanningWrite: "secret_scanning:write",
SecretsRead: "secrets:read",
SecretsWrite: "secrets:write",
VariablesRead: "variables:read",
VariablesWrite: "variables:write",
WebhooksRead: "webhooks:read",
WebhooksWrite: "webhooks:write",
WorkflowsRead: "workflows:read",
WorkflowsWrite: "workflows:write",
BlockUserRead: "block_user:read",
BlockUserWrite: "block_user:write",
CodespaceUserSecretsRead: "codespace_user_secrets:read",
CodespaceUserSecretsWrite: "codespace_user_secrets:write",
EmailRead: "email:read",
EmailWrite: "email:write",
FollowersRead: "followers:read",
FollowersWrite: "followers:write",
GpgKeysRead: "gpg_keys:read",
GpgKeysWrite: "gpg_keys:write",
GistsRead: "gists:read",
GistsWrite: "gists:write",
GitKeysRead: "git_keys:read",
GitKeysWrite: "git_keys:write",
LimitsRead: "limits:read",
LimitsWrite: "limits:write",
PlanRead: "plan:read",
PlanWrite: "plan:write",
PrivateInvitesRead: "private_invites:read",
PrivateInvitesWrite: "private_invites:write",
ProfileRead: "profile:read",
ProfileWrite: "profile:write",
SigningKeysRead: "signing_keys:read",
SigningKeysWrite: "signing_keys:write",
StarringRead: "starring:read",
StarringWrite: "starring:write",
WatchingRead: "watching:read",
WatchingWrite: "watching:write",
}
stringToPermission = map[string]Permission{
"actions:read": ActionsRead,
"actions:write": ActionsWrite,
"administration:read": AdministrationRead,
"administration:write": AdministrationWrite,
"code_scanning_alerts:read": CodeScanningAlertsRead,
"code_scanning_alerts:write": CodeScanningAlertsWrite,
"codespaces:read": CodespacesRead,
"codespaces:write": CodespacesWrite,
"codespaces_lifecycle:read": CodespacesLifecycleRead,
"codespaces_lifecycle:write": CodespacesLifecycleWrite,
"codespaces_metadata:read": CodespacesMetadataRead,
"codespaces_metadata:write": CodespacesMetadataWrite,
"codespaces_secrets:read": CodespacesSecretsRead,
"codespaces_secrets:write": CodespacesSecretsWrite,
"commit_statuses:read": CommitStatusesRead,
"commit_statuses:write": CommitStatusesWrite,
"contents:read": ContentsRead,
"contents:write": ContentsWrite,
"custom_properties:read": CustomPropertiesRead,
"custom_properties:write": CustomPropertiesWrite,
"dependabot_alerts:read": DependabotAlertsRead,
"dependabot_alerts:write": DependabotAlertsWrite,
"dependabot_secrets:read": DependabotSecretsRead,
"dependabot_secrets:write": DependabotSecretsWrite,
"deployments:read": DeploymentsRead,
"deployments:write": DeploymentsWrite,
"environments:read": EnvironmentsRead,
"environments:write": EnvironmentsWrite,
"issues:read": IssuesRead,
"issues:write": IssuesWrite,
"merge_queues:read": MergeQueuesRead,
"merge_queues:write": MergeQueuesWrite,
"metadata:read": MetadataRead,
"metadata:write": MetadataWrite,
"pages:read": PagesRead,
"pages:write": PagesWrite,
"pull_requests:read": PullRequestsRead,
"pull_requests:write": PullRequestsWrite,
"repo_security:read": RepoSecurityRead,
"repo_security:write": RepoSecurityWrite,
"secret_scanning:read": SecretScanningRead,
"secret_scanning:write": SecretScanningWrite,
"secrets:read": SecretsRead,
"secrets:write": SecretsWrite,
"variables:read": VariablesRead,
"variables:write": VariablesWrite,
"webhooks:read": WebhooksRead,
"webhooks:write": WebhooksWrite,
"workflows:read": WorkflowsRead,
"workflows:write": WorkflowsWrite,
"block_user:read": BlockUserRead,
"block_user:write": BlockUserWrite,
"codespace_user_secrets:read": CodespaceUserSecretsRead,
"codespace_user_secrets:write": CodespaceUserSecretsWrite,
"email:read": EmailRead,
"email:write": EmailWrite,
"followers:read": FollowersRead,
"followers:write": FollowersWrite,
"gpg_keys:read": GpgKeysRead,
"gpg_keys:write": GpgKeysWrite,
"gists:read": GistsRead,
"gists:write": GistsWrite,
"git_keys:read": GitKeysRead,
"git_keys:write": GitKeysWrite,
"limits:read": LimitsRead,
"limits:write": LimitsWrite,
"plan:read": PlanRead,
"plan:write": PlanWrite,
"private_invites:read": PrivateInvitesRead,
"private_invites:write": PrivateInvitesWrite,
"profile:read": ProfileRead,
"profile:write": ProfileWrite,
"signing_keys:read": SigningKeysRead,
"signing_keys:write": SigningKeysWrite,
"starring:read": StarringRead,
"starring:write": StarringWrite,
"watching:read": WatchingRead,
"watching:write": WatchingWrite,
}
permissionIDs = map[Permission]int{
ActionsRead: 0,
ActionsWrite: 1,
AdministrationRead: 2,
AdministrationWrite: 3,
CodeScanningAlertsRead: 4,
CodeScanningAlertsWrite: 5,
CodespacesRead: 6,
CodespacesWrite: 7,
CodespacesLifecycleRead: 8,
CodespacesLifecycleWrite: 9,
CodespacesMetadataRead: 10,
CodespacesMetadataWrite: 11,
CodespacesSecretsRead: 12,
CodespacesSecretsWrite: 13,
CommitStatusesRead: 14,
CommitStatusesWrite: 15,
ContentsRead: 16,
ContentsWrite: 17,
CustomPropertiesRead: 18,
CustomPropertiesWrite: 19,
DependabotAlertsRead: 20,
DependabotAlertsWrite: 21,
DependabotSecretsRead: 22,
DependabotSecretsWrite: 23,
DeploymentsRead: 24,
DeploymentsWrite: 25,
EnvironmentsRead: 26,
EnvironmentsWrite: 27,
IssuesRead: 28,
IssuesWrite: 29,
MergeQueuesRead: 30,
MergeQueuesWrite: 31,
MetadataRead: 32,
MetadataWrite: 33,
PagesRead: 34,
PagesWrite: 35,
PullRequestsRead: 36,
PullRequestsWrite: 37,
RepoSecurityRead: 38,
RepoSecurityWrite: 39,
SecretScanningRead: 40,
SecretScanningWrite: 41,
SecretsRead: 42,
SecretsWrite: 43,
VariablesRead: 44,
VariablesWrite: 45,
WebhooksRead: 46,
WebhooksWrite: 47,
WorkflowsRead: 48,
WorkflowsWrite: 49,
BlockUserRead: 50,
BlockUserWrite: 51,
CodespaceUserSecretsRead: 52,
CodespaceUserSecretsWrite: 53,
EmailRead: 54,
EmailWrite: 55,
FollowersRead: 56,
FollowersWrite: 57,
GpgKeysRead: 58,
GpgKeysWrite: 59,
GistsRead: 60,
GistsWrite: 61,
GitKeysRead: 62,
GitKeysWrite: 63,
LimitsRead: 64,
LimitsWrite: 65,
PlanRead: 66,
PlanWrite: 67,
PrivateInvitesRead: 68,
PrivateInvitesWrite: 69,
ProfileRead: 70,
ProfileWrite: 71,
SigningKeysRead: 72,
SigningKeysWrite: 73,
StarringRead: 74,
StarringWrite: 75,
WatchingRead: 76,
WatchingWrite: 77,
}
idToPermission = map[int]Permission{
0: ActionsRead,
1: ActionsWrite,
2: AdministrationRead,
3: AdministrationWrite,
4: CodeScanningAlertsRead,
5: CodeScanningAlertsWrite,
6: CodespacesRead,
7: CodespacesWrite,
8: CodespacesLifecycleRead,
9: CodespacesLifecycleWrite,
10: CodespacesMetadataRead,
11: CodespacesMetadataWrite,
12: CodespacesSecretsRead,
13: CodespacesSecretsWrite,
14: CommitStatusesRead,
15: CommitStatusesWrite,
16: ContentsRead,
17: ContentsWrite,
18: CustomPropertiesRead,
19: CustomPropertiesWrite,
20: DependabotAlertsRead,
21: DependabotAlertsWrite,
22: DependabotSecretsRead,
23: DependabotSecretsWrite,
24: DeploymentsRead,
25: DeploymentsWrite,
26: EnvironmentsRead,
27: EnvironmentsWrite,
28: IssuesRead,
29: IssuesWrite,
30: MergeQueuesRead,
31: MergeQueuesWrite,
32: MetadataRead,
33: MetadataWrite,
34: PagesRead,
35: PagesWrite,
36: PullRequestsRead,
37: PullRequestsWrite,
38: RepoSecurityRead,
39: RepoSecurityWrite,
40: SecretScanningRead,
41: SecretScanningWrite,
42: SecretsRead,
43: SecretsWrite,
44: VariablesRead,
45: VariablesWrite,
46: WebhooksRead,
47: WebhooksWrite,
48: WorkflowsRead,
49: WorkflowsWrite,
50: BlockUserRead,
51: BlockUserWrite,
52: CodespaceUserSecretsRead,
53: CodespaceUserSecretsWrite,
54: EmailRead,
55: EmailWrite,
56: FollowersRead,
57: FollowersWrite,
58: GpgKeysRead,
59: GpgKeysWrite,
60: GistsRead,
61: GistsWrite,
62: GitKeysRead,
63: GitKeysWrite,
64: LimitsRead,
65: LimitsWrite,
66: PlanRead,
67: PlanWrite,
68: PrivateInvitesRead,
69: PrivateInvitesWrite,
70: ProfileRead,
71: ProfileWrite,
72: SigningKeysRead,
73: SigningKeysWrite,
74: StarringRead,
75: StarringWrite,
76: WatchingRead,
77: WatchingWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := permissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := permissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := stringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := idToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}

View file

@ -2,15 +2,16 @@ package github
import (
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
gh "github.com/google/go-github/v63/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/classic"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github/finegrained"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
@ -32,7 +33,7 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
func secretInfoToAnalyzerResult(info *common.SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
@ -74,7 +75,7 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
return result
}
func secretInfoToUserBindings(info *SecretInfo) []analyzers.Binding {
func secretInfoToUserBindings(info *common.SecretInfo) []analyzers.Binding {
return analyzers.BindAllPermissions(*userToResource(info.Metadata.User), info.Metadata.OauthScopes...)
}
@ -87,7 +88,7 @@ func userToResource(user *gh.User) *analyzers.Resource {
}
}
func secretInfoToRepoBindings(info *SecretInfo) []analyzers.Binding {
func secretInfoToRepoBindings(info *common.SecretInfo) []analyzers.Binding {
repos := info.Repos
if len(info.AccessibleRepos) > 0 {
repos = info.AccessibleRepos
@ -105,7 +106,7 @@ func secretInfoToRepoBindings(info *SecretInfo) []analyzers.Binding {
return bindings
}
func secretInfoToGistBindings(info *SecretInfo) []analyzers.Binding {
func secretInfoToGistBindings(info *common.SecretInfo) []analyzers.Binding {
var bindings []analyzers.Binding
for _, gist := range info.Gists {
resource := analyzers.Resource{
@ -119,186 +120,21 @@ func secretInfoToGistBindings(info *SecretInfo) []analyzers.Binding {
return bindings
}
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 showAll && *gist.Public {
t.AppendRow([]interface{}{*gist.ID, *gist.HTMLURL, *gist.Description, "false"})
} else if !*gist.Public {
privateCount++
green := color.New(color.FgGreen).SprintFunc()
t.AppendRow([]interface{}{green(*gist.ID), green(*gist.HTMLURL), green(*gist.Description), 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")
}
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) (*TokenMetadata, error) {
user, resp, err := client.Users.Get(context.Background(), "")
if err != nil {
return nil, err
}
expiration, _ := time.Parse("2006-01-02 15:04:05 MST", resp.Header.Get("github-authentication-token-expiration"))
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)
return &TokenMetadata{
Type: tokenType,
FineGrained: fineGrained,
User: user,
Expiration: expiration,
OauthScopes: oauthScopes,
}, nil
}
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_") {
return "GitHub User-to-Server Token", true
}
// Handle github_pat_ tokens
if strings.HasPrefix(token, "github_pat") {
return "Fine-Grained GitHub Personal Access Token", true
}
// Handle classic PATs
if strings.HasPrefix(token, "ghp_") {
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
if len(oauthScopes) > 0 {
return "GitHub Token", false
}
return "GitHub Token", true
}
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
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
func AnalyzePermissions(cfg *config.Config, key string) (*common.SecretInfo, error) {
if cfg == nil {
cfg = &config.Config{}
}
client := gh.NewClient(analyzers.NewAnalyzeClient(cfg)).WithAuthToken(key)
md, err := getTokenMetadata(key, client)
md, err := common.GetTokenMetadata(key, client)
if err != nil {
return nil, err
}
if md.FineGrained {
return analyzeFineGrainedToken(client, md, cfg.Shallow)
return finegrained.AnalyzeFineGrainedToken(client, md, cfg.Shallow)
}
return analyzeClassicToken(client, md)
return classic.AnalyzeClassicToken(client, md)
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
@ -318,8 +154,8 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
color.Yellow("[i] Token Type: %s\n\n", info.Metadata.Type)
if info.Metadata.FineGrained {
printFineGrainedToken(cfg, info)
finegrained.PrintFineGrainedToken(cfg, info)
return
}
printClassicToken(cfg, info)
classic.PrintClassicToken(cfg, info)
}

View file

@ -0,0 +1,121 @@
package github
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "v2 ghp",
key: testSecrets.MustGetField("GITHUB_VERIFIED_GHP"),
want: `{
"AnalyzerType": 0,
"Bindings": [
{
"Resource": {
"Name": "truffle-sandbox",
"FullyQualifiedName": "github.com/truffle-sandbox",
"Type": "user",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "notifications",
"AccessLevel": "",
"Parent": null
}
},
{
"Resource": {
"Name": "public gist",
"FullyQualifiedName": "gist.github.com/truffle-sandbox/fecf272c606ddbc5f8486f9c44821312",
"Type": "gist",
"Metadata": null,
"Parent": {
"Name": "truffle-sandbox",
"FullyQualifiedName": "github.com/truffle-sandbox",
"Type": "user",
"Metadata": null,
"Parent": null
}
},
"Permission": {
"Value": "notifications",
"AccessLevel": "",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"expiration": "0001-01-01T00:00:00Z",
"fine_grained": false,
"type": "Classic GitHub Personal Access Token"
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}

View file

@ -1,3 +1,5 @@
//go:generate generate_permissions permissions.yaml permissions.go openai
package openai
import (
@ -39,7 +41,6 @@ func secretInfoToAnalyzerResult(info *AnalyzerJSON) *analyzers.AnalyzerResult {
Metadata: map[string]any{
"user": info.me.Name,
"email": info.me.Email,
"phone": info.me.Phone,
"mfa": strconv.FormatBool(info.me.MfaEnabled),
"is_admin": strconv.FormatBool(info.isAdmin),
"is_restricted": strconv.FormatBool(info.isRestricted),
@ -259,14 +260,20 @@ func printUserData(meJSON MeJSON) {
fmt.Print("\n\n")
}
func stringifyPermissionStatus(tests []analyzers.HttpStatusTest) analyzers.PermissionType {
func stringifyPermissionStatus(readTests []analyzers.HttpStatusTest, writeTests []analyzers.HttpStatusTest) analyzers.PermissionType {
readStatus := false
writeStatus := false
errors := false
for _, test := range tests {
for _, test := range readTests {
if test.Type == analyzers.READ {
readStatus = test.Status.Value
} else if test.Type == analyzers.WRITE {
}
if test.Status.IsError {
errors = true
}
}
for _, test := range writeTests {
if test.Type == analyzers.WRITE {
writeStatus = test.Status.Value
}
if test.Status.IsError {
@ -291,9 +298,9 @@ func getPermissions() []permissionData {
var perms []permissionData
for _, scope := range SCOPES {
status := stringifyPermissionStatus(scope.Tests)
status := stringifyPermissionStatus(scope.ReadTests, scope.WriteTests)
perms = append(perms, permissionData{
name: scope.Name,
name: scope.Endpoints[0], // Using the first endpoint as the name for simplicity
endpoints: scope.Endpoints,
status: status,
})

View file

@ -0,0 +1,125 @@
package openai
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid OpenAI key",
key: testSecrets.MustGetField("OPENAI_VERIFIED"),
want: `{
"AnalyzerType": 0,
"Bindings": [
{
"Resource": {
"Name": "Truffle Security Co",
"FullyQualifiedName": "org-n56tuYdSewh06PEGJZC0xWHf",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "truffle-security-co"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"AccessLevel": "",
"Parent": null
}
},
{
"Resource": {
"Name": "Personal",
"FullyQualifiedName": "org-S2T2qOGM1KofMLUxb9rt7eV0",
"Type": "organization",
"Metadata": {
"description": "Personal org for dustin@trufflesec.com",
"user": "user-ohfap0ky8lkatw97iskuhghv"
},
"Parent": null
},
"Permission": {
"Value": "full_access",
"AccessLevel": "",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": {
"email": "dustin@trufflesec.com",
"is_admin": "true",
"is_restricted": "false",
"mfa": "true",
"user": "Dustin Decker"
}
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}

View file

@ -0,0 +1,106 @@
// Code generated by go generate; DO NOT EDIT.
package openai
import "errors"
type Permission int
const (
NoAccess Permission = iota
ModelsRead Permission = iota
ModelCapabilitiesWrite Permission = iota
AssistantsRead Permission = iota
AssistantsWrite Permission = iota
ThreadsRead Permission = iota
ThreadsWrite Permission = iota
FineTuningRead Permission = iota
FineTuningWrite Permission = iota
FilesRead Permission = iota
FilesWrite Permission = iota
)
var (
permissionStrings = map[Permission]string{
ModelsRead: "models:read",
ModelCapabilitiesWrite: "model_capabilities:write",
AssistantsRead: "assistants:read",
AssistantsWrite: "assistants:write",
ThreadsRead: "threads:read",
ThreadsWrite: "threads:write",
FineTuningRead: "fine_tuning:read",
FineTuningWrite: "fine_tuning:write",
FilesRead: "files:read",
FilesWrite: "files:write",
}
stringToPermission = map[string]Permission{
"models:read": ModelsRead,
"model_capabilities:write": ModelCapabilitiesWrite,
"assistants:read": AssistantsRead,
"assistants:write": AssistantsWrite,
"threads:read": ThreadsRead,
"threads:write": ThreadsWrite,
"fine_tuning:read": FineTuningRead,
"fine_tuning:write": FineTuningWrite,
"files:read": FilesRead,
"files:write": FilesWrite,
}
permissionIDs = map[Permission]int{
ModelsRead: 0,
ModelCapabilitiesWrite: 1,
AssistantsRead: 2,
AssistantsWrite: 3,
ThreadsRead: 4,
ThreadsWrite: 5,
FineTuningRead: 6,
FineTuningWrite: 7,
FilesRead: 8,
FilesWrite: 9,
}
idToPermission = map[int]Permission{
0: ModelsRead,
1: ModelCapabilitiesWrite,
2: AssistantsRead,
3: AssistantsWrite,
4: ThreadsRead,
5: ThreadsWrite,
6: FineTuningRead,
7: FineTuningWrite,
8: FilesRead,
9: FilesWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := permissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := permissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := stringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := idToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}

View file

@ -0,0 +1,11 @@
permissions:
- models:read
- model_capabilities:write
- assistants:read
- assistants:write
- threads:read
- threads:write
- fine_tuning:read
- fine_tuning:write
- files:read
- files:write

View file

@ -1,11 +1,15 @@
package openai
import "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
import (
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
)
type OpenAIScope struct {
Name string
Tests []analyzers.HttpStatusTest
Endpoints []string
ReadTests []analyzers.HttpStatusTest
WriteTests []analyzers.HttpStatusTest
Endpoints []string
ReadPermission Permission
WritePermission Permission
}
func (s *OpenAIScope) RunTests(key string) error {
@ -13,8 +17,14 @@ func (s *OpenAIScope) RunTests(key string) error {
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
}
for i := range s.Tests {
test := &s.Tests[i]
for i := range s.ReadTests {
test := &s.ReadTests[i]
if err := test.RunTest(headers); err != nil {
return err
}
}
for i := range s.WriteTests {
test := &s.WriteTests[i]
if err := test.RunTest(headers); err != nil {
return err
}
@ -24,49 +34,61 @@ func (s *OpenAIScope) RunTests(key string) error {
var SCOPES = []OpenAIScope{
{
Name: "Models",
Tests: []analyzers.HttpStatusTest{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/models", Method: "GET", Valid: []int{200}, Invalid: []int{403}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/models"},
Endpoints: []string{"/v1/models"},
ReadPermission: ModelsRead,
},
{
Name: "Model capabilities",
Tests: []analyzers.HttpStatusTest{
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/images/generations", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/audio", "/v1/chat/completions", "/v1/embeddings", "/v1/images", "/v1/moderations"},
Endpoints: []string{"/v1/audio", "/v1/chat/completions", "/v1/embeddings", "/v1/images", "/v1/moderations"},
WritePermission: ModelCapabilitiesWrite,
},
{
Name: "Assistants",
Tests: []analyzers.HttpStatusTest{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/assistants", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/assistants", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/assistants"},
Endpoints: []string{"/v1/assistants"},
ReadPermission: AssistantsRead,
WritePermission: AssistantsWrite,
},
{
Name: "Threads",
Tests: []analyzers.HttpStatusTest{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/threads/1", Method: "GET", Valid: []int{400}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/threads", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/threads"},
Endpoints: []string{"/v1/threads"},
ReadPermission: ThreadsRead,
WritePermission: ThreadsWrite,
},
{
Name: "Fine-tuning",
Tests: []analyzers.HttpStatusTest{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/fine_tuning/jobs", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{400}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/fine_tuning"},
Endpoints: []string{"/v1/fine_tuning"},
ReadPermission: FineTuningRead,
WritePermission: FineTuningWrite,
},
{
Name: "Files",
Tests: []analyzers.HttpStatusTest{
ReadTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/files", Method: "GET", Valid: []int{200}, Invalid: []int{401}, Type: analyzers.READ, Status: analyzers.PermissionStatus{}},
},
WriteTests: []analyzers.HttpStatusTest{
{URL: BASE_URL + "/v1/files", Method: "POST", Payload: POST_PAYLOAD, Valid: []int{415}, Invalid: []int{401}, Type: analyzers.WRITE, Status: analyzers.PermissionStatus{}},
},
Endpoints: []string{"/v1/files"},
Endpoints: []string{"/v1/files"},
ReadPermission: FilesRead,
WritePermission: FilesWrite,
},
}

View file

@ -0,0 +1,136 @@
// Code generated by go generate; DO NOT EDIT.
package twilio
import "errors"
type Permission int
const (
NoAccess Permission = iota
AccountManagementRead Permission = iota
AccountManagementWrite Permission = iota
SubaccountConfigurationRead Permission = iota
SubaccountConfigurationWrite Permission = iota
KeyManagementRead Permission = iota
KeyManagementWrite Permission = iota
ServiceVerificationRead Permission = iota
ServiceVerificationWrite Permission = iota
SmsRead Permission = iota
SmsWrite Permission = iota
VoiceRead Permission = iota
VoiceWrite Permission = iota
MessagingRead Permission = iota
MessagingWrite Permission = iota
CallManagementRead Permission = iota
CallManagementWrite Permission = iota
)
var (
permissionStrings = map[Permission]string{
AccountManagementRead: "account_management:read",
AccountManagementWrite: "account_management:write",
SubaccountConfigurationRead: "subaccount_configuration:read",
SubaccountConfigurationWrite: "subaccount_configuration:write",
KeyManagementRead: "key_management:read",
KeyManagementWrite: "key_management:write",
ServiceVerificationRead: "service_verification:read",
ServiceVerificationWrite: "service_verification:write",
SmsRead: "sms:read",
SmsWrite: "sms:write",
VoiceRead: "voice:read",
VoiceWrite: "voice:write",
MessagingRead: "messaging:read",
MessagingWrite: "messaging:write",
CallManagementRead: "call_management:read",
CallManagementWrite: "call_management:write",
}
stringToPermission = map[string]Permission{
"account_management:read": AccountManagementRead,
"account_management:write": AccountManagementWrite,
"subaccount_configuration:read": SubaccountConfigurationRead,
"subaccount_configuration:write": SubaccountConfigurationWrite,
"key_management:read": KeyManagementRead,
"key_management:write": KeyManagementWrite,
"service_verification:read": ServiceVerificationRead,
"service_verification:write": ServiceVerificationWrite,
"sms:read": SmsRead,
"sms:write": SmsWrite,
"voice:read": VoiceRead,
"voice:write": VoiceWrite,
"messaging:read": MessagingRead,
"messaging:write": MessagingWrite,
"call_management:read": CallManagementRead,
"call_management:write": CallManagementWrite,
}
permissionIDs = map[Permission]int{
AccountManagementRead: 0,
AccountManagementWrite: 1,
SubaccountConfigurationRead: 2,
SubaccountConfigurationWrite: 3,
KeyManagementRead: 4,
KeyManagementWrite: 5,
ServiceVerificationRead: 6,
ServiceVerificationWrite: 7,
SmsRead: 8,
SmsWrite: 9,
VoiceRead: 10,
VoiceWrite: 11,
MessagingRead: 12,
MessagingWrite: 13,
CallManagementRead: 14,
CallManagementWrite: 15,
}
idToPermission = map[int]Permission{
0: AccountManagementRead,
1: AccountManagementWrite,
2: SubaccountConfigurationRead,
3: SubaccountConfigurationWrite,
4: KeyManagementRead,
5: KeyManagementWrite,
6: ServiceVerificationRead,
7: ServiceVerificationWrite,
8: SmsRead,
9: SmsWrite,
10: VoiceRead,
11: VoiceWrite,
12: MessagingRead,
13: MessagingWrite,
14: CallManagementRead,
15: CallManagementWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := permissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := permissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := stringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := idToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}

View file

@ -0,0 +1,17 @@
permissions:
- account_management:read
- account_management:write
- subaccount_configuration:read
- subaccount_configuration:write
- key_management:read
- key_management:write
- service_verification:read
- service_verification:write
- sms:read
- sms:write
- voice:read
- voice:write
- messaging:read
- messaging:write
- call_management:read
- call_management:write

View file

@ -1,3 +1,5 @@
//go:generate generate_permissions permissions.yaml permissions.go twilio
package twilio
import (
@ -10,8 +12,86 @@ import (
"github.com/fatih/color"
"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"
)
type Analyzer struct{}
func (a *Analyzer) Type() analyzerpb.AnalyzerType {
return analyzerpb.AnalyzerType_Twilio
}
func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credentialInfo["key"]
if !ok {
return nil, errors.New("key not found in credentialInfo")
}
cfg := &config.Config{} // You might need to adjust this based on how you want to handle config
info, err := AnalyzePermissions(cfg, key)
if err != nil {
return nil, err
}
var permissions []Permission
if info.AccountStatusCode == 200 {
permissions = []Permission{
AccountManagementRead,
AccountManagementWrite,
SubaccountConfigurationRead,
SubaccountConfigurationWrite,
KeyManagementRead,
KeyManagementWrite,
ServiceVerificationRead,
ServiceVerificationWrite,
SmsRead,
SmsWrite,
VoiceRead,
VoiceWrite,
MessagingRead,
MessagingWrite,
CallManagementRead,
CallManagementWrite,
}
} else if info.AccountStatusCode == 401 {
permissions = []Permission{
ServiceVerificationRead,
ServiceVerificationWrite,
SmsRead,
SmsWrite,
VoiceRead,
VoiceWrite,
MessagingRead,
MessagingWrite,
CallManagementRead,
CallManagementWrite,
}
}
// Can we get org information?
resource := analyzers.Resource{
Name: "Twilio API",
Type: "API",
}
var bindings []analyzers.Binding
for _, perm := range permissions {
permStr, _ := perm.ToString()
bindings = append(bindings, analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permStr,
},
})
}
return &analyzers.AnalyzerResult{
AnalyzerType: analyzerpb.AnalyzerType_Twilio,
Bindings: bindings,
}, nil
}
type VerifyJSON struct {
Code int `json:"code"`
}

View file

@ -0,0 +1,144 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
type PermissionsData struct {
Permissions []string `yaml:"permissions"`
PackageName string `yaml:"package_name"`
}
const templateText = `// Code generated by go generate; DO NOT EDIT.
package {{ .PackageName }}
import "errors"
type Permission int
const (
NoAccess Permission = iota
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }} Permission = iota
{{- end }}
)
var (
permissionStrings = map[Permission]string{
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }}: "{{ $permission }}",
{{- end }}
}
stringToPermission = map[string]Permission{
{{- range $index, $permission := .Permissions }}
"{{ $permission }}": {{ ToCamelCase $permission }},
{{- end }}
}
permissionIDs = map[Permission]int{
{{- range $index, $permission := .Permissions }}
{{ ToCamelCase $permission }}: {{ $index }},
{{- end }}
}
idToPermission = map[int]Permission{
{{- range $index, $permission := .Permissions }}
{{ $index }}: {{ ToCamelCase $permission }},
{{- end }}
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := permissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := permissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := stringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := idToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
`
// ToCamelCase converts a string to CamelCase
func ToCamelCase(s string) string {
parts := strings.Split(s, ":")
caser := cases.Title(language.English)
for i := range parts {
subParts := strings.Split(parts[i], "_")
for j := range subParts {
subParts[j] = caser.String(subParts[j])
}
parts[i] = strings.Join(subParts, "")
}
return strings.Join(parts, "")
}
func main() {
// Read the YAML file from first argument
file, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("Failed to open YAML file: %v", err)
}
defer file.Close()
var data PermissionsData
decoder := yaml.NewDecoder(file)
err = decoder.Decode(&data)
if err != nil {
log.Fatalf("Failed to decode YAML file: %v", err)
}
data.PackageName = os.Args[3]
// Parse the template
tmpl, err := template.New("permissions").Funcs(template.FuncMap{
"ToCamelCase": ToCamelCase,
}).Parse(templateText)
if err != nil {
log.Fatalf("Failed to parse template: %v", err)
}
// Generate the code
outputFile, err := os.Create(os.Args[2])
if err != nil {
log.Fatalf("Failed to create output file: %v", err)
}
defer outputFile.Close()
err = tmpl.Execute(outputFile, data)
if err != nil {
log.Fatalf("Failed to execute template: %v", err)
}
fmt.Println("Permissions code generated successfully.")
}