mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 07:04:24 +00:00
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:
parent
6fccac7f3d
commit
25b01019b3
22 changed files with 2544 additions and 876 deletions
21
.github/workflows/secrets.yml
vendored
21
.github/workflows/secrets.yml
vendored
|
@ -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
15
pkg/analyzer/README.md
Normal 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.
|
|
@ -38,9 +38,8 @@ type (
|
|||
}
|
||||
|
||||
Permission struct {
|
||||
Value string
|
||||
AccessLevel string
|
||||
Parent *Permission
|
||||
Value string
|
||||
Parent *Permission
|
||||
}
|
||||
|
||||
Binding struct {
|
||||
|
|
49
pkg/analyzer/analyzers/github/classic/classic.yaml
Normal file
49
pkg/analyzer/analyzers/github/classic/classic.yaml
Normal 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
|
296
pkg/analyzer/analyzers/github/classic/classic_permissions.go
Normal file
296
pkg/analyzer/analyzers/github/classic/classic_permissions.go
Normal 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")
|
||||
}
|
246
pkg/analyzer/analyzers/github/classic/classictoken.go
Normal file
246
pkg/analyzer/analyzers/github/classic/classictoken.go
Normal 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")
|
||||
}
|
|
@ -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")
|
||||
}
|
182
pkg/analyzer/analyzers/github/common/github.go
Normal file
182
pkg/analyzer/analyzers/github/common/github.go
Normal 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")
|
||||
}
|
File diff suppressed because it is too large
Load diff
80
pkg/analyzer/analyzers/github/finegrained/finegrained.yaml
Normal file
80
pkg/analyzer/analyzers/github/finegrained/finegrained.yaml
Normal 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
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
121
pkg/analyzer/analyzers/github/github_test.go
Normal file
121
pkg/analyzer/analyzers/github/github_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
|
|
125
pkg/analyzer/analyzers/openai/openai_test.go
Normal file
125
pkg/analyzer/analyzers/openai/openai_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
106
pkg/analyzer/analyzers/openai/permissions.go
Normal file
106
pkg/analyzer/analyzers/openai/permissions.go
Normal 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")
|
||||
}
|
11
pkg/analyzer/analyzers/openai/permissions.yaml
Normal file
11
pkg/analyzer/analyzers/openai/permissions.yaml
Normal 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
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
136
pkg/analyzer/analyzers/twilio/permissions.go
Normal file
136
pkg/analyzer/analyzers/twilio/permissions.go
Normal 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")
|
||||
}
|
17
pkg/analyzer/analyzers/twilio/permissions.yaml
Normal file
17
pkg/analyzer/analyzers/twilio/permissions.yaml
Normal 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
|
|
@ -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"`
|
||||
}
|
||||
|
|
144
pkg/analyzer/generate_permissions/generate_permissions.go
Normal file
144
pkg/analyzer/generate_permissions/generate_permissions.go
Normal 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.")
|
||||
}
|
Loading…
Reference in a new issue