[Feat] Added Figma (PAT) Analyzer (#3974)
* added figma analyzer * fixed lint issue * fixed lint issue (2) * added endpoint config from json * simplified scope extraction and endpoint configuration * updated with lint issue fixes --------- Co-authored-by: Kashif Khan <[email protected]>
This commit is contained in:
co-authored by
Kashif Khan
parent
9a59a7a52a
commit
b2ba219e24
@@ -92,6 +92,7 @@ const (
|
|||||||
AnalyzerTypeAirtablePat
|
AnalyzerTypeAirtablePat
|
||||||
AnalyzerTypeGroq
|
AnalyzerTypeGroq
|
||||||
AnalyzerTypeLaunchDarkly
|
AnalyzerTypeLaunchDarkly
|
||||||
|
AnalyzerTypeFigma
|
||||||
// Add new items here with AnalyzerType prefix
|
// Add new items here with AnalyzerType prefix
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
|||||||
AnalyzerTypeAirtablePat: "AirtablePat",
|
AnalyzerTypeAirtablePat: "AirtablePat",
|
||||||
AnalyzerTypeGroq: "Groq",
|
AnalyzerTypeGroq: "Groq",
|
||||||
AnalyzerTypeLaunchDarkly: "LaunchDarkly",
|
AnalyzerTypeLaunchDarkly: "LaunchDarkly",
|
||||||
|
AnalyzerTypeFigma: "Figma",
|
||||||
// Add new mappings here
|
// Add new mappings here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"files:read": {
|
||||||
|
"url": "https://api.figma.com/v1/me",
|
||||||
|
"method": "GET",
|
||||||
|
"expected_status_code_with_scope": 200,
|
||||||
|
"expected_status_code_without_scope": 403
|
||||||
|
},
|
||||||
|
"library_analytics:read": {
|
||||||
|
"url": "https://api.figma.com/v1/analytics/libraries/0/component/actions",
|
||||||
|
"method": "GET",
|
||||||
|
"expected_status_code_with_scope": 400,
|
||||||
|
"expected_status_code_without_scope": 403
|
||||||
|
},
|
||||||
|
"file_dev_resources:write": {
|
||||||
|
"url": "https://api.figma.com/v1/dev_resources",
|
||||||
|
"method": "POST",
|
||||||
|
"expected_status_code_with_scope": 400,
|
||||||
|
"expected_status_code_without_scope": 403
|
||||||
|
},
|
||||||
|
"file_variables:read": {
|
||||||
|
"url": "https://api.figma.com/v1/files/0/variables/published",
|
||||||
|
"method": "GET",
|
||||||
|
"expected_status_code_with_scope": 404,
|
||||||
|
"expected_status_code_without_scope": 403
|
||||||
|
},
|
||||||
|
"webhooks:write": {
|
||||||
|
"url": "https://api.figma.com/v2/webhooks",
|
||||||
|
"method": "POST",
|
||||||
|
"expected_status_code_with_scope": 400,
|
||||||
|
"expected_status_code_without_scope": 403
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"AnalyzerType":32,"Bindings":[{"Resource":{"Name":"Source Integration","FullyQualifiedName":"1287160752716166666","Type":"user","Metadata":{"email":"[email protected]","img_url":"https://www.gravatar.com/avatar/48da7f448c34d4271a51d2ccf058f473?size=240&default=https%3A%2F%2Fs3-alpha.figma.com%2Fstatic%2Fuser_s_v2.png"},"Parent":null},"Permission":{"Value":"files:read","Parent":null}}],"UnboundedResources":null,"Metadata":null}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
//go:generate generate_permissions permissions.yaml permissions.go figma
|
||||||
|
|
||||||
|
package figma
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
||||||
|
|
||||||
|
type Analyzer struct {
|
||||||
|
Cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeFigma }
|
||||||
|
|
||||||
|
type ScopeStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusError ScopeStatus = "Error"
|
||||||
|
StatusGranted ScopeStatus = "Granted"
|
||||||
|
StatusDenied ScopeStatus = "Denied"
|
||||||
|
StatusUnverified ScopeStatus = "Unverified"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||||
|
token, ok := credInfo["token"]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("token not found in credInfo")
|
||||||
|
}
|
||||||
|
info, err := AnalyzePermissions(a.Cfg, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return MapToAnalyzerResult(info), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
|
||||||
|
info, err := AnalyzePermissions(cfg, token)
|
||||||
|
if err != nil {
|
||||||
|
color.Red("[x] Error : %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Green("[!] Valid Figma Personal Access Token\n\n")
|
||||||
|
PrintUserAndPermissions(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzePermissions(cfg *config.Config, token string) (*secretInfo, error) {
|
||||||
|
client := analyzers.NewAnalyzeClient(cfg)
|
||||||
|
allScopes := getAllScopes()
|
||||||
|
scopeToEndpoints, err := getScopeEndpointsMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = &secretInfo{Scopes: map[Scope]ScopeStatus{}}
|
||||||
|
for _, scope := range allScopes {
|
||||||
|
info.Scopes[scope] = StatusUnverified
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range orderedScopeList {
|
||||||
|
endpoint, err := getScopeEndpoint(scopeToEndpoints, scope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := callAPIEndpoint(client, token, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeStatus := determineScopeStatus(resp.StatusCode, endpoint)
|
||||||
|
if scopeStatus == StatusGranted {
|
||||||
|
if scope == ScopeFilesRead {
|
||||||
|
if err := json.Unmarshal(body, &info.UserInfo); err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding user info from response %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Scopes[scope] = StatusGranted
|
||||||
|
}
|
||||||
|
// If the token does NOT have the scope, response will include all the scopes it does have
|
||||||
|
if scopeStatus == StatusDenied {
|
||||||
|
scopes, ok := extractScopesFromError(body)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("could not extract scopes from error message")
|
||||||
|
}
|
||||||
|
for scope := range info.Scopes {
|
||||||
|
info.Scopes[scope] = StatusDenied
|
||||||
|
}
|
||||||
|
for _, scope := range scopes {
|
||||||
|
info.Scopes[scope] = StatusGranted
|
||||||
|
}
|
||||||
|
// We have enough info to finish analysis
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineScopeStatus takes the API response status code and uses it along with the expected
|
||||||
|
// status codes to dermine whether the access token has the required scope to perform that action.
|
||||||
|
// It returns a ScopeStatus which can be Granted, Denied, or Unverified.
|
||||||
|
func determineScopeStatus(statusCode int, endpoint endpoint) ScopeStatus {
|
||||||
|
if statusCode == endpoint.ExpectedStatusCodeWithScope || statusCode == http.StatusOK {
|
||||||
|
return StatusGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusCode == endpoint.ExpectedStatusCodeWithoutScope {
|
||||||
|
return StatusDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can not determine scope as the expected error is unknown
|
||||||
|
return StatusUnverified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches API response body with expected message pattern in case the token is missing a scope
|
||||||
|
// If the responses match, we can extract all available scopes from the response msg
|
||||||
|
func extractScopesFromError(body []byte) ([]Scope, bool) {
|
||||||
|
filteredBody := filterErrorResponseBody(string(body))
|
||||||
|
re := regexp.MustCompile(`Invalid scope(?:\(s\))?: ([a-zA-Z_:, ]+)\. This endpoint requires.*`)
|
||||||
|
matches := re.FindStringSubmatch(filteredBody)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
scopes := strings.Split(matches[1], ", ")
|
||||||
|
return getScopesFromScopeStrings(scopes), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filterErrorResponseBody function cleans the provided "invalid permission" API
|
||||||
|
// response message by removing the characters '"', '[', ']', '\', and '"'.
|
||||||
|
func filterErrorResponseBody(msg string) string {
|
||||||
|
result := strings.ReplaceAll(msg, "\\", "")
|
||||||
|
result = strings.ReplaceAll(result, "\"", "")
|
||||||
|
result = strings.ReplaceAll(result, "[", "")
|
||||||
|
return strings.ReplaceAll(result, "]", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
|
||||||
|
if info == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := analyzers.AnalyzerResult{
|
||||||
|
AnalyzerType: analyzers.AnalyzerTypeFigma,
|
||||||
|
}
|
||||||
|
var permissions []analyzers.Permission
|
||||||
|
for scope, status := range info.Scopes {
|
||||||
|
if status != StatusGranted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
permissions = append(permissions, analyzers.Permission{Value: string(scope)})
|
||||||
|
}
|
||||||
|
userResource := analyzers.Resource{
|
||||||
|
Name: info.UserInfo.Handle,
|
||||||
|
FullyQualifiedName: info.UserInfo.ID,
|
||||||
|
Type: "user",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"email": info.UserInfo.Email,
|
||||||
|
"img_url": info.UserInfo.ImgURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Bindings = analyzers.BindAllPermissions(userResource, permissions...)
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintUserAndPermissions(info *secretInfo) {
|
||||||
|
color.Yellow("[i] User Info:")
|
||||||
|
t1 := table.NewWriter()
|
||||||
|
t1.SetOutputMirror(os.Stdout)
|
||||||
|
t1.AppendHeader(table.Row{"ID", "Handle", "Email", "Image URL"})
|
||||||
|
t1.AppendRow(table.Row{
|
||||||
|
color.GreenString(info.UserInfo.ID),
|
||||||
|
color.GreenString(info.UserInfo.Handle),
|
||||||
|
color.GreenString(info.UserInfo.Email),
|
||||||
|
color.GreenString(info.UserInfo.ImgURL),
|
||||||
|
})
|
||||||
|
t1.SetOutputMirror(os.Stdout)
|
||||||
|
t1.Render()
|
||||||
|
|
||||||
|
color.Yellow("\n[i] Scopes:")
|
||||||
|
t2 := table.NewWriter()
|
||||||
|
t2.AppendHeader(table.Row{"Scope", "Status", "Actions"})
|
||||||
|
for scope, status := range info.Scopes {
|
||||||
|
actions := getScopeActions(scope)
|
||||||
|
rows := []table.Row{}
|
||||||
|
for i, action := range actions {
|
||||||
|
var scopeCell string
|
||||||
|
var statusCell string
|
||||||
|
if i == 0 {
|
||||||
|
scopeCell = color.GreenString(string(scope))
|
||||||
|
statusCell = color.GreenString(string(status))
|
||||||
|
}
|
||||||
|
rows = append(rows, table.Row{scopeCell, statusCell, color.GreenString(action)})
|
||||||
|
}
|
||||||
|
t2.AppendRows(rows)
|
||||||
|
t2.AppendSeparator()
|
||||||
|
}
|
||||||
|
t2.SetOutputMirror(os.Stdout)
|
||||||
|
t2.Render()
|
||||||
|
fmt.Printf("%s: https://www.figma.com/developers/api\n\n", color.GreenString("Ref"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package figma
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed expected_output.json
|
||||||
|
var expectedOutput []byte
|
||||||
|
|
||||||
|
func TestAnalyzer_Analyze(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
token string
|
||||||
|
want string // JSON string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
token: testSecrets.MustGetField("FIGMAPERSONALACCESSTOKEN_V2_TOKEN"),
|
||||||
|
name: "valid Figma Personal Access Token",
|
||||||
|
want: string(expectedOutput),
|
||||||
|
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{"token": tt.token})
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindings need to be in the same order to be comparable
|
||||||
|
sortBindings(got.Bindings)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindings need to be in the same order to be comparable
|
||||||
|
sortBindings(wantObj.Bindings)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to sort bindings
|
||||||
|
func sortBindings(bindings []analyzers.Binding) {
|
||||||
|
sort.SliceStable(bindings, func(i, j int) bool {
|
||||||
|
if bindings[i].Resource.Name == bindings[j].Resource.Name {
|
||||||
|
return bindings[i].Permission.Value < bindings[j].Permission.Value
|
||||||
|
}
|
||||||
|
return bindings[i].Resource.Name < bindings[j].Resource.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package figma
|
||||||
|
|
||||||
|
type userInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
ImgURL string `json:"img_url"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretInfo struct {
|
||||||
|
UserInfo userInfo
|
||||||
|
Scopes map[Scope]ScopeStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpoint struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
ExpectedStatusCodeWithScope int `json:"expected_status_code_with_scope"`
|
||||||
|
ExpectedStatusCodeWithoutScope int `json:"expected_status_code_without_scope"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Code generated by go generate; DO NOT EDIT.
|
||||||
|
package figma
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
type Permission int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Invalid Permission = iota
|
||||||
|
FilesRead Permission = iota
|
||||||
|
FileVariablesRead Permission = iota
|
||||||
|
FileVariablesWrite Permission = iota
|
||||||
|
FileCommentsWrite Permission = iota
|
||||||
|
FileDevResourcesRead Permission = iota
|
||||||
|
FileDevResourcesWrite Permission = iota
|
||||||
|
LibraryAnalyticsRead Permission = iota
|
||||||
|
WebhooksWrite Permission = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
PermissionStrings = map[Permission]string{
|
||||||
|
FilesRead: "files:read",
|
||||||
|
FileVariablesRead: "file_variables:read",
|
||||||
|
FileVariablesWrite: "file_variables:write",
|
||||||
|
FileCommentsWrite: "file_comments:write",
|
||||||
|
FileDevResourcesRead: "file_dev_resources:read",
|
||||||
|
FileDevResourcesWrite: "file_dev_resources:write",
|
||||||
|
LibraryAnalyticsRead: "library_analytics:read",
|
||||||
|
WebhooksWrite: "webhooks:write",
|
||||||
|
}
|
||||||
|
|
||||||
|
StringToPermission = map[string]Permission{
|
||||||
|
"files:read": FilesRead,
|
||||||
|
"file_variables:read": FileVariablesRead,
|
||||||
|
"file_variables:write": FileVariablesWrite,
|
||||||
|
"file_comments:write": FileCommentsWrite,
|
||||||
|
"file_dev_resources:read": FileDevResourcesRead,
|
||||||
|
"file_dev_resources:write": FileDevResourcesWrite,
|
||||||
|
"library_analytics:read": LibraryAnalyticsRead,
|
||||||
|
"webhooks:write": WebhooksWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionIDs = map[Permission]int{
|
||||||
|
FilesRead: 1,
|
||||||
|
FileVariablesRead: 2,
|
||||||
|
FileVariablesWrite: 3,
|
||||||
|
FileCommentsWrite: 4,
|
||||||
|
FileDevResourcesRead: 5,
|
||||||
|
FileDevResourcesWrite: 6,
|
||||||
|
LibraryAnalyticsRead: 7,
|
||||||
|
WebhooksWrite: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
IdToPermission = map[int]Permission{
|
||||||
|
1: FilesRead,
|
||||||
|
2: FileVariablesRead,
|
||||||
|
3: FileVariablesWrite,
|
||||||
|
4: FileCommentsWrite,
|
||||||
|
5: FileDevResourcesRead,
|
||||||
|
6: FileDevResourcesWrite,
|
||||||
|
7: LibraryAnalyticsRead,
|
||||||
|
8: WebhooksWrite,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
permissions:
|
||||||
|
- files:read
|
||||||
|
- file_variables:read
|
||||||
|
- file_variables:write
|
||||||
|
- file_comments:write
|
||||||
|
- file_dev_resources:read
|
||||||
|
- file_dev_resources:write
|
||||||
|
- library_analytics:read
|
||||||
|
- webhooks:write
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package figma
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func callAPIEndpoint(client *http.Client, token string, endpoint endpoint) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(endpoint.Method, endpoint.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("X-FIGMA-TOKEN", token)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package figma
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeFilesRead Scope = "files:read"
|
||||||
|
ScopeFileVariablesRead Scope = "file_variables:read"
|
||||||
|
ScopeFileVariablesWrite Scope = "file_variables:write"
|
||||||
|
ScopeFileCommentsWrite Scope = "file_comments:write"
|
||||||
|
ScopeFileDevResourcesRead Scope = "file_dev_resources:read"
|
||||||
|
ScopeFileDevResourcesWrite Scope = "file_dev_resources:write"
|
||||||
|
ScopeLibraryAnalyticsRead Scope = "library_analytics:read"
|
||||||
|
ScopeWebhooksWrite Scope = "webhooks:write"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This list orders the scope in which they must be tested
|
||||||
|
var orderedScopeList = []Scope{
|
||||||
|
ScopeFilesRead,
|
||||||
|
ScopeLibraryAnalyticsRead,
|
||||||
|
ScopeFileDevResourcesWrite,
|
||||||
|
ScopeFileVariablesRead,
|
||||||
|
ScopeWebhooksWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopeToActions = map[Scope][]string{
|
||||||
|
ScopeFilesRead: {
|
||||||
|
"Get user info",
|
||||||
|
"Read files",
|
||||||
|
"Read projects",
|
||||||
|
"Read users",
|
||||||
|
"Read versions",
|
||||||
|
"Read comments",
|
||||||
|
"Read components & styles",
|
||||||
|
"Read webhooks",
|
||||||
|
},
|
||||||
|
ScopeFileVariablesRead: {
|
||||||
|
"Read file variables",
|
||||||
|
},
|
||||||
|
ScopeFileVariablesWrite: {
|
||||||
|
"Write file variables",
|
||||||
|
},
|
||||||
|
ScopeFileCommentsWrite: {
|
||||||
|
"Post comments",
|
||||||
|
"Delete comments",
|
||||||
|
"Post comment reactions",
|
||||||
|
"Delete comment reactions",
|
||||||
|
},
|
||||||
|
ScopeFileDevResourcesRead: {
|
||||||
|
"Read file dev resources",
|
||||||
|
},
|
||||||
|
ScopeFileDevResourcesWrite: {
|
||||||
|
"Write file dev resources",
|
||||||
|
},
|
||||||
|
ScopeLibraryAnalyticsRead: {
|
||||||
|
"Read design system analytics",
|
||||||
|
},
|
||||||
|
ScopeWebhooksWrite: {
|
||||||
|
"Create webhooks",
|
||||||
|
"Manage webhooks",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopeStringToScope map[string]Scope
|
||||||
|
|
||||||
|
//go:embed endpoints.json
|
||||||
|
var endpointsConfig []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
scopeStringToScope = map[string]Scope{
|
||||||
|
string(ScopeFilesRead): ScopeFilesRead,
|
||||||
|
string(ScopeFileVariablesRead): ScopeFileVariablesRead,
|
||||||
|
string(ScopeFileVariablesWrite): ScopeFileVariablesWrite,
|
||||||
|
string(ScopeFileCommentsWrite): ScopeFileCommentsWrite,
|
||||||
|
string(ScopeFileDevResourcesRead): ScopeFileDevResourcesRead,
|
||||||
|
string(ScopeFileDevResourcesWrite): ScopeFileDevResourcesWrite,
|
||||||
|
string(ScopeLibraryAnalyticsRead): ScopeLibraryAnalyticsRead,
|
||||||
|
string(ScopeWebhooksWrite): ScopeWebhooksWrite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScopeActions(scope Scope) []string {
|
||||||
|
return scopeToActions[scope]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScopeEndpointsMap() (map[Scope]endpoint, error) {
|
||||||
|
var scopeToEndpoints map[Scope]endpoint
|
||||||
|
if err := json.Unmarshal(endpointsConfig, &scopeToEndpoints); err != nil {
|
||||||
|
return nil, errors.New("failed to unmarshal endpoints.json: " + err.Error())
|
||||||
|
}
|
||||||
|
return scopeToEndpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScopeEndpoint(scopeToEndpoint map[Scope]endpoint, scope Scope) (endpoint, error) {
|
||||||
|
if endpoint, ok := scopeToEndpoint[scope]; ok {
|
||||||
|
return endpoint, nil
|
||||||
|
}
|
||||||
|
return endpoint{}, errors.New("invalid scope or endpoint doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScopesFromScopeStrings(scopeStrings []string) []Scope {
|
||||||
|
var scopes []Scope
|
||||||
|
for _, scopeString := range scopeStrings {
|
||||||
|
if scope, ok := scopeStringToScope[scopeString]; ok {
|
||||||
|
scopes = append(scopes, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllScopes() []Scope {
|
||||||
|
return []Scope{
|
||||||
|
ScopeFilesRead,
|
||||||
|
ScopeFileVariablesRead,
|
||||||
|
ScopeFileVariablesWrite,
|
||||||
|
ScopeFileCommentsWrite,
|
||||||
|
ScopeFileDevResourcesRead,
|
||||||
|
ScopeFileDevResourcesWrite,
|
||||||
|
ScopeLibraryAnalyticsRead,
|
||||||
|
ScopeWebhooksWrite,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/digitalocean"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/digitalocean"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/elevenlabs"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/elevenlabs"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/figma"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/groq"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/groq"
|
||||||
@@ -115,5 +116,7 @@ func Run(keyType string, secretInfo SecretInfo) {
|
|||||||
groq.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
groq.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
case "launchdarkly":
|
case "launchdarkly":
|
||||||
launchdarkly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
launchdarkly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
|
case "figma":
|
||||||
|
figma.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
} else {
|
} else {
|
||||||
s1.SetVerificationError(err, resMatch)
|
s1.SetVerificationError(err, resMatch)
|
||||||
}
|
}
|
||||||
|
if s1.Verified {
|
||||||
|
s1.AnalysisInfo = map[string]string{"token": resMatch}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, s1)
|
results = append(results, s1)
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
} else {
|
} else {
|
||||||
s1.SetVerificationError(err, resMatch)
|
s1.SetVerificationError(err, resMatch)
|
||||||
}
|
}
|
||||||
|
if s1.Verified {
|
||||||
|
s1.AnalysisInfo = map[string]string{"token": resMatch}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, s1)
|
results = append(results, s1)
|
||||||
|
|||||||
Reference in New Issue
Block a user