Files
trufflehog/pkg/analyzer/analyzers/fastly/requests.go
Kashif Khan 08b7e3b4a9 Fastly Analyzer (#4082)
* initial commit

* added more apis and test cases

* fixed test cases

* added more apis

* fixed analyzer type

* Update launchdarkly_test.go
2025-05-08 12:59:16 +05:00

745 lines
20 KiB
Go

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)
}
}