mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-10 09:41:25 -10:00
722 lines
20 KiB
Go
722 lines
20 KiB
Go
package cloudrunservice
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
|
|
"github.com/BishopFox/cloudfox/internal/gcp/sdk"
|
|
regionservice "github.com/BishopFox/cloudfox/gcp/services/regionService"
|
|
run "google.golang.org/api/run/v2"
|
|
)
|
|
|
|
type CloudRunService struct{
|
|
session *gcpinternal.SafeSession
|
|
}
|
|
|
|
func New() *CloudRunService {
|
|
return &CloudRunService{}
|
|
}
|
|
|
|
func NewWithSession(session *gcpinternal.SafeSession) *CloudRunService {
|
|
return &CloudRunService{
|
|
session: session,
|
|
}
|
|
}
|
|
|
|
func (crs *CloudRunService) getServiceV2(ctx context.Context) (*run.Service, error) {
|
|
if crs.session != nil {
|
|
return sdk.CachedGetCloudRunServiceV2(ctx, crs.session)
|
|
}
|
|
return run.NewService(ctx)
|
|
}
|
|
|
|
// ServiceInfo holds Cloud Run service details with security-relevant information
|
|
type ServiceInfo struct {
|
|
// Basic info
|
|
Name string
|
|
ProjectID string
|
|
Region string
|
|
Description string
|
|
Creator string
|
|
UpdateTime string
|
|
|
|
// URL and traffic
|
|
URL string
|
|
LatestRevision string
|
|
LatestReadyRevision string
|
|
TrafficAllOnLatest bool
|
|
|
|
// Security-relevant configuration
|
|
ServiceAccount string
|
|
IngressSettings string // INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_ONLY, INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER
|
|
VPCAccess string // VPC Connector or Direct VPC
|
|
VPCEgressSettings string // ALL_TRAFFIC, PRIVATE_RANGES_ONLY
|
|
BinaryAuthorizationPolicy string
|
|
|
|
// Container configuration
|
|
ContainerImage string
|
|
ContainerPort int64
|
|
CPULimit string
|
|
MemoryLimit string
|
|
MaxInstances int64
|
|
MinInstances int64
|
|
Timeout string
|
|
|
|
// Environment variables (counts, not values)
|
|
EnvVarCount int
|
|
SecretEnvVarCount int
|
|
SecretVolumeCount int
|
|
|
|
// Security analysis
|
|
HardcodedSecrets []HardcodedSecret // Potential secrets in env vars (not using Secret Manager)
|
|
UsesDefaultSA bool // Uses default compute service account
|
|
|
|
// Detailed env var and secret info
|
|
EnvVars []EnvVarInfo // All environment variables
|
|
SecretRefs []SecretRefInfo // All Secret Manager references
|
|
|
|
// IAM
|
|
InvokerMembers []string
|
|
IsPublic bool
|
|
IAMBindings []IAMBinding // All IAM bindings on this service
|
|
|
|
// Status
|
|
Status string // Service status
|
|
}
|
|
|
|
// HardcodedSecret represents a potential secret found in environment variables
|
|
type HardcodedSecret struct {
|
|
EnvVarName string
|
|
SecretType string // password, api-key, token, credential, connection-string
|
|
}
|
|
|
|
// EnvVarInfo represents an environment variable configuration
|
|
type EnvVarInfo struct {
|
|
Name string
|
|
Value string // Direct value (may be empty if using secret ref)
|
|
Source string // "direct", "secret-manager", or "config-map"
|
|
// For Secret Manager references
|
|
SecretName string
|
|
SecretVersion string
|
|
}
|
|
|
|
// SecretRefInfo represents a Secret Manager reference used by the service
|
|
type SecretRefInfo struct {
|
|
EnvVarName string // The env var name that references this secret
|
|
SecretName string // Secret Manager secret name
|
|
SecretVersion string // Version (e.g., "latest", "1")
|
|
MountPath string // For volume mounts, the path where it's mounted
|
|
Type string // "env" or "volume"
|
|
}
|
|
|
|
// IAMBinding represents a single IAM role binding
|
|
type IAMBinding struct {
|
|
Role string
|
|
Member string
|
|
}
|
|
|
|
// JobInfo holds Cloud Run job details
|
|
type JobInfo struct {
|
|
Name string
|
|
ProjectID string
|
|
Region string
|
|
ServiceAccount string
|
|
ContainerImage string
|
|
LastExecution string
|
|
Creator string
|
|
UpdateTime string
|
|
|
|
// Configuration
|
|
TaskCount int64
|
|
Parallelism int64
|
|
MaxRetries int64
|
|
Timeout string
|
|
|
|
// VPC Access
|
|
VPCAccess string
|
|
VPCEgressSettings string
|
|
|
|
// Environment
|
|
EnvVarCount int
|
|
SecretEnvVarCount int
|
|
SecretVolumeCount int
|
|
|
|
// Security analysis
|
|
HardcodedSecrets []HardcodedSecret
|
|
UsesDefaultSA bool
|
|
|
|
// Detailed env var and secret info
|
|
EnvVars []EnvVarInfo
|
|
SecretRefs []SecretRefInfo
|
|
|
|
// IAM
|
|
IAMBindings []IAMBinding // All IAM bindings on this job
|
|
|
|
// Status
|
|
Status string
|
|
}
|
|
|
|
// Services retrieves all Cloud Run services in a project across all regions
|
|
// Note: This excludes Cloud Functions 2nd gen which are deployed as Cloud Run services
|
|
// but should be enumerated via the functions module instead
|
|
func (cs *CloudRunService) Services(projectID string) ([]ServiceInfo, error) {
|
|
ctx := context.Background()
|
|
|
|
service, err := cs.getServiceV2(ctx)
|
|
if err != nil {
|
|
return nil, gcpinternal.ParseGCPError(err, "run.googleapis.com")
|
|
}
|
|
|
|
var services []ServiceInfo
|
|
|
|
// List services across all locations
|
|
parent := fmt.Sprintf("projects/%s/locations/-", projectID)
|
|
|
|
call := service.Projects.Locations.Services.List(parent)
|
|
err = call.Pages(ctx, func(page *run.GoogleCloudRunV2ListServicesResponse) error {
|
|
for _, svc := range page.Services {
|
|
// Skip Cloud Functions 2nd gen - they have label "goog-managed-by: cloudfunctions"
|
|
// These should be enumerated via the functions module, not cloudrun
|
|
if isCloudFunction(svc.Labels) {
|
|
continue
|
|
}
|
|
|
|
info := parseServiceInfo(svc, projectID)
|
|
|
|
// Try to get IAM policy
|
|
iamPolicy, iamErr := cs.getServiceIAMPolicy(service, svc.Name)
|
|
if iamErr == nil && iamPolicy != nil {
|
|
info.IAMBindings, info.InvokerMembers, info.IsPublic = parseAllIAMBindings(iamPolicy)
|
|
}
|
|
|
|
services = append(services, info)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, gcpinternal.ParseGCPError(err, "run.googleapis.com")
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
// isCloudFunction checks if a Cloud Run service is actually a Cloud Function 2nd gen
|
|
// Cloud Functions 2nd gen are deployed as Cloud Run services but have specific labels
|
|
func isCloudFunction(labels map[string]string) bool {
|
|
if labels == nil {
|
|
return false
|
|
}
|
|
// Cloud Functions 2nd gen have "goog-managed-by: cloudfunctions" label
|
|
if managedBy, ok := labels["goog-managed-by"]; ok && managedBy == "cloudfunctions" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Jobs retrieves all Cloud Run jobs in a project across all regions
|
|
// Note: The Cloud Run Jobs API does NOT support the "-" wildcard for locations
|
|
// unlike the Services API, so we must iterate through regions explicitly
|
|
func (cs *CloudRunService) Jobs(projectID string) ([]JobInfo, error) {
|
|
ctx := context.Background()
|
|
|
|
service, err := cs.getServiceV2(ctx)
|
|
if err != nil {
|
|
return nil, gcpinternal.ParseGCPError(err, "run.googleapis.com")
|
|
}
|
|
|
|
var jobs []JobInfo
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
var lastErr error
|
|
var errMu sync.Mutex
|
|
|
|
// Use a semaphore to limit concurrent API calls
|
|
semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
|
|
|
|
// Get regions from regionService (with automatic fallback)
|
|
regions := regionservice.GetCachedRegionNames(ctx, projectID)
|
|
|
|
// Iterate through all Cloud Run regions in parallel
|
|
for _, region := range regions {
|
|
wg.Add(1)
|
|
go func(region string) {
|
|
defer wg.Done()
|
|
|
|
// Acquire semaphore
|
|
semaphore <- struct{}{}
|
|
defer func() { <-semaphore }()
|
|
|
|
parent := fmt.Sprintf("projects/%s/locations/%s", projectID, region)
|
|
|
|
call := service.Projects.Locations.Jobs.List(parent)
|
|
err := call.Pages(ctx, func(page *run.GoogleCloudRunV2ListJobsResponse) error {
|
|
for _, job := range page.Jobs {
|
|
info := parseJobInfo(job, projectID)
|
|
|
|
// Try to get IAM policy for job
|
|
iamPolicy, iamErr := cs.getJobIAMPolicy(service, job.Name)
|
|
if iamErr == nil && iamPolicy != nil {
|
|
info.IAMBindings, _, _ = parseAllIAMBindings(iamPolicy)
|
|
}
|
|
|
|
mu.Lock()
|
|
jobs = append(jobs, info)
|
|
mu.Unlock()
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
// Track the last error but continue - region may not have jobs or API may not be enabled
|
|
// Common errors: 404 (no jobs in region), 403 (permission denied)
|
|
errMu.Lock()
|
|
lastErr = err
|
|
errMu.Unlock()
|
|
}
|
|
}(region)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Only return error if we got no jobs AND had errors
|
|
// If we found jobs in some regions, that's success
|
|
if len(jobs) == 0 && lastErr != nil {
|
|
return nil, gcpinternal.ParseGCPError(lastErr, "run.googleapis.com")
|
|
}
|
|
|
|
return jobs, nil
|
|
}
|
|
|
|
// parseServiceInfo extracts relevant information from a Cloud Run service
|
|
func parseServiceInfo(svc *run.GoogleCloudRunV2Service, projectID string) ServiceInfo {
|
|
info := ServiceInfo{
|
|
Name: extractName(svc.Name),
|
|
ProjectID: projectID,
|
|
Description: svc.Description,
|
|
Creator: svc.Creator,
|
|
UpdateTime: svc.UpdateTime,
|
|
URL: svc.Uri,
|
|
}
|
|
|
|
// Parse conditions for status
|
|
if len(svc.Conditions) > 0 {
|
|
for _, cond := range svc.Conditions {
|
|
if cond.Type == "Ready" {
|
|
if cond.State == "CONDITION_SUCCEEDED" {
|
|
info.Status = "Ready"
|
|
} else {
|
|
info.Status = cond.State
|
|
if cond.Reason != "" {
|
|
info.Status = cond.Reason
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if info.Status == "" {
|
|
info.Status = "Unknown"
|
|
}
|
|
|
|
// Extract region from service name
|
|
// Format: projects/{project}/locations/{location}/services/{name}
|
|
parts := strings.Split(svc.Name, "/")
|
|
if len(parts) >= 4 {
|
|
info.Region = parts[3]
|
|
}
|
|
|
|
// Ingress settings
|
|
info.IngressSettings = svc.Ingress
|
|
|
|
// Latest revision info
|
|
info.LatestRevision = svc.LatestCreatedRevision
|
|
info.LatestReadyRevision = svc.LatestReadyRevision
|
|
|
|
// Check if all traffic goes to latest
|
|
for _, traffic := range svc.Traffic {
|
|
if traffic.Type == "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" && traffic.Percent == 100 {
|
|
info.TrafficAllOnLatest = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Binary authorization
|
|
if svc.BinaryAuthorization != nil {
|
|
info.BinaryAuthorizationPolicy = svc.BinaryAuthorization.Policy
|
|
if svc.BinaryAuthorization.UseDefault {
|
|
info.BinaryAuthorizationPolicy = "default"
|
|
}
|
|
}
|
|
|
|
// Template configuration (current revision settings)
|
|
if svc.Template != nil {
|
|
info.ServiceAccount = svc.Template.ServiceAccount
|
|
info.Timeout = svc.Template.Timeout
|
|
|
|
if svc.Template.Scaling != nil {
|
|
info.MaxInstances = svc.Template.Scaling.MaxInstanceCount
|
|
info.MinInstances = svc.Template.Scaling.MinInstanceCount
|
|
}
|
|
|
|
// VPC access configuration
|
|
if svc.Template.VpcAccess != nil {
|
|
info.VPCAccess = svc.Template.VpcAccess.Connector
|
|
info.VPCEgressSettings = svc.Template.VpcAccess.Egress
|
|
if info.VPCAccess == "" && svc.Template.VpcAccess.NetworkInterfaces != nil {
|
|
info.VPCAccess = "Direct VPC"
|
|
}
|
|
}
|
|
|
|
// Container configuration
|
|
if len(svc.Template.Containers) > 0 {
|
|
container := svc.Template.Containers[0]
|
|
info.ContainerImage = container.Image
|
|
|
|
// Port
|
|
if len(container.Ports) > 0 {
|
|
info.ContainerPort = container.Ports[0].ContainerPort
|
|
}
|
|
|
|
// Resources
|
|
if container.Resources != nil {
|
|
if container.Resources.Limits != nil {
|
|
if cpu, ok := container.Resources.Limits["cpu"]; ok {
|
|
info.CPULimit = cpu
|
|
}
|
|
if mem, ok := container.Resources.Limits["memory"]; ok {
|
|
info.MemoryLimit = mem
|
|
}
|
|
}
|
|
}
|
|
|
|
// Environment variables
|
|
info.EnvVarCount = len(container.Env)
|
|
|
|
// Process each environment variable
|
|
for _, env := range container.Env {
|
|
envInfo := EnvVarInfo{
|
|
Name: env.Name,
|
|
}
|
|
|
|
if env.ValueSource != nil && env.ValueSource.SecretKeyRef != nil {
|
|
// Secret Manager reference
|
|
info.SecretEnvVarCount++
|
|
envInfo.Source = "secret-manager"
|
|
envInfo.SecretName = env.ValueSource.SecretKeyRef.Secret
|
|
envInfo.SecretVersion = env.ValueSource.SecretKeyRef.Version
|
|
|
|
// Also add to SecretRefs
|
|
info.SecretRefs = append(info.SecretRefs, SecretRefInfo{
|
|
EnvVarName: env.Name,
|
|
SecretName: env.ValueSource.SecretKeyRef.Secret,
|
|
SecretVersion: env.ValueSource.SecretKeyRef.Version,
|
|
Type: "env",
|
|
})
|
|
} else {
|
|
// Direct value
|
|
envInfo.Source = "direct"
|
|
envInfo.Value = env.Value
|
|
}
|
|
|
|
info.EnvVars = append(info.EnvVars, envInfo)
|
|
}
|
|
|
|
// Count secret volumes
|
|
for _, vol := range container.VolumeMounts {
|
|
// Check if this volume is a secret
|
|
for _, svcVol := range svc.Template.Volumes {
|
|
if svcVol.Name == vol.Name && svcVol.Secret != nil {
|
|
info.SecretVolumeCount++
|
|
info.SecretRefs = append(info.SecretRefs, SecretRefInfo{
|
|
SecretName: svcVol.Secret.Secret,
|
|
SecretVersion: "latest",
|
|
MountPath: vol.MountPath,
|
|
Type: "volume",
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detect hardcoded secrets in env vars
|
|
info.HardcodedSecrets = detectHardcodedSecrets(container.Env)
|
|
}
|
|
|
|
// Check for default service account
|
|
info.UsesDefaultSA = isDefaultServiceAccount(info.ServiceAccount, projectID)
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// parseJobInfo extracts relevant information from a Cloud Run job
|
|
func parseJobInfo(job *run.GoogleCloudRunV2Job, projectID string) JobInfo {
|
|
info := JobInfo{
|
|
Name: extractName(job.Name),
|
|
ProjectID: projectID,
|
|
Creator: job.Creator,
|
|
UpdateTime: job.UpdateTime,
|
|
}
|
|
|
|
// Extract region from job name
|
|
parts := strings.Split(job.Name, "/")
|
|
if len(parts) >= 4 {
|
|
info.Region = parts[3]
|
|
}
|
|
|
|
// Parse conditions for status
|
|
if len(job.Conditions) > 0 {
|
|
for _, cond := range job.Conditions {
|
|
if cond.Type == "Ready" {
|
|
if cond.State == "CONDITION_SUCCEEDED" {
|
|
info.Status = "Ready"
|
|
} else {
|
|
info.Status = cond.State
|
|
if cond.Reason != "" {
|
|
info.Status = cond.Reason
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if info.Status == "" {
|
|
info.Status = "Unknown"
|
|
}
|
|
|
|
// Last execution
|
|
if job.LatestCreatedExecution != nil {
|
|
info.LastExecution = job.LatestCreatedExecution.Name
|
|
}
|
|
|
|
// Template configuration
|
|
if job.Template != nil {
|
|
info.TaskCount = job.Template.TaskCount
|
|
info.Parallelism = job.Template.Parallelism
|
|
|
|
if job.Template.Template != nil {
|
|
info.MaxRetries = job.Template.Template.MaxRetries
|
|
info.Timeout = job.Template.Template.Timeout
|
|
info.ServiceAccount = job.Template.Template.ServiceAccount
|
|
|
|
// VPC access configuration
|
|
if job.Template.Template.VpcAccess != nil {
|
|
info.VPCAccess = job.Template.Template.VpcAccess.Connector
|
|
info.VPCEgressSettings = job.Template.Template.VpcAccess.Egress
|
|
if info.VPCAccess == "" && job.Template.Template.VpcAccess.NetworkInterfaces != nil {
|
|
info.VPCAccess = "Direct VPC"
|
|
}
|
|
}
|
|
|
|
// Container configuration
|
|
if len(job.Template.Template.Containers) > 0 {
|
|
container := job.Template.Template.Containers[0]
|
|
info.ContainerImage = container.Image
|
|
|
|
// Environment variables
|
|
info.EnvVarCount = len(container.Env)
|
|
|
|
// Process each environment variable
|
|
for _, env := range container.Env {
|
|
envInfo := EnvVarInfo{
|
|
Name: env.Name,
|
|
}
|
|
|
|
if env.ValueSource != nil && env.ValueSource.SecretKeyRef != nil {
|
|
// Secret Manager reference
|
|
info.SecretEnvVarCount++
|
|
envInfo.Source = "secret-manager"
|
|
envInfo.SecretName = env.ValueSource.SecretKeyRef.Secret
|
|
envInfo.SecretVersion = env.ValueSource.SecretKeyRef.Version
|
|
|
|
// Also add to SecretRefs
|
|
info.SecretRefs = append(info.SecretRefs, SecretRefInfo{
|
|
EnvVarName: env.Name,
|
|
SecretName: env.ValueSource.SecretKeyRef.Secret,
|
|
SecretVersion: env.ValueSource.SecretKeyRef.Version,
|
|
Type: "env",
|
|
})
|
|
} else {
|
|
// Direct value
|
|
envInfo.Source = "direct"
|
|
envInfo.Value = env.Value
|
|
}
|
|
|
|
info.EnvVars = append(info.EnvVars, envInfo)
|
|
}
|
|
|
|
// Count secret volumes
|
|
for _, vol := range container.VolumeMounts {
|
|
for _, jobVol := range job.Template.Template.Volumes {
|
|
if jobVol.Name == vol.Name && jobVol.Secret != nil {
|
|
info.SecretVolumeCount++
|
|
info.SecretRefs = append(info.SecretRefs, SecretRefInfo{
|
|
SecretName: jobVol.Secret.Secret,
|
|
SecretVersion: "latest",
|
|
MountPath: vol.MountPath,
|
|
Type: "volume",
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detect hardcoded secrets in env vars
|
|
info.HardcodedSecrets = detectHardcodedSecrets(container.Env)
|
|
}
|
|
|
|
// Check for default service account
|
|
info.UsesDefaultSA = isDefaultServiceAccount(info.ServiceAccount, projectID)
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// getServiceIAMPolicy retrieves the IAM policy for a Cloud Run service
|
|
func (cs *CloudRunService) getServiceIAMPolicy(service *run.Service, serviceName string) (*run.GoogleIamV1Policy, error) {
|
|
ctx := context.Background()
|
|
|
|
policy, err := service.Projects.Locations.Services.GetIamPolicy(serviceName).Context(ctx).Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
// getJobIAMPolicy retrieves the IAM policy for a Cloud Run job
|
|
func (cs *CloudRunService) getJobIAMPolicy(service *run.Service, jobName string) (*run.GoogleIamV1Policy, error) {
|
|
ctx := context.Background()
|
|
|
|
policy, err := service.Projects.Locations.Jobs.GetIamPolicy(jobName).Context(ctx).Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
// parseAllIAMBindings extracts all IAM bindings, invokers, and checks for public access
|
|
func parseAllIAMBindings(policy *run.GoogleIamV1Policy) ([]IAMBinding, []string, bool) {
|
|
var allBindings []IAMBinding
|
|
var invokers []string
|
|
isPublic := false
|
|
|
|
for _, binding := range policy.Bindings {
|
|
for _, member := range binding.Members {
|
|
allBindings = append(allBindings, IAMBinding{
|
|
Role: binding.Role,
|
|
Member: member,
|
|
})
|
|
}
|
|
|
|
// Check for invoker role
|
|
if binding.Role == "roles/run.invoker" {
|
|
invokers = append(invokers, binding.Members...)
|
|
|
|
// Check for public access
|
|
for _, member := range binding.Members {
|
|
if member == "allUsers" || member == "allAuthenticatedUsers" {
|
|
isPublic = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return allBindings, invokers, isPublic
|
|
}
|
|
|
|
// extractName extracts just the resource name from the full resource name
|
|
func extractName(fullName string) string {
|
|
parts := strings.Split(fullName, "/")
|
|
if len(parts) > 0 {
|
|
return parts[len(parts)-1]
|
|
}
|
|
return fullName
|
|
}
|
|
|
|
// secretPatterns maps env var name patterns to secret types
|
|
var secretPatterns = map[string]string{
|
|
"PASSWORD": "password",
|
|
"PASSWD": "password",
|
|
"SECRET": "secret",
|
|
"API_KEY": "api-key",
|
|
"APIKEY": "api-key",
|
|
"API-KEY": "api-key",
|
|
"TOKEN": "token",
|
|
"ACCESS_TOKEN": "token",
|
|
"AUTH_TOKEN": "token",
|
|
"BEARER": "token",
|
|
"CREDENTIAL": "credential",
|
|
"PRIVATE_KEY": "credential",
|
|
"PRIVATEKEY": "credential",
|
|
"CONNECTION_STRING": "connection-string",
|
|
"CONN_STR": "connection-string",
|
|
"DATABASE_URL": "connection-string",
|
|
"DB_PASSWORD": "password",
|
|
"DB_PASS": "password",
|
|
"MYSQL_PASSWORD": "password",
|
|
"POSTGRES_PASSWORD": "password",
|
|
"REDIS_PASSWORD": "password",
|
|
"MONGODB_URI": "connection-string",
|
|
"AWS_ACCESS_KEY": "credential",
|
|
"AWS_SECRET": "credential",
|
|
"AZURE_KEY": "credential",
|
|
"GCP_KEY": "credential",
|
|
"ENCRYPTION_KEY": "credential",
|
|
"SIGNING_KEY": "credential",
|
|
"JWT_SECRET": "credential",
|
|
"SESSION_SECRET": "credential",
|
|
"OAUTH": "credential",
|
|
"CLIENT_SECRET": "credential",
|
|
}
|
|
|
|
// detectHardcodedSecrets analyzes env vars to find potential hardcoded secrets
|
|
func detectHardcodedSecrets(envVars []*run.GoogleCloudRunV2EnvVar) []HardcodedSecret {
|
|
var secrets []HardcodedSecret
|
|
|
|
for _, env := range envVars {
|
|
if env == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip if using Secret Manager reference
|
|
if env.ValueSource != nil && env.ValueSource.SecretKeyRef != nil {
|
|
continue
|
|
}
|
|
|
|
// Only flag if there's a direct value (not empty)
|
|
if env.Value == "" {
|
|
continue
|
|
}
|
|
|
|
envNameUpper := strings.ToUpper(env.Name)
|
|
|
|
for pattern, secretType := range secretPatterns {
|
|
if strings.Contains(envNameUpper, pattern) {
|
|
secrets = append(secrets, HardcodedSecret{
|
|
EnvVarName: env.Name,
|
|
SecretType: secretType,
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return secrets
|
|
}
|
|
|
|
// isDefaultServiceAccount checks if the service account is a default compute SA
|
|
func isDefaultServiceAccount(sa, projectID string) bool {
|
|
if sa == "" {
|
|
return true // Empty means using default
|
|
}
|
|
// Default compute SA pattern: {project-number}[email protected]
|
|
return strings.Contains(sa, "[email protected]")
|
|
}
|