Merge branch 'main' into draft-rate-limiter
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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package azuredirectmanagementkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
const RFC3339WithoutMicroseconds = "2006-01-02T15:04:05"
|
||||
|
||||
type Scanner struct {
|
||||
client *http.Client
|
||||
detectors.DefaultMultiPartCredentialProvider
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
urlPat = regexp.MustCompile(`https://([a-z0-9][a-z0-9-]{0,48}[a-z0-9])\.management\.azure-api\.net`) // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.Name/
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", ".management.azure-api.net"}) + `([a-zA-Z0-9+\/]{83,85}[a-zA-Z0-9]==)`) // pattern for both Primary and secondary key
|
||||
|
||||
invalidHosts = simple.NewCache[struct{}]()
|
||||
noSuchHostErr = errors.New("no such host")
|
||||
)
|
||||
|
||||
// Keywords are used for efficiently pre-filtering chunks.
|
||||
// Use identifiers in the secret preferably, or the provider name.
|
||||
func (s Scanner) Keywords() []string {
|
||||
return []string{".management.azure-api.net"}
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Azure Management API keys in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
logger := logContext.AddLogger(ctx).Logger().WithName("azuredirectmanagementkey")
|
||||
dataStr := string(data)
|
||||
|
||||
urlMatchesUnique := make(map[string]string)
|
||||
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
urlMatchesUnique[urlMatch[0]] = urlMatch[1] // urlMatch[0] is the full url, urlMatch[1] is the service name
|
||||
}
|
||||
keyMatchesUnique := make(map[string]struct{})
|
||||
for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
keyMatchesUnique[strings.TrimSpace(keyMatch[1])] = struct{}{}
|
||||
}
|
||||
|
||||
EndpointLoop:
|
||||
for baseUrl, serviceName := range urlMatchesUnique {
|
||||
for key := range keyMatchesUnique {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
|
||||
Raw: []byte(baseUrl),
|
||||
RawV2: []byte(baseUrl + ":" + key),
|
||||
}
|
||||
|
||||
if verify {
|
||||
if invalidHosts.Exists(baseUrl) {
|
||||
logger.V(3).Info("Skipping invalid registry", "baseUrl", baseUrl)
|
||||
continue EndpointLoop
|
||||
}
|
||||
|
||||
client := s.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
|
||||
isVerified, verificationErr := s.verifyMatch(ctx, client, baseUrl, serviceName, key)
|
||||
s1.Verified = isVerified
|
||||
if verificationErr != nil {
|
||||
if errors.Is(verificationErr, noSuchHostErr) {
|
||||
invalidHosts.Set(baseUrl, struct{}{})
|
||||
continue EndpointLoop
|
||||
}
|
||||
s1.SetVerificationError(verificationErr, baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_AzureDirectManagementKey
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Azure API Management provides a direct management REST API for performing operations on selected entities, such as users, groups, products, and subscriptions."
|
||||
}
|
||||
|
||||
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, baseUrl, serviceName, key string) (bool, error) {
|
||||
url := fmt.Sprintf(
|
||||
"%s/subscriptions/default/resourceGroups/default/providers/Microsoft.ApiManagement/service/%s/apis?api-version=2024-05-01",
|
||||
baseUrl, serviceName,
|
||||
)
|
||||
accessToken, err := generateAccessToken(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("SharedAccessSignature %s", accessToken))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/rest/api/apimanagement/apimanagementrest/azure-api-management-rest-api-authentication
|
||||
func generateAccessToken(key string) (string, error) {
|
||||
expiry := time.Now().UTC().Add(5 * time.Second).Format(RFC3339WithoutMicroseconds) // expire in 5 seconds
|
||||
expiry = expiry + ".0000000Z" // 7 decimals microsecond's precision is must for access token
|
||||
|
||||
// Construct the string-to-sign
|
||||
stringToSign := fmt.Sprintf("integration\n%s", expiry)
|
||||
|
||||
// Generate HMAC-SHA512 signature
|
||||
h := hmac.New(sha512.New, []byte(key))
|
||||
h.Write([]byte(stringToSign))
|
||||
signature := h.Sum(nil)
|
||||
|
||||
// Base64 encode the signature
|
||||
encodedSignature := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
// Create the access token
|
||||
accessToken := fmt.Sprintf("uid=integration&ex=%s&sn=%s", expiry, encodedSignature)
|
||||
return accessToken, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package azuredirectmanagementkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
func TestAzureDirectManagementAPIKey_FromChunk(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)
|
||||
}
|
||||
url := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_URL")
|
||||
secret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_KEY")
|
||||
inactiveSecret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_KEY_INACTIVE")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
data: []byte(fmt.Sprintf("You can find a azure management api url %s and key %s within", url, secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
data: []byte(fmt.Sprintf("You can find a azure management api url %s and key %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AzureDirectManagementAPIKey.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
}
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("AzureDirectManagementAPIKey.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromData(benchmark *testing.B) {
|
||||
ctx := context.Background()
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package azuredirectmanagementkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = `
|
||||
AZURE_MANGEMENT_API_KEY=UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKpBqPeppTHCharbaMeKqKs/H4gA/go1w==
|
||||
AZURE_MANAGEMENT_API_URL=https://trufflesecuritytest.management.azure-api.net
|
||||
`
|
||||
invalidPattern = `
|
||||
AZURE_MANGEMENT_API_KEY=UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKp
|
||||
AZURE_MANAGEMENT_API_URL=https://trufflesecuritytest.management.azure-api.net
|
||||
`
|
||||
)
|
||||
|
||||
func TestAzureDirectManagementAPIKey_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern",
|
||||
input: validPattern,
|
||||
want: []string{"https://trufflesecuritytest.management.azure-api.net:UJh1Wn7txjls2GPK1YxO9+3tpqQffSfxb+97PmT8j3cSQoXvGa74lCKpBqPeppTHCharbaMeKqKs/H4gA/go1w=="},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: invalidPattern,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
|
||||
if len(matchedDetectors) == 0 {
|
||||
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
|
||||
return
|
||||
}
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
if err != nil {
|
||||
t.Errorf("error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) != len(test.want) {
|
||||
if len(results) == 0 {
|
||||
t.Errorf("did not receive result")
|
||||
} else {
|
||||
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
actual := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
if len(r.RawV2) > 0 {
|
||||
actual[string(r.RawV2)] = struct{}{}
|
||||
} else {
|
||||
actual[string(r.Raw)] = struct{}{}
|
||||
}
|
||||
}
|
||||
expected := make(map[string]struct{}, len(test.want))
|
||||
for _, v := range test.want {
|
||||
expected[v] = struct{}{}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,6 +77,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azureapimanagementsubscriptionkey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azurecontainerregistry"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuredevopspersonalaccesstoken"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuredirectmanagementkey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresastoken"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresearchadminkey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresearchquerykey"
|
||||
@@ -916,6 +917,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&azure_cosmosdb.Scanner{},
|
||||
&azurecontainerregistry.Scanner{},
|
||||
&azuredevopspersonalaccesstoken.Scanner{},
|
||||
&azuredirectmanagementkey.Scanner{},
|
||||
// &azurefunctionkey.Scanner{}, // detector is throwing some FPs
|
||||
&azure_openai.Scanner{},
|
||||
&azuresastoken.Scanner{},
|
||||
|
||||
@@ -1128,6 +1128,7 @@ const (
|
||||
DetectorType_Langfuse DetectorType = 1021
|
||||
DetectorType_BingSubscriptionKey DetectorType = 1022
|
||||
DetectorType_XAI DetectorType = 1023
|
||||
DetectorType_AzureDirectManagementKey DetectorType = 1024
|
||||
)
|
||||
|
||||
// Enum value maps for DetectorType.
|
||||
@@ -2153,6 +2154,7 @@ var (
|
||||
1021: "Langfuse",
|
||||
1022: "BingSubscriptionKey",
|
||||
1023: "XAI",
|
||||
1024: "AzureDirectManagementKey",
|
||||
}
|
||||
DetectorType_value = map[string]int32{
|
||||
"Alibaba": 0,
|
||||
@@ -3175,6 +3177,7 @@ var (
|
||||
"Langfuse": 1021,
|
||||
"BingSubscriptionKey": 1022,
|
||||
"XAI": 1023,
|
||||
"AzureDirectManagementKey": 1024,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3628,7 +3631,7 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x4c, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x54, 0x46, 0x31, 0x36, 0x10, 0x03, 0x12, 0x13, 0x0a,
|
||||
0x0f, 0x45, 0x53, 0x43, 0x41, 0x50, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x49, 0x43, 0x4f, 0x44, 0x45,
|
||||
0x10, 0x04, 0x2a, 0xdc, 0x83, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x10, 0x04, 0x2a, 0xfb, 0x83, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10,
|
||||
0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41,
|
||||
0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10, 0x03, 0x12,
|
||||
@@ -4682,11 +4685,13 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x0d, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x66, 0x75, 0x73, 0x65, 0x10, 0xfd, 0x07, 0x12, 0x18,
|
||||
0x0a, 0x13, 0x42, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfe, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x58, 0x41, 0x49, 0x10,
|
||||
0xff, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79,
|
||||
0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70,
|
||||
0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70,
|
||||
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0xff, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63,
|
||||
0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x10, 0x80,
|
||||
0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
||||
0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f,
|
||||
0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b,
|
||||
0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -152,7 +152,7 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
|
||||
if err = json.Unmarshal(contents, &env); err != nil {
|
||||
return err
|
||||
}
|
||||
s.scanVariableData(ctx, chunksChan, Metadata{EnvironmentID: env.ID, EnvironmentName: env.Name, fromLocal: true, Link: envPath, LocationType: source_metadatapb.PostmanLocationType_ENVIRONMENT_VARIABLE}, env)
|
||||
s.scanVariableData(ctx, chunksChan, Metadata{EnvironmentID: env.Id, EnvironmentName: env.Name, fromLocal: true, Link: envPath, LocationType: source_metadatapb.PostmanLocationType_ENVIRONMENT_VARIABLE}, env)
|
||||
}
|
||||
|
||||
// Scan local collections
|
||||
@@ -180,7 +180,7 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
|
||||
}
|
||||
}
|
||||
basename := path.Base(workspacePath)
|
||||
workspace.ID = strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
workspace.Id = strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
s.scanLocalWorkspace(ctx, chunksChan, workspace, workspacePath)
|
||||
}
|
||||
|
||||
@@ -225,9 +225,9 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
|
||||
}
|
||||
ctx.Logger().V(2).Info("enumerated workspaces", "workspaces", workspaces)
|
||||
for _, workspace := range workspaces {
|
||||
s.SetProgressOngoing(fmt.Sprintf("Scanning workspace %s", workspace.ID), "")
|
||||
s.SetProgressOngoing(fmt.Sprintf("Scanning workspace %s", workspace.Id), "")
|
||||
if err = s.scanWorkspace(ctx, chunksChan, workspace); err != nil {
|
||||
return fmt.Errorf("error scanning workspace %s: %w", workspace.ID, err)
|
||||
return fmt.Errorf("error scanning workspace %s: %w", workspace.Id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,12 +241,12 @@ func (s *Source) scanLocalWorkspace(ctx context.Context, chunksChan chan *source
|
||||
s.resetKeywords()
|
||||
|
||||
metadata := Metadata{
|
||||
WorkspaceUUID: workspace.ID,
|
||||
WorkspaceUUID: workspace.Id,
|
||||
fromLocal: true,
|
||||
}
|
||||
|
||||
for _, environment := range workspace.EnvironmentsRaw {
|
||||
metadata.Link = strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) + "/environments/" + environment.ID + ".json"
|
||||
metadata.Link = strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) + "/environments/" + environment.Id + ".json"
|
||||
metadata.LocationType = source_metadatapb.PostmanLocationType_ENVIRONMENT_VARIABLE
|
||||
s.scanVariableData(ctx, chunksChan, metadata, environment)
|
||||
metadata.LocationType = source_metadatapb.PostmanLocationType_UNKNOWN_POSTMAN
|
||||
@@ -264,7 +264,7 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
|
||||
|
||||
// initiate metadata to track the tree structure of postman data
|
||||
metadata := Metadata{
|
||||
WorkspaceUUID: workspace.ID,
|
||||
WorkspaceUUID: workspace.Id,
|
||||
WorkspaceName: workspace.Name,
|
||||
CreatedBy: workspace.CreatedBy,
|
||||
Type: "workspace",
|
||||
@@ -272,18 +272,18 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
|
||||
|
||||
// gather and scan environment variables
|
||||
for _, envID := range workspace.Environments {
|
||||
envVars, err := s.client.GetEnvironmentVariables(ctx, envID.UUID)
|
||||
envVars, err := s.client.GetEnvironmentVariables(ctx, envID.Uid)
|
||||
if err != nil {
|
||||
ctx.Logger().Error(err, "could not get env variables", "environment_uuid", envID.UUID)
|
||||
ctx.Logger().Error(err, "could not get env variables", "environment_uuid", envID.Uid)
|
||||
continue
|
||||
}
|
||||
if shouldSkip(envID.UUID, s.conn.IncludeEnvironments, s.conn.ExcludeEnvironments) {
|
||||
if shouldSkip(envID.Uid, s.conn.IncludeEnvironments, s.conn.ExcludeEnvironments) {
|
||||
continue
|
||||
}
|
||||
metadata.Type = ENVIRONMENT_TYPE
|
||||
metadata.Link = LINK_BASE_URL + "environments/" + envID.UUID
|
||||
metadata.FullID = envVars.ID
|
||||
metadata.EnvironmentID = envID.UUID
|
||||
metadata.Link = LINK_BASE_URL + "environments/" + envID.Uid
|
||||
metadata.FullID = envVars.Id
|
||||
metadata.EnvironmentID = envID.Uid
|
||||
metadata.EnvironmentName = envVars.Name
|
||||
|
||||
ctx.Logger().V(2).Info("scanning environment vars", "environment_uuid", metadata.FullID)
|
||||
@@ -306,10 +306,10 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
|
||||
// at this point we have all the possible
|
||||
// substitutions from Environment variables
|
||||
for _, collectionID := range workspace.Collections {
|
||||
if shouldSkip(collectionID.UUID, s.conn.IncludeCollections, s.conn.ExcludeCollections) {
|
||||
if shouldSkip(collectionID.Uid, s.conn.IncludeCollections, s.conn.ExcludeCollections) {
|
||||
continue
|
||||
}
|
||||
collection, err := s.client.GetCollection(ctx, collectionID.UUID)
|
||||
collection, err := s.client.GetCollection(ctx, collectionID.Uid)
|
||||
if err != nil {
|
||||
// Log and move on, because sometimes the Postman API seems to give us collection IDs
|
||||
// that we don't have access to, so we don't want to kill the scan because of it.
|
||||
@@ -324,13 +324,13 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
|
||||
// scanCollection scans a collection and all its items, folders, and requests.
|
||||
// locally scoped Metadata is updated as we drill down into the collection.
|
||||
func (s *Source) scanCollection(ctx context.Context, chunksChan chan *sources.Chunk, metadata Metadata, collection Collection) {
|
||||
ctx.Logger().V(2).Info("starting to scan collection", "collection_name", collection.Info.Name, "collection_uuid", collection.Info.UID)
|
||||
ctx.Logger().V(2).Info("starting to scan collection", "collection_name", collection.Info.Name, "collection_uuid", collection.Info.Uid)
|
||||
metadata.CollectionInfo = collection.Info
|
||||
metadata.Type = COLLECTION_TYPE
|
||||
s.attemptToAddKeyword(collection.Info.Name)
|
||||
|
||||
if !metadata.fromLocal {
|
||||
metadata.FullID = metadata.CollectionInfo.UID
|
||||
metadata.FullID = metadata.CollectionInfo.Uid
|
||||
metadata.Link = LINK_BASE_URL + COLLECTION_TYPE + "/" + metadata.FullID
|
||||
}
|
||||
|
||||
@@ -367,20 +367,20 @@ func (s *Source) scanItem(ctx context.Context, chunksChan chan *sources.Chunk, c
|
||||
metadata.FolderName = item.Name
|
||||
}
|
||||
|
||||
if item.UID != "" {
|
||||
metadata.FullID = item.UID
|
||||
if item.Uid != "" {
|
||||
metadata.FullID = item.Uid
|
||||
metadata.Link = LINK_BASE_URL + FOLDER_TYPE + "/" + metadata.FullID
|
||||
}
|
||||
// recurse through the folders
|
||||
for _, subItem := range item.Items {
|
||||
s.scanItem(ctx, chunksChan, collection, metadata, subItem, item.UID)
|
||||
s.scanItem(ctx, chunksChan, collection, metadata, subItem, item.Uid)
|
||||
}
|
||||
|
||||
// The assignment of the folder ID to be the current item UID is due to wanting to assume that your current item is a folder unless you have request data inside of your item.
|
||||
// If your current item is a folder, you will want the folder ID to match the UID of the current item.
|
||||
// If your current item is a request, you will want the folder ID to match the UID of the parent folder.
|
||||
// If the request is at the root of a collection and has no parent folder, the folder ID will be empty.
|
||||
metadata.FolderID = item.UID
|
||||
metadata.FolderID = item.Uid
|
||||
// check if there are any requests in the folder
|
||||
if item.Request.Method != "" {
|
||||
metadata.FolderName = strings.Replace(metadata.FolderName, (" > " + item.Name), "", -1)
|
||||
@@ -388,16 +388,16 @@ func (s *Source) scanItem(ctx context.Context, chunksChan chan *sources.Chunk, c
|
||||
if metadata.FolderID == "" {
|
||||
metadata.FolderName = ""
|
||||
}
|
||||
metadata.RequestID = item.UID
|
||||
metadata.RequestID = item.Uid
|
||||
metadata.RequestName = item.Name
|
||||
metadata.Type = REQUEST_TYPE
|
||||
if item.UID != "" {
|
||||
if item.Uid != "" {
|
||||
// Route to API endpoint
|
||||
metadata.FullID = item.UID
|
||||
metadata.Link = LINK_BASE_URL + REQUEST_TYPE + "/" + item.UID
|
||||
metadata.FullID = item.Uid
|
||||
metadata.Link = LINK_BASE_URL + REQUEST_TYPE + "/" + item.Uid
|
||||
} else {
|
||||
// Route to collection.json
|
||||
metadata.FullID = item.ID
|
||||
metadata.FullID = item.Id
|
||||
}
|
||||
s.scanHTTPRequest(ctx, chunksChan, metadata, item.Request)
|
||||
}
|
||||
@@ -630,9 +630,9 @@ func (s *Source) scanRequestBody(ctx context.Context, chunksChan chan *sources.C
|
||||
}
|
||||
|
||||
func (s *Source) scanHTTPResponse(ctx context.Context, chunksChan chan *sources.Chunk, m Metadata, response Response) {
|
||||
if response.UID != "" {
|
||||
m.Link = LINK_BASE_URL + "example/" + response.UID
|
||||
m.FullID = response.UID
|
||||
if response.Uid != "" {
|
||||
m.Link = LINK_BASE_URL + "example/" + response.Uid
|
||||
m.FullID = response.Uid
|
||||
}
|
||||
originalType := m.Type
|
||||
|
||||
@@ -726,7 +726,7 @@ func (s *Source) scanData(ctx context.Context, chunksChan chan *sources.Chunk, d
|
||||
Link: metadata.Link,
|
||||
WorkspaceUuid: metadata.WorkspaceUUID,
|
||||
WorkspaceName: metadata.WorkspaceName,
|
||||
CollectionId: metadata.CollectionInfo.UID,
|
||||
CollectionId: metadata.CollectionInfo.Uid,
|
||||
CollectionName: metadata.CollectionInfo.Name,
|
||||
EnvironmentId: metadata.EnvironmentID,
|
||||
EnvironmentName: metadata.EnvironmentName,
|
||||
|
||||
@@ -22,25 +22,25 @@ const (
|
||||
)
|
||||
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Collections []IDNameUUID `json:"collections"`
|
||||
Environments []IDNameUUID `json:"environments"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Collections []IdNameUid `json:"collections"`
|
||||
Environments []IdNameUid `json:"environments"`
|
||||
CollectionsRaw []Collection
|
||||
EnvironmentsRaw []VariableData
|
||||
}
|
||||
|
||||
type IDNameUUID struct {
|
||||
ID string `json:"id"`
|
||||
type IdNameUid struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uid"`
|
||||
Uid string `json:"uid"`
|
||||
}
|
||||
|
||||
type KeyValue struct {
|
||||
@@ -53,7 +53,7 @@ type KeyValue struct {
|
||||
}
|
||||
|
||||
type VariableData struct {
|
||||
ID string `json:"id"` // For globals and envs, this is just the UUID, not the full ID.
|
||||
Id string `json:"id"` // For globals and envs, this is just the UUID, not the full ID.
|
||||
Name string `json:"name"`
|
||||
KeyValues []KeyValue `json:"values"`
|
||||
Owner string `json:"owner"`
|
||||
@@ -99,20 +99,20 @@ type Info struct {
|
||||
Description string `json:"description"`
|
||||
Schema string `json:"schema"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UID string `json:"uid"` //Need to use this to get the collection via API
|
||||
Uid string `json:"uid"` //Need to use this to get the collection via API
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Items []Item `json:"item,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Auth Auth `json:"auth,omitempty"`
|
||||
Events []Event `json:"event,omitempty"`
|
||||
Variable []KeyValue `json:"variable,omitempty"`
|
||||
Request Request `json:"request,omitempty"`
|
||||
Response []Response `json:"response,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
UID string `json:"uid,omitempty"` //Need to use this to get the collection via API. The UID is a concatenation of the ID and the user ID of whoever created the item.
|
||||
Uid string `json:"uid,omitempty"` //Need to use this to get the collection via API. The UID is a concatenation of the ID and the user ID of whoever created the item.
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
@@ -173,14 +173,14 @@ type URL struct {
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
OriginalRequest Request `json:"originalRequest,omitempty"`
|
||||
HeaderRaw json.RawMessage `json:"header,omitempty"`
|
||||
HeaderKeyValue []KeyValue
|
||||
HeaderString []string
|
||||
Body string `json:"body,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Uid string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
// A Client manages communication with the Postman API.
|
||||
@@ -318,11 +318,11 @@ func (c *Client) EnumerateWorkspaces(ctx context.Context) ([]Workspace, error) {
|
||||
}
|
||||
|
||||
for i, workspace := range workspacesObj.Workspaces {
|
||||
tempWorkspace, err := c.GetWorkspace(ctx, workspace.ID)
|
||||
tempWorkspace, err := c.GetWorkspace(ctx, workspace.Id)
|
||||
if err != nil {
|
||||
// Log and move on, because sometimes the Postman API seems to give us workspace IDs
|
||||
// that we don't have access to, so we don't want to kill the scan because of it.
|
||||
ctx.Logger().Error(err, "could not get workspace %q (%s) during enumeration", workspace.Name, workspace.ID)
|
||||
ctx.Logger().Error(err, "could not get workspace %q (%s) during enumeration", workspace.Name, workspace.Id)
|
||||
continue
|
||||
}
|
||||
workspacesObj.Workspaces[i] = tempWorkspace
|
||||
|
||||
@@ -1033,6 +1033,7 @@ enum DetectorType {
|
||||
Langfuse = 1021;
|
||||
BingSubscriptionKey = 1022;
|
||||
XAI = 1023;
|
||||
AzureDirectManagementKey = 1024;
|
||||
}
|
||||
|
||||
message Result {
|
||||
|
||||
Reference in New Issue
Block a user