Fastly Analyzer (#4082)
* initial commit * added more apis and test cases * fixed test cases * added more apis * fixed analyzer type * Update launchdarkly_test.go
This commit is contained in:
@@ -95,6 +95,7 @@ const (
|
||||
AnalyzerTypeFigma
|
||||
AnalyzerTypePlaid
|
||||
AnalyzerTypeNetlify
|
||||
AnalyzerTypeFastly
|
||||
// Add new items here with AnalyzerType prefix
|
||||
)
|
||||
|
||||
@@ -135,6 +136,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
||||
AnalyzerTypeFigma: "Figma",
|
||||
AnalyzerTypePlaid: "Plaid",
|
||||
AnalyzerTypeNetlify: "Netlify",
|
||||
AnalyzerTypeFastly: "Fastly",
|
||||
// Add new mappings here
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go fastly
|
||||
package fastly
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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 (a Analyzer) Type() analyzers.AnalyzerType {
|
||||
return analyzers.AnalyzerTypeFastly
|
||||
}
|
||||
|
||||
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||
key, exist := credInfo["key"]
|
||||
if !exist {
|
||||
return nil, fmt.Errorf("key not found in credential info")
|
||||
}
|
||||
|
||||
// analyze permissions
|
||||
info, err := AnalyzePermissions(a.Cfg, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// secret info to analyzer
|
||||
return secretInfoToAnalyzerResult(info), nil
|
||||
}
|
||||
|
||||
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
||||
info, err := AnalyzePermissions(cfg, key)
|
||||
if err != nil {
|
||||
// just print the error in cli and continue as a partial success
|
||||
color.Red("[x] Error : %s", err.Error())
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
color.Red("[x] Error : %s", "No information found")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("[!] Valid Fastly API key\n\n")
|
||||
|
||||
if info.TokenInfo.hasGlobalScope() {
|
||||
printUserInfo(info.UserInfo)
|
||||
}
|
||||
|
||||
printScopes(info.TokenInfo.Scopes)
|
||||
|
||||
if len(info.Resources) > 0 {
|
||||
printResources(info.Resources)
|
||||
}
|
||||
|
||||
color.Yellow("\n[i] Expires: %s", info.TokenInfo.ExpiresAt)
|
||||
}
|
||||
|
||||
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
||||
// create http client
|
||||
client := analyzers.NewAnalyzeClient(cfg)
|
||||
|
||||
var secretInfo = &SecretInfo{}
|
||||
|
||||
// capture the token details
|
||||
if err := captureTokenInfo(client, key, secretInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/*
|
||||
Fastly defines four types of permissions. Two of these are related specifically to purging:
|
||||
|
||||
- If a token has either `purge_select` or `purge_all` access, it is limited to calling purge-related APIs only.
|
||||
- If a token has `global` or `global:read` access, it can call APIs that retrieve resource and user information.
|
||||
*/
|
||||
|
||||
if !secretInfo.TokenInfo.hasGlobalScope() {
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
// capture the user information
|
||||
if err := captureUserInfo(client, key, secretInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// capture the resources
|
||||
if err := captureResources(client, key, secretInfo); err != nil {
|
||||
// return secretInfo as well in case of error for partial success
|
||||
return secretInfo, err
|
||||
}
|
||||
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
|
||||
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypeFastly,
|
||||
Metadata: map[string]any{},
|
||||
Bindings: make([]analyzers.Binding, 0),
|
||||
}
|
||||
|
||||
// extract information from resource to create bindings and append to result bindings
|
||||
for _, resource := range info.Resources {
|
||||
binding := analyzers.Binding{
|
||||
Resource: *secretInfoResourceToAnalyzerResource(resource),
|
||||
Permission: analyzers.Permission{
|
||||
Value: info.TokenInfo.Scope,
|
||||
},
|
||||
}
|
||||
|
||||
if resource.Parent != nil {
|
||||
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
|
||||
}
|
||||
|
||||
result.Bindings = append(result.Bindings, binding)
|
||||
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
|
||||
func secretInfoResourceToAnalyzerResource(resource FastlyResource) *analyzers.Resource {
|
||||
analyzerRes := analyzers.Resource{
|
||||
// make fully qualified name unique
|
||||
FullyQualifiedName: resource.Type + "/" + resource.ID,
|
||||
Name: resource.Name,
|
||||
Type: resource.Type,
|
||||
Metadata: map[string]any{},
|
||||
}
|
||||
|
||||
for key, value := range resource.Metadata {
|
||||
analyzerRes.Metadata[key] = value
|
||||
}
|
||||
|
||||
return &analyzerRes
|
||||
}
|
||||
|
||||
// cli print functions
|
||||
func printUserInfo(user User) {
|
||||
color.Yellow("[i] User Information:")
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"ID", "Name", "Login", "Role", "Last Active At"})
|
||||
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Login), color.GreenString(user.Role), color.GreenString(user.LastActiveAt)})
|
||||
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func printScopes(scopes []string) {
|
||||
color.Yellow("[i] Scopes:")
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"Scopes"})
|
||||
for _, scope := range scopes {
|
||||
t.AppendRow(table.Row{color.GreenString(scope)})
|
||||
}
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func printResources(resources []FastlyResource) {
|
||||
color.Yellow("[i] Resources:")
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"Name", "Type"})
|
||||
for _, resource := range resources {
|
||||
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package fastly
|
||||
|
||||
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 result_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
key := testSecrets.MustGetField("FASTLYPERSONALTOKEN_TOKEN")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
want []byte // JSON string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid fastly token",
|
||||
key: key,
|
||||
want: 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{"key": tt.key})
|
||||
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.FullyQualifiedName == bindings[j].Resource.FullyQualifiedName {
|
||||
return bindings[i].Permission.Value < bindings[j].Permission.Value
|
||||
}
|
||||
return bindings[i].Resource.FullyQualifiedName < bindings[j].Resource.FullyQualifiedName
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package fastly
|
||||
|
||||
import "sync"
|
||||
|
||||
const (
|
||||
// types
|
||||
TypeUserToken string = "User Token"
|
||||
TypeAutomationToken string = "Automation Token"
|
||||
TypeService string = "Service"
|
||||
TypeSvcVersion string = "Service Version"
|
||||
TypeSvcVersionACL string = "Service Version ACL"
|
||||
TypeSvcVersionDict string = "Service Version Dictionary"
|
||||
TypeSvcVersionBackend string = "Service Version Backend"
|
||||
TypeSvcVersionDomain string = "Service Version Domain"
|
||||
TypeSvcVersionHealthCheck string = "Service Version Health Check"
|
||||
TypeConfigStore string = "Config Store"
|
||||
TypeSecretStore string = "Secret Store"
|
||||
TypeTLSPrivateKey string = "TLS Private Key"
|
||||
TypeTLSCertificate string = "TLS Certificates"
|
||||
TypeTLSDomain string = "TLS Domain"
|
||||
TypeInvoice string = "Invoice"
|
||||
)
|
||||
|
||||
type SecretInfo struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
UserInfo User
|
||||
TokenInfo SelfToken
|
||||
Resources []FastlyResource
|
||||
}
|
||||
|
||||
type FastlyResource struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Metadata map[string]string
|
||||
Parent *FastlyResource
|
||||
}
|
||||
|
||||
// AppendResource append resource to secret info resource list
|
||||
func (s *SecretInfo) appendResource(resource FastlyResource) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.Resources = append(s.Resources, resource)
|
||||
}
|
||||
|
||||
// listResourceByType returns a list of resources matching the given type.
|
||||
func (s *SecretInfo) listResourceByType(resourceType string) []FastlyResource {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
resources := make([]FastlyResource, 0, len(s.Resources))
|
||||
for _, resource := range s.Resources {
|
||||
if resource.Type == resourceType {
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
// API Response models
|
||||
|
||||
// User is /current_user API Response
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
LastActiveAt string `json:"last_active_at"`
|
||||
}
|
||||
|
||||
// SelfToken is /tokens/self API Response
|
||||
type SelfToken struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
LastUsedAt string `json:"last_used_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Scope string `json:"scope"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Services []string `json:"services"`
|
||||
}
|
||||
|
||||
// hasGlobalScope returns true if any global scope is assigned to the token
|
||||
func (t SelfToken) hasGlobalScope() bool {
|
||||
for _, scope := range t.Scopes {
|
||||
if scope == PermissionStrings[Global] || scope == PermissionStrings[GlobalRead] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TokenData is /automation-tokens API Response
|
||||
type TokenData struct {
|
||||
Data []Token `json:"data"`
|
||||
}
|
||||
|
||||
// Token is /tokens API Response
|
||||
type Token struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
Role string `json:"role"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
// Service is /service API Response
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Version is /service/<id>/version API Response
|
||||
type Version struct {
|
||||
Number int `json:"number"`
|
||||
Active bool `json:"active"`
|
||||
Deployed bool `json:"deployed"`
|
||||
ServiceID string `json:"service_id"`
|
||||
}
|
||||
|
||||
// ACL is /service/<id>/version/<number>/acl API Response
|
||||
type ACL struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Dictionary is the /service/<id>/version/<number>/dictionary API Response
|
||||
type Dictionary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Backend is the /service/<id>/version/<number>/backend API Response
|
||||
type Backend struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
// Domain is the /service/<id>/version/<number>/domain API Response
|
||||
type Domain struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// HealthCheck is the /service/<id>/version/<number>/healthcheck API Response
|
||||
type HealthCheck struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// ConfigStore is the /resources/stores/config API Response
|
||||
type ConfigStore struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// SecretStoreData is the /resources/stores/secret API Response
|
||||
type SecretStoreData struct {
|
||||
Data []SecretStore `json:"data"`
|
||||
}
|
||||
|
||||
// SecretStore is a single store in SecretStoreData
|
||||
type SecretStore struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TLSPrivateKeyData is the /tls/private_keys API Response
|
||||
type TLSPrivateKeyData struct {
|
||||
Data []TLSPrivateKey `json:"data"`
|
||||
}
|
||||
|
||||
// TLSPrivateKey is the single TLS private key in TLSPrivateKeyData
|
||||
type TLSPrivateKey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TLSCertificatesData is the /tls/certificates API Response
|
||||
type TLSCertificatesData struct {
|
||||
Data []TLSCertificate `json:"data"`
|
||||
}
|
||||
|
||||
// TLSCertificate is the single TLS certificate in TLSCertificatesData
|
||||
type TLSCertificate struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TLSDomainsData is the /tls/domains API Response
|
||||
type TLSDomainsData struct {
|
||||
Data []TLSDomain `json:"data"`
|
||||
}
|
||||
|
||||
// TLSDomain is the single TLS Domain in TLSDomainsData
|
||||
type TLSDomain struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// InvoicesData is the /billing/v3/invoices API Response
|
||||
type InvoicesData struct {
|
||||
Data []Invoice `json:"data"`
|
||||
}
|
||||
|
||||
// Invoice is the single invoice in InvoicesData
|
||||
type Invoice struct {
|
||||
ID string `json:"invoice_id"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
Region string `json:"region"`
|
||||
StatementNo string `json:"statement_number"`
|
||||
InvoicePostedOn string `json:"invoice_posted_on"`
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
package fastly
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Global Permission = iota
|
||||
GlobalRead Permission = iota
|
||||
PurgeAll Permission = iota
|
||||
PurgeSelect Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
Global: "global",
|
||||
GlobalRead: "global:read",
|
||||
PurgeAll: "purge_all",
|
||||
PurgeSelect: "purge_select",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"global": Global,
|
||||
"global:read": GlobalRead,
|
||||
"purge_all": PurgeAll,
|
||||
"purge_select": PurgeSelect,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
Global: 1,
|
||||
GlobalRead: 2,
|
||||
PurgeAll: 3,
|
||||
PurgeSelect: 4,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: Global,
|
||||
2: GlobalRead,
|
||||
3: PurgeAll,
|
||||
4: PurgeSelect,
|
||||
}
|
||||
)
|
||||
|
||||
// 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,5 @@
|
||||
permissions:
|
||||
- global
|
||||
- global:read
|
||||
- purge_all
|
||||
- purge_select
|
||||
@@ -0,0 +1,744 @@
|
||||
package fastly
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type endpoint int
|
||||
|
||||
const (
|
||||
// list of endpoints
|
||||
selfToken endpoint = iota
|
||||
currentUser
|
||||
userTokens
|
||||
automationTokens
|
||||
service
|
||||
serviceVersions
|
||||
serviceVersionACLs
|
||||
serviceVersionDictionaries
|
||||
serviceVersionBackends
|
||||
serviceVersionDomains
|
||||
serviceVersionHealthChecks
|
||||
configStores
|
||||
secretStores
|
||||
tlsPrivateKeys
|
||||
tlsCertificates
|
||||
tlsDomains
|
||||
invoices
|
||||
)
|
||||
|
||||
var (
|
||||
baseURL = "https://api.fastly.com"
|
||||
|
||||
// endpoints contain Fastly API endpoints
|
||||
endpoints = map[endpoint]string{
|
||||
selfToken: "/tokens/self",
|
||||
currentUser: "/current_user",
|
||||
userTokens: "/tokens",
|
||||
automationTokens: "/automation-tokens",
|
||||
service: "/service",
|
||||
serviceVersions: "/service/%s/version", // require service id
|
||||
serviceVersionACLs: "/service/%s/version/%s/acl", // require service id and version number
|
||||
serviceVersionDictionaries: "/service/%s/version/%s/dictionary", // require service id and version number
|
||||
serviceVersionBackends: "/service/%s/version/%s/backend", // require service id and version number
|
||||
serviceVersionDomains: "/service/%s/version/%s/domain", // require service id and version number
|
||||
serviceVersionHealthChecks: "/service/%s/version/%s/healthcheck", // require service id and version number
|
||||
configStores: "/resources/stores/config",
|
||||
secretStores: "/resources/stores/secret",
|
||||
tlsPrivateKeys: "/tls/private_keys",
|
||||
tlsCertificates: "/tls/certificates",
|
||||
tlsDomains: "/tls/domains",
|
||||
invoices: "/billing/v3/invoices",
|
||||
|
||||
/*
|
||||
API:
|
||||
- /service/service_id/version/version_id/package (The use of this API is discouraged as per documentation due to limited availability release)
|
||||
- /tls/bulk/certificates (The use of this API is discouraged as per documentation due to limited availability release)
|
||||
- /security/workspaces (This Fastly Security API is only available to customers with access to the Next-Gen WAF product )
|
||||
- /events (This API just returns the account events like user logged in or user logged out etc)
|
||||
|
||||
Utilities API Docs:
|
||||
Some of these APIs are deprecated while others return same response for everyone with a global access key.
|
||||
- https://www.fastly.com/documentation/reference/api/utils/
|
||||
*/
|
||||
}
|
||||
)
|
||||
|
||||
// makeFastlyRequest send the API request to passed url with passed key as API Key and return response body and status code
|
||||
func makeFastlyRequest(client *http.Client, endpoint, key string) ([]byte, int, error) {
|
||||
// create request
|
||||
req, err := http.NewRequest(http.MethodGet, baseURL+endpoint, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// add key in the header
|
||||
req.Header.Add("Fastly-Key", key)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
responseBodyByte, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return responseBodyByte, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// captureResources try to capture all the resource that the key can access
|
||||
func captureResources(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
errAggWg sync.WaitGroup
|
||||
aggregatedErrs = make([]error, 0)
|
||||
errChan = make(chan error, 1)
|
||||
)
|
||||
|
||||
errAggWg.Add(1)
|
||||
go func() {
|
||||
defer errAggWg.Done()
|
||||
for err := range errChan {
|
||||
aggregatedErrs = append(aggregatedErrs, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// helper to launch tasks concurrently.
|
||||
launchTask := func(task func() error) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := task(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
launchTask(func() error { return captureAutomationTokens(client, key, secretInfo) })
|
||||
launchTask(func() error { return captureUserTokens(client, key, secretInfo) })
|
||||
|
||||
// capture services and their sub resources
|
||||
launchTask(func() error {
|
||||
if err := captureServices(client, key, secretInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services := secretInfo.listResourceByType(TypeService)
|
||||
for _, service := range services {
|
||||
if err := captureSvcVersions(client, key, service, secretInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// capture each version sub resources
|
||||
versions := secretInfo.listResourceByType(TypeSvcVersion)
|
||||
for _, version := range versions {
|
||||
launchTask(func() error { return captureSvcVersionACLs(client, key, version, secretInfo) })
|
||||
launchTask(func() error { return captureSvcVersionDicts(client, key, version, secretInfo) })
|
||||
launchTask(func() error { return captureSvcVersionBackends(client, key, version, secretInfo) })
|
||||
launchTask(func() error { return captureSvcVersionDomains(client, key, version, secretInfo) })
|
||||
launchTask(func() error { return captureSvcVersionHealthChecks(client, key, version, secretInfo) })
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
launchTask(func() error { return captureConfigStores(client, key, secretInfo) })
|
||||
launchTask(func() error { return captureSecretStores(client, key, secretInfo) })
|
||||
launchTask(func() error { return capturePrivateKeys(client, key, secretInfo) })
|
||||
launchTask(func() error { return captureCertificates(client, key, secretInfo) })
|
||||
launchTask(func() error { return captureTLSDomains(client, key, secretInfo) })
|
||||
launchTask(func() error { return captureInvoices(client, key, secretInfo) })
|
||||
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
errAggWg.Wait()
|
||||
|
||||
if len(aggregatedErrs) > 0 {
|
||||
return errors.Join(aggregatedErrs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// captureTokenInfo calls `/tokens/self` API and capture the token information in secretInfo
|
||||
func captureTokenInfo(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[selfToken], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var token SelfToken
|
||||
|
||||
if err := json.Unmarshal(respBody, &token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if token.ExpiresAt == "" {
|
||||
token.ExpiresAt = "never"
|
||||
}
|
||||
|
||||
secretInfo.TokenInfo = token
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized:
|
||||
return fmt.Errorf("invalid/expired api key")
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[selfToken])
|
||||
}
|
||||
}
|
||||
|
||||
// captureUserInfo calls `/current_user` API and capture the current user information in secretInfo
|
||||
func captureUserInfo(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[currentUser], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var user User
|
||||
|
||||
if err := json.Unmarshal(respBody, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretInfo.UserInfo = user
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[currentUser])
|
||||
}
|
||||
}
|
||||
|
||||
// captureUserTokens calls `/tokens` API
|
||||
func captureUserTokens(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[userTokens], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var tokens []Token
|
||||
|
||||
if err := json.Unmarshal(respBody, &tokens); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
resource := FastlyResource{
|
||||
ID: token.ID,
|
||||
Name: token.Name,
|
||||
Type: TypeUserToken,
|
||||
Metadata: map[string]string{
|
||||
"Scope": token.Scope,
|
||||
"Role": token.Role,
|
||||
"Expires At": token.ExpiresAt,
|
||||
},
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureAutomationTokens calls `/automation-tokens` API
|
||||
func captureAutomationTokens(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[automationTokens], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var tokens TokenData
|
||||
|
||||
if err := json.Unmarshal(respBody, &tokens); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens.Data {
|
||||
resource := FastlyResource{
|
||||
ID: token.ID,
|
||||
Name: token.Name,
|
||||
Type: TypeAutomationToken,
|
||||
Metadata: map[string]string{
|
||||
"Scope": token.Scope,
|
||||
"Role": token.Role,
|
||||
"Expires At": token.ExpiresAt,
|
||||
},
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureServices calls `/service` API
|
||||
func captureServices(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[service], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var services []Service
|
||||
|
||||
if err := json.Unmarshal(respBody, &services); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
resource := FastlyResource{
|
||||
ID: service.ID,
|
||||
Name: service.Name,
|
||||
Type: TypeService,
|
||||
Metadata: map[string]string{
|
||||
"Service Type": service.Type,
|
||||
},
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d for API: %s", statusCode, endpoints[service])
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersions calls `/service/<id>/version` API
|
||||
func captureSvcVersions(client *http.Client, key string, parentService FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersions], parentService.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var versions []Version
|
||||
|
||||
if err := json.Unmarshal(respBody, &versions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
resource := FastlyResource{
|
||||
ID: strconv.Itoa(version.Number),
|
||||
Name: parentService.ID + "/version/" + strconv.Itoa(version.Number), // versions has no specific name
|
||||
Type: TypeSvcVersion,
|
||||
Metadata: map[string]string{"service_id": version.ServiceID},
|
||||
Parent: &parentService,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersionACLs calls `/service/<id>/version/<number>/acl` API
|
||||
func captureSvcVersionACLs(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionACLs], parentVersion.Metadata["service_id"], parentVersion.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var acls []ACL
|
||||
|
||||
if err := json.Unmarshal(respBody, &acls); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, acl := range acls {
|
||||
resource := FastlyResource{
|
||||
ID: acl.ID,
|
||||
Name: acl.Name,
|
||||
Type: TypeSvcVersionACL,
|
||||
Parent: &parentVersion,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersionDicts calls `/service/<id>/version/<number>/dictionaries` API
|
||||
func captureSvcVersionDicts(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionDictionaries], parentVersion.Metadata["service_id"], parentVersion.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var dicts []Dictionary
|
||||
|
||||
if err := json.Unmarshal(respBody, &dicts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dict := range dicts {
|
||||
resource := FastlyResource{
|
||||
ID: dict.ID,
|
||||
Name: dict.Name,
|
||||
Type: TypeSvcVersionDict,
|
||||
Parent: &parentVersion,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersionBackends calls `/service/<id>/version/<number>/backend` API
|
||||
func captureSvcVersionBackends(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionBackends], parentVersion.Metadata["service_id"], parentVersion.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var backends []Backend
|
||||
|
||||
if err := json.Unmarshal(respBody, &backends); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, backend := range backends {
|
||||
resource := FastlyResource{
|
||||
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/backend/" + backend.Name, // no specific ID
|
||||
Name: backend.Name,
|
||||
Type: TypeSvcVersionBackend,
|
||||
Parent: &parentVersion,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersionDomains calls `/service/<id>/version/<number>/domain` API
|
||||
func captureSvcVersionDomains(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionDomains], parentVersion.Metadata["service_id"], parentVersion.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var domains []Domain
|
||||
|
||||
if err := json.Unmarshal(respBody, &domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
resource := FastlyResource{
|
||||
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/domain/" + domain.Name, // no specific ID
|
||||
Name: domain.Name,
|
||||
Type: TypeSvcVersionDomain,
|
||||
Parent: &parentVersion,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSvcVersionHealthChecks calls `/service/<id>/version/<number>/healthcheck` API
|
||||
func captureSvcVersionHealthChecks(client *http.Client, key string, parentVersion FastlyResource, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, fmt.Sprintf(endpoints[serviceVersionHealthChecks], parentVersion.Metadata["service_id"], parentVersion.ID), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var healthChecks []HealthCheck
|
||||
|
||||
if err := json.Unmarshal(respBody, &healthChecks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, healthCheck := range healthChecks {
|
||||
resource := FastlyResource{
|
||||
ID: parentVersion.Metadata["service_id"] + "/version/" + parentVersion.ID + "/healthcheck/" + healthCheck.Name, // no specific ID
|
||||
Name: healthCheck.Name,
|
||||
Type: TypeSvcVersionHealthCheck,
|
||||
Parent: &parentVersion,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureConfigStores calls `/resources/stores/config` API
|
||||
func captureConfigStores(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[configStores], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var configs []ConfigStore
|
||||
|
||||
if err := json.Unmarshal(respBody, &configs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
resource := FastlyResource{
|
||||
ID: config.ID,
|
||||
Name: config.Name,
|
||||
Type: TypeConfigStore,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureSecretStores calls `/resources/stores/secret` API
|
||||
func captureSecretStores(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[secretStores], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var secretStores SecretStoreData
|
||||
|
||||
if err := json.Unmarshal(respBody, &secretStores); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, secret := range secretStores.Data {
|
||||
resource := FastlyResource{
|
||||
ID: secret.ID,
|
||||
Name: secret.Name,
|
||||
Type: TypeSecretStore,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// capturePrivateKeys calls `/tls/private_keys` API
|
||||
func capturePrivateKeys(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsPrivateKeys], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var privateKeys TLSPrivateKeyData
|
||||
|
||||
if err := json.Unmarshal(respBody, &privateKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, privateKey := range privateKeys.Data {
|
||||
resource := FastlyResource{
|
||||
ID: privateKey.ID,
|
||||
Name: privateKey.Name,
|
||||
Type: TypeTLSPrivateKey,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureCertificates calls `/tls/certificates` API
|
||||
func captureCertificates(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsCertificates], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var certData TLSCertificatesData
|
||||
|
||||
if err := json.Unmarshal(respBody, &certData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cert := range certData.Data {
|
||||
resource := FastlyResource{
|
||||
ID: cert.ID,
|
||||
Name: cert.Name,
|
||||
Type: TypeTLSCertificate,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureTLSDomains calls `/tls/domains` API
|
||||
func captureTLSDomains(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[tlsDomains], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var domainData TLSDomainsData
|
||||
|
||||
if err := json.Unmarshal(respBody, &domainData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, domain := range domainData.Data {
|
||||
resource := FastlyResource{
|
||||
ID: domain.ID,
|
||||
Name: domain.ID,
|
||||
Type: TypeTLSDomain,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureInvoices calls `/billing/v3/invoices` API
|
||||
func captureInvoices(client *http.Client, key string, secretInfo *SecretInfo) error {
|
||||
respBody, statusCode, err := makeFastlyRequest(client, endpoints[invoices], key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var invoices InvoicesData
|
||||
|
||||
if err := json.Unmarshal(respBody, &invoices); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, invoice := range invoices.Data {
|
||||
resource := FastlyResource{
|
||||
ID: invoice.CustomerID + "/region/" + invoice.Region + "/statement/" + invoice.StatementNo + "/invoice/" + invoice.ID,
|
||||
Name: invoice.ID, // no specific name
|
||||
Type: TypeInvoice,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"AnalyzerType": 34,
|
||||
"Bindings": [
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "test",
|
||||
"FullyQualifiedName": "Config Store/Q9uDqi7ODnLUrhMFifFVT4",
|
||||
"Type": "Config Store",
|
||||
"Metadata": {},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "centrally-decent-lynx.edgecompute.app",
|
||||
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/1/domain/centrally-decent-lynx.edgecompute.app",
|
||||
"Type": "Service Version Domain",
|
||||
"Metadata": {},
|
||||
"Parent": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/1",
|
||||
"FullyQualifiedName": "Service Version/1",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "centrally-decent-lynx.edgecompute.app",
|
||||
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/2/domain/centrally-decent-lynx.edgecompute.app",
|
||||
"Type": "Service Version Domain",
|
||||
"Metadata": {},
|
||||
"Parent": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/2",
|
||||
"FullyQualifiedName": "Service Version/2",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "centrally-decent-lynx.edgecompute.app",
|
||||
"FullyQualifiedName": "Service Version Domain/vInh5jJ0qnGdhiCO04INR7/version/3/domain/centrally-decent-lynx.edgecompute.app",
|
||||
"Type": "Service Version Domain",
|
||||
"Metadata": {},
|
||||
"Parent": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
|
||||
"FullyQualifiedName": "Service Version/3",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Detectors",
|
||||
"FullyQualifiedName": "Service Version Health Check/vInh5jJ0qnGdhiCO04INR7/version/3/healthcheck/Detectors",
|
||||
"Type": "Service Version Health Check",
|
||||
"Metadata": {},
|
||||
"Parent": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
|
||||
"FullyQualifiedName": "Service Version/3",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "yja0K1GNPRDNTA6vizIFK4/version/1",
|
||||
"FullyQualifiedName": "Service Version/1",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "yja0K1GNPRDNTA6vizIFK4"
|
||||
},
|
||||
"Parent": {
|
||||
"Name": "Truffle Security's website",
|
||||
"FullyQualifiedName": "Service/yja0K1GNPRDNTA6vizIFK4",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "vcl"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/1",
|
||||
"FullyQualifiedName": "Service Version/1",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": {
|
||||
"Name": "this is a test service",
|
||||
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "wasm"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/2",
|
||||
"FullyQualifiedName": "Service Version/2",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": {
|
||||
"Name": "this is a test service",
|
||||
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "wasm"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "vInh5jJ0qnGdhiCO04INR7/version/3",
|
||||
"FullyQualifiedName": "Service Version/3",
|
||||
"Type": "Service Version",
|
||||
"Metadata": {
|
||||
"service_id": "vInh5jJ0qnGdhiCO04INR7"
|
||||
},
|
||||
"Parent": {
|
||||
"Name": "this is a test service",
|
||||
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "wasm"
|
||||
},
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "this is a test service",
|
||||
"FullyQualifiedName": "Service/vInh5jJ0qnGdhiCO04INR7",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "wasm"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "Truffle Security's website",
|
||||
"FullyQualifiedName": "Service/yja0K1GNPRDNTA6vizIFK4",
|
||||
"Type": "Service",
|
||||
"Metadata": {
|
||||
"Service Type": "vcl"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "test-user-global",
|
||||
"FullyQualifiedName": "User Token/24K13teXo9GhmaUGhwBS2V",
|
||||
"Type": "User Token",
|
||||
"Metadata": {
|
||||
"Expires At": "2025-12-31T19:00:00Z",
|
||||
"Role": "",
|
||||
"Scope": "global"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "test-user-purge-select",
|
||||
"FullyQualifiedName": "User Token/2782vHUyFqralr1GKmWmVF",
|
||||
"Type": "User Token",
|
||||
"Metadata": {
|
||||
"Expires At": "",
|
||||
"Role": "",
|
||||
"Scope": "purge_select"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "test",
|
||||
"FullyQualifiedName": "User Token/278C9jIudzPv9NC6BvZT4z",
|
||||
"Type": "User Token",
|
||||
"Metadata": {
|
||||
"Expires At": "2025-07-22T19:00:00Z",
|
||||
"Role": "",
|
||||
"Scope": "global:read global"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Resource": {
|
||||
"Name": "integration-test",
|
||||
"FullyQualifiedName": "User Token/2ICO7ArmhY8OMiiOyNpXfc",
|
||||
"Type": "User Token",
|
||||
"Metadata": {
|
||||
"Expires At": "",
|
||||
"Role": "",
|
||||
"Scope": "global:read global"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
"Value": "global:read global",
|
||||
"Parent": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"UnboundedResources": null,
|
||||
"Metadata": {}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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/elevenlabs"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/fastly"
|
||||
"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/gitlab"
|
||||
@@ -124,5 +125,7 @@ func Run(keyType string, secretInfo SecretInfo) {
|
||||
plaid.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["secret"], secretInfo.Parts["id"], secretInfo.Parts["token"])
|
||||
case "netlify":
|
||||
netlify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "fastly":
|
||||
fastly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
s1.Verified = verified
|
||||
s1.ExtraData = extraData
|
||||
s1.SetVerificationError(verificationErr, match)
|
||||
|
||||
if s1.Verified {
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": match,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
@@ -51,10 +52,10 @@ func TestFastlyPersonalToken_FromChunk(t *testing.T) {
|
||||
DetectorType: detectorspb.DetectorType_FastlyPersonalToken,
|
||||
Verified: true,
|
||||
ExtraData: map[string]string{
|
||||
"token_id": "2GUTBVFzHG2zVOMGtEpi9q",
|
||||
"user_id": "2j1UhHmRhefRMNNrlxcyf5",
|
||||
"token_id": "2ICO7ArmhY8OMiiOyNpXfc",
|
||||
"user_id": "7anDA1ct17E8pkFAE0tJkk",
|
||||
"token_expires_at": "never",
|
||||
"token_scope": "global:read",
|
||||
"token_scope": "global:read global",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -72,7 +73,7 @@ func TestFastlyPersonalToken_FromChunk(t *testing.T) {
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_FastlyPersonalToken,
|
||||
Verified: false,
|
||||
ExtraData: map[string]string{},
|
||||
ExtraData: nil,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -91,8 +92,7 @@ func TestFastlyPersonalToken_FromChunk(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := Scanner{}
|
||||
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("FastlyPersonalToken.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -101,9 +101,9 @@ func TestFastlyPersonalToken_FromChunk(t *testing.T) {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("FastlyPersonalToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user