diff --git a/.gitignore b/.gitignore index d413a25..1bf9080 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Binaries bin/ +server *.exe *.dll *.so diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index aa3c8e5..638980f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -624,7 +624,21 @@ make run-locally # One-shot ### Monitoring - **Metrics**: Expose Prometheus metrics from gRPC service -- **Logs**: Structured JSON logging +- **Logs**: Structured JSON logging via `log/slog` + - Machine-readable JSON format for log aggregation tools (Datadog, Splunk, CloudWatch Insights) + - Context-aware logging with typed fields for queryable log data + - Configurable log levels (Info/Debug via `--verbose` flag) + - All components (detectors, inventory sources, EOL providers) use structured logging + - Example log entry: + ```json + { + "time": "2024-01-15T10:30:45Z", + "level": "WARN", + "msg": "failed to parse resource from CSV row", + "row_number": 42, + "error": "missing ARN" + } + ``` - **Alerts**: Based on RED/YELLOW finding counts - **Dashboards**: Query gRPC API for real-time data diff --git a/README.md b/README.md index d3c7a29..d63511b 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ Version Guard is configured via environment variables or CLI flags: | `TAG_APP_KEYS` | Comma-separated AWS tag keys for app/service | `app,application,service` | | `TAG_ENV_KEYS` | Comma-separated AWS tag keys for environment | `environment,env` | | `TAG_BRAND_KEYS` | Comma-separated AWS tag keys for brand/business unit | `brand` | +| `--verbose` / `-v` | Enable debug-level logging | `false` | **Customizing AWS Tag Keys:** @@ -217,6 +218,36 @@ export TAG_APP_KEYS="team,squad,application" The tag keys are tried in order — the first matching tag wins. +**Logging:** + +Version Guard uses structured JSON logging via Go's `log/slog` package for production observability: + +```bash +# Run with debug-level logging +./bin/version-guard --verbose + +# Production mode (info-level logging only) +./bin/version-guard +``` + +Logs are output in JSON format for easy parsing by log aggregation tools (Datadog, Splunk, CloudWatch Insights): + +```json +{ + "time": "2024-01-15T10:30:45Z", + "level": "WARN", + "msg": "failed to detect drift for resource", + "resource_id": "arn:aws:rds:us-west-2:123456789012:cluster:my-db", + "error": "version not found in EOL database" +} +``` + +Benefits: +- Machine-readable structured data with typed fields +- Context-aware logging with trace IDs +- Queryable logs (e.g., filter by `resource_id` or `error`) +- Integrates seamlessly with observability platforms + See `./bin/version-guard --help` for all options. ## 🎨 Classification Policy diff --git a/cmd/server/main.go b/cmd/server/main.go index 0749c7a..3902b64 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log/slog" "os" "os/signal" "strings" @@ -99,6 +100,16 @@ func (s *ServerCLI) buildTagConfig() *wiz.TagConfig { } func (s *ServerCLI) Run(_ *kong.Context) error { + // Initialize structured logger + logLevel := slog.LevelInfo + if s.Verbose { + logLevel = slog.LevelDebug + } + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: logLevel, + })) + slog.SetDefault(logger) + fmt.Println("Starting Version Guard Detector Service (Open Source)") fmt.Printf(" Version: %s\n", version) fmt.Printf(" Temporal Endpoint: %s\n", s.TemporalEndpoint) @@ -184,17 +195,17 @@ func (s *ServerCLI) Run(_ *kong.Context) error { wizClient := wiz.NewClient(wizHTTPClient, time.Duration(s.WizCacheTTLHours)*time.Hour) if s.WizAuroraReportID != "" { - invSources[types.ResourceTypeAurora] = wiz.NewAuroraInventorySource(wizClient, s.WizAuroraReportID). + invSources[types.ResourceTypeAurora] = wiz.NewAuroraInventorySource(wizClient, s.WizAuroraReportID, logger). WithTagConfig(tagConfig) fmt.Println("✓ Aurora inventory source configured (Wiz)") } if s.WizElastiCacheReportID != "" { - invSources[types.ResourceTypeElastiCache] = wiz.NewElastiCacheInventorySource(wizClient, s.WizElastiCacheReportID). + invSources[types.ResourceTypeElastiCache] = wiz.NewElastiCacheInventorySource(wizClient, s.WizElastiCacheReportID, logger). WithTagConfig(tagConfig) fmt.Println("✓ ElastiCache inventory source configured (Wiz)") } if s.WizEKSReportID != "" { - invSources[types.ResourceTypeEKS] = wiz.NewEKSInventorySource(wizClient, s.WizEKSReportID). + invSources[types.ResourceTypeEKS] = wiz.NewEKSInventorySource(wizClient, s.WizEKSReportID, logger). WithTagConfig(tagConfig) fmt.Println("✓ EKS inventory source configured (Wiz)") } @@ -240,15 +251,15 @@ func (s *ServerCLI) Run(_ *kong.Context) error { cacheTTL := 24 * time.Hour // Aurora EOL provider (using endoflife.date for PostgreSQL versions) - eolProviders[types.ResourceTypeAurora] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL) + eolProviders[types.ResourceTypeAurora] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL, logger) fmt.Println("✓ Aurora EOL provider configured (endoflife.date API)") // EKS EOL provider (using endoflife.date for Kubernetes versions) - eolProviders[types.ResourceTypeEKS] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL) + eolProviders[types.ResourceTypeEKS] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL, logger) fmt.Println("✓ EKS EOL provider configured (endoflife.date API)") // ElastiCache EOL provider - eolProviders[types.ResourceTypeElastiCache] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL) + eolProviders[types.ResourceTypeElastiCache] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL, logger) fmt.Println("✓ ElastiCache EOL provider configured (endoflife.date API)") // Initialize policy engine @@ -261,6 +272,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error { invSources[types.ResourceTypeAurora], eolProviders[types.ResourceTypeAurora], policyEngine, + logger, ) fmt.Println("✓ Aurora detector initialized") } @@ -271,6 +283,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error { invSources[types.ResourceTypeEKS], eolProviders[types.ResourceTypeEKS], policyEngine, + logger, ) fmt.Println("✓ EKS detector initialized") } diff --git a/pkg/detector/aurora/detector.go b/pkg/detector/aurora/detector.go index 468cd29..5e93952 100644 --- a/pkg/detector/aurora/detector.go +++ b/pkg/detector/aurora/detector.go @@ -3,6 +3,7 @@ package aurora import ( "context" "fmt" + "log/slog" "time" "github.com/pkg/errors" @@ -18,6 +19,7 @@ type Detector struct { inventory inventory.InventorySource eol eol.Provider policy policy.VersionPolicy + logger *slog.Logger } // NewDetector creates a new Aurora detector @@ -25,11 +27,16 @@ func NewDetector( inventory inventory.InventorySource, eol eol.Provider, policy policy.VersionPolicy, + logger *slog.Logger, ) *Detector { + if logger == nil { + logger = slog.Default() + } return &Detector{ inventory: inventory, eol: eol, policy: policy, + logger: logger, } } diff --git a/pkg/detector/aurora/detector_test.go b/pkg/detector/aurora/detector_test.go index 612ca0c..f2fa8af 100644 --- a/pkg/detector/aurora/detector_test.go +++ b/pkg/detector/aurora/detector_test.go @@ -49,6 +49,7 @@ func TestDetector_Detect_EOLVersion(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -119,6 +120,7 @@ func TestDetector_Detect_CurrentVersion(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -176,6 +178,7 @@ func TestDetector_Detect_ExtendedSupport(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -257,6 +260,7 @@ func TestDetector_Detect_MultipleResources(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -311,6 +315,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -327,7 +332,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) { } func TestDetector_Name(t *testing.T) { - detector := NewDetector(nil, nil, nil) + detector := NewDetector(nil, nil, nil, nil) name := detector.Name() expected := "aurora-detector" @@ -338,7 +343,7 @@ func TestDetector_Name(t *testing.T) { } func TestDetector_ResourceType(t *testing.T) { - detector := NewDetector(nil, nil, nil) + detector := NewDetector(nil, nil, nil, nil) resourceType := detector.ResourceType() diff --git a/pkg/detector/aurora/integration_test.go b/pkg/detector/aurora/integration_test.go index effb6c8..855d39e 100644 --- a/pkg/detector/aurora/integration_test.go +++ b/pkg/detector/aurora/integration_test.go @@ -154,6 +154,7 @@ func TestFullFlow_MultipleResourcesWithDifferentStatuses(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Execute: Run the full detection flow @@ -368,7 +369,7 @@ func TestFullFlow_SummaryStatistics(t *testing.T) { }, } - detector := NewDetector(mockInventory, mockEOL, policy.NewDefaultPolicy()) + detector := NewDetector(mockInventory, mockEOL, policy.NewDefaultPolicy(), nil) findings, err := detector.Detect(context.Background()) if err != nil { diff --git a/pkg/detector/eks/detector.go b/pkg/detector/eks/detector.go index c4d7c7e..1b85d76 100644 --- a/pkg/detector/eks/detector.go +++ b/pkg/detector/eks/detector.go @@ -2,7 +2,7 @@ package eks import ( "context" - "log" + "log/slog" "time" "github.com/pkg/errors" @@ -18,6 +18,7 @@ type Detector struct { inventory inventory.InventorySource eol eol.Provider policy policy.VersionPolicy + logger *slog.Logger } // NewDetector creates a new EKS detector @@ -25,11 +26,16 @@ func NewDetector( inventory inventory.InventorySource, eol eol.Provider, policy policy.VersionPolicy, + logger *slog.Logger, ) *Detector { + if logger == nil { + logger = slog.Default() + } return &Detector{ inventory: inventory, eol: eol, policy: policy, + logger: logger, } } @@ -62,8 +68,9 @@ func (d *Detector) Detect(ctx context.Context) ([]*types.Finding, error) { finding, err := d.detectResource(ctx, resource) if err != nil { // Log error but continue with other resources - // TODO: wire through proper structured logger (e.g., *slog.Logger) - log.Printf("WARN: failed to detect drift for %s: %v", resource.ID, err) + d.logger.WarnContext(ctx, "failed to detect drift for resource", + "resource_id", resource.ID, + "error", err) continue } diff --git a/pkg/detector/eks/detector_test.go b/pkg/detector/eks/detector_test.go index c6dc975..3f5c806 100644 --- a/pkg/detector/eks/detector_test.go +++ b/pkg/detector/eks/detector_test.go @@ -49,6 +49,7 @@ func TestDetector_Detect_EOLVersion(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -119,6 +120,7 @@ func TestDetector_Detect_CurrentVersion(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -176,6 +178,7 @@ func TestDetector_Detect_ExtendedSupport(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -257,6 +260,7 @@ func TestDetector_Detect_MultipleResources(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -311,6 +315,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Run detection @@ -327,7 +332,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) { } func TestDetector_Name(t *testing.T) { - detector := NewDetector(nil, nil, nil) + detector := NewDetector(nil, nil, nil, nil) name := detector.Name() expected := "eks-detector" @@ -338,7 +343,7 @@ func TestDetector_Name(t *testing.T) { } func TestDetector_ResourceType(t *testing.T) { - detector := NewDetector(nil, nil, nil) + detector := NewDetector(nil, nil, nil, nil) resourceType := detector.ResourceType() diff --git a/pkg/detector/eks/integration_test.go b/pkg/detector/eks/integration_test.go index 265267d..25c642e 100644 --- a/pkg/detector/eks/integration_test.go +++ b/pkg/detector/eks/integration_test.go @@ -146,6 +146,7 @@ func TestFullFlow_MultipleResourcesWithDifferentStatuses(t *testing.T) { mockInventory, mockEOL, policy.NewDefaultPolicy(), + nil, // logger ) // Execute: Run detection diff --git a/pkg/eol/endoflife/integration_test.go b/pkg/eol/endoflife/integration_test.go index b261b3d..cd6f9a4 100644 --- a/pkg/eol/endoflife/integration_test.go +++ b/pkg/eol/endoflife/integration_test.go @@ -66,7 +66,7 @@ func TestProviderRealAPIIntegration(t *testing.T) { // Create provider with real client client := NewRealHTTPClient() - provider := NewProvider(client, 1*time.Hour) + provider := NewProvider(client, 1*time.Hour, nil) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -124,7 +124,7 @@ func TestCachingRealAPI(t *testing.T) { } client := NewRealHTTPClient() - provider := NewProvider(client, 1*time.Hour) + provider := NewProvider(client, 1*time.Hour, nil) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/pkg/eol/endoflife/provider.go b/pkg/eol/endoflife/provider.go index 4d2d7bd..559c08e 100644 --- a/pkg/eol/endoflife/provider.go +++ b/pkg/eol/endoflife/provider.go @@ -3,6 +3,7 @@ package endoflife import ( "context" "fmt" + "log/slog" "strings" "sync" "time" @@ -62,6 +63,7 @@ type Provider struct { client Client cacheTTL time.Duration group singleflight.Group // Prevents thundering herd on API calls + logger *slog.Logger } //nolint:govet // field alignment sacrificed for readability @@ -71,15 +73,19 @@ type cachedVersions struct { } // NewProvider creates a new endoflife.date EOL provider -func NewProvider(client Client, cacheTTL time.Duration) *Provider { +func NewProvider(client Client, cacheTTL time.Duration, logger *slog.Logger) *Provider { if cacheTTL == 0 { cacheTTL = 24 * time.Hour // Default: cache for 24 hours } + if logger == nil { + logger = slog.Default() + } return &Provider{ client: client, cacheTTL: cacheTTL, cache: make(map[string]*cachedVersions), + logger: logger, } } @@ -191,7 +197,10 @@ func (p *Provider) ListAllVersions(ctx context.Context, engine string) ([]*types lifecycle, err := p.convertCycle(engine, product, cycle) if err != nil { // Skip cycles we can't parse, but log a warning - // TODO: wire through proper structured logger + p.logger.WarnContext(ctx, "failed to convert EOL cycle, skipping", + "engine", engine, + "product", product, + "error", err) continue } versions = append(versions, lifecycle) diff --git a/pkg/eol/endoflife/provider_test.go b/pkg/eol/endoflife/provider_test.go index 1b41f26..4830538 100644 --- a/pkg/eol/endoflife/provider_test.go +++ b/pkg/eol/endoflife/provider_test.go @@ -46,7 +46,7 @@ func TestProvider_GetVersionLifecycle_PostgreSQL(t *testing.T) { }, } - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) tests := []struct { name string @@ -149,7 +149,7 @@ func TestProvider_ListAllVersions(t *testing.T) { }, } - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) versions, err := provider.ListAllVersions(context.Background(), "postgres") if err != nil { @@ -188,7 +188,7 @@ func TestProvider_Caching(t *testing.T) { }, } - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) // First call - should hit API _, err := provider.ListAllVersions(context.Background(), "postgres") @@ -235,7 +235,7 @@ func TestProvider_CacheExpiration(t *testing.T) { } // Very short TTL for testing - provider := NewProvider(mockClient, 50*time.Millisecond) + provider := NewProvider(mockClient, 50*time.Millisecond, nil) // First call _, err := provider.ListAllVersions(context.Background(), "postgres") @@ -261,7 +261,7 @@ func TestProvider_CacheExpiration(t *testing.T) { func TestProvider_UnsupportedEngine(t *testing.T) { mockClient := &MockClient{} - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) _, err := provider.GetVersionLifecycle(context.Background(), "unsupported-engine", "1.0") if err == nil { @@ -283,7 +283,7 @@ func TestProvider_VersionNotFound(t *testing.T) { }, } - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) lifecycle, err := provider.GetVersionLifecycle(context.Background(), "postgres", "99.99") if err != nil { @@ -303,14 +303,14 @@ func TestProvider_VersionNotFound(t *testing.T) { } func TestProvider_Name(t *testing.T) { - provider := NewProvider(&MockClient{}, 1*time.Hour) + provider := NewProvider(&MockClient{}, 1*time.Hour, nil) if name := provider.Name(); name != "endoflife-date-api" { t.Errorf("Name() = %s, want endoflife-date-api", name) } } func TestProvider_Engines(t *testing.T) { - provider := NewProvider(&MockClient{}, 1*time.Hour) + provider := NewProvider(&MockClient{}, 1*time.Hour, nil) engines := provider.Engines() // Check that common engines are present @@ -340,7 +340,7 @@ func TestProvider_BlocksNonStandardSchema(t *testing.T) { }, } - provider := NewProvider(mockClient, 1*time.Hour) + provider := NewProvider(mockClient, 1*time.Hour, nil) // Test that all EKS-related engine names are blocked blockedEngines := []string{"kubernetes", "k8s", "eks"} @@ -364,7 +364,7 @@ func TestProvider_BlocksNonStandardSchema(t *testing.T) { } func TestConvertCycle_ExtendedSupport(t *testing.T) { - provider := NewProvider(&MockClient{}, 1*time.Hour) + provider := NewProvider(&MockClient{}, 1*time.Hour, nil) tests := []struct { name string diff --git a/pkg/inventory/wiz/aurora.go b/pkg/inventory/wiz/aurora.go index 10b4d7e..5f1bd3b 100644 --- a/pkg/inventory/wiz/aurora.go +++ b/pkg/inventory/wiz/aurora.go @@ -3,6 +3,7 @@ package wiz import ( "context" "fmt" + "log/slog" "strings" "github.com/block/Version-Guard/pkg/registry" @@ -30,15 +31,20 @@ type AuroraInventorySource struct { reportID string registryClient registry.Client // Optional: for service attribution when tags are missing tagConfig *TagConfig // Configurable tag key mappings + logger *slog.Logger } // NewAuroraInventorySource creates a new Wiz-based Aurora inventory source with default tag configuration. // Use WithTagConfig() to customize tag key mappings. -func NewAuroraInventorySource(client *Client, reportID string) *AuroraInventorySource { +func NewAuroraInventorySource(client *Client, reportID string, logger *slog.Logger) *AuroraInventorySource { + if logger == nil { + logger = slog.Default() + } return &AuroraInventorySource{ client: client, reportID: reportID, tagConfig: DefaultTagConfig(), + logger: logger, } } @@ -85,6 +91,7 @@ func (s *AuroraInventorySource) ListResources(ctx context.Context, resourceType return isAuroraResource(cols.col(row, colHeaderNativeType)) }, s.parseAuroraRow, + s.logger, ) } diff --git a/pkg/inventory/wiz/aurora_test.go b/pkg/inventory/wiz/aurora_test.go index 9b03bbc..d20dd4b 100644 --- a/pkg/inventory/wiz/aurora_test.go +++ b/pkg/inventory/wiz/aurora_test.go @@ -43,7 +43,7 @@ func TestAuroraInventorySource_ListResources_Success(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.AuroraCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) // Execute: List Aurora resources resources, err := source.ListResources(ctx, types.ResourceTypeAurora) @@ -110,7 +110,7 @@ func TestAuroraInventorySource_ListResources_EmptyReport(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.EmptyCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "empty-report-id") + source := NewAuroraInventorySource(client, "empty-report-id", nil) // Execute: List resources from empty report (only header row) resources, err := source.ListResources(ctx, types.ResourceTypeAurora) @@ -127,7 +127,7 @@ func TestAuroraInventorySource_ListResources_UnsupportedResourceType(t *testing. mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) // Execute: Request unsupported resource type _, err := source.ListResources(ctx, types.ResourceTypeElastiCache) @@ -148,7 +148,7 @@ func TestAuroraInventorySource_GetResource_Found(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.AuroraCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) // Execute: Get specific resource by ARN resource, err := source.GetResource(ctx, types.ResourceTypeAurora, @@ -174,7 +174,7 @@ func TestAuroraInventorySource_GetResource_NotFound(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.AuroraCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) // Execute: Get non-existent resource _, err := source.GetResource(ctx, types.ResourceTypeAurora, @@ -190,7 +190,7 @@ func TestAuroraInventorySource_GetResource_NotFound(t *testing.T) { func TestAuroraInventorySource_Name(t *testing.T) { mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) assert.Equal(t, "wiz-aurora", source.Name()) } @@ -198,7 +198,7 @@ func TestAuroraInventorySource_Name(t *testing.T) { func TestAuroraInventorySource_CloudProvider(t *testing.T) { mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) assert.Equal(t, types.CloudProviderAWS, source.CloudProvider()) } @@ -214,7 +214,7 @@ func TestParseAuroraRow_ServiceExtraction(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.AuroraCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) resources, err := source.ListResources(context.Background(), types.ResourceTypeAurora) require.NoError(t, err) @@ -256,7 +256,7 @@ arn:aws:rds:us-west-2:999888777666:cluster:untagged-cluster,untagged-cluster,rds } client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id").WithRegistryClient(mockRegistry) + source := NewAuroraInventorySource(client, "aurora-report-id", nil).WithRegistryClient(mockRegistry) // Execute: List resources resources, err := source.ListResources(ctx, types.ResourceTypeAurora) @@ -296,7 +296,7 @@ arn:aws:rds:us-east-1:888777666555:cluster:tagged-cluster,tagged-cluster,rds/Ama } client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id").WithRegistryClient(mockRegistry) + source := NewAuroraInventorySource(client, "aurora-report-id", nil).WithRegistryClient(mockRegistry) // Execute: List resources resources, err := source.ListResources(ctx, types.ResourceTypeAurora) @@ -328,7 +328,7 @@ arn:aws:rds:eu-west-1:777666555444:cluster:my-service-cluster,my-service-cluster // NO registry client configured client := NewClient(mockWizClient, time.Hour) - source := NewAuroraInventorySource(client, "aurora-report-id") + source := NewAuroraInventorySource(client, "aurora-report-id", nil) // Note: Not calling WithRegistryClient() // Execute: List resources diff --git a/pkg/inventory/wiz/eks.go b/pkg/inventory/wiz/eks.go index 41acebe..0a10b39 100644 --- a/pkg/inventory/wiz/eks.go +++ b/pkg/inventory/wiz/eks.go @@ -3,6 +3,7 @@ package wiz import ( "context" "fmt" + "log/slog" "strings" "time" @@ -30,15 +31,20 @@ type EKSInventorySource struct { reportID string registryClient registry.Client // Optional: for service attribution when tags are missing tagConfig *TagConfig // Configurable tag key mappings + logger *slog.Logger } // NewEKSInventorySource creates a new Wiz-based EKS inventory source with default tag configuration. // Use WithTagConfig() to customize tag key mappings. -func NewEKSInventorySource(client *Client, reportID string) *EKSInventorySource { +func NewEKSInventorySource(client *Client, reportID string, logger *slog.Logger) *EKSInventorySource { + if logger == nil { + logger = slog.Default() + } return &EKSInventorySource{ client: client, reportID: reportID, tagConfig: DefaultTagConfig(), + logger: logger, } } @@ -85,6 +91,7 @@ func (s *EKSInventorySource) ListResources(ctx context.Context, resourceType typ return isEKSResource(cols.col(row, colHeaderNativeType)) }, s.parseEKSRow, + s.logger, ) } diff --git a/pkg/inventory/wiz/elasticache.go b/pkg/inventory/wiz/elasticache.go index cbe7a87..4108b63 100644 --- a/pkg/inventory/wiz/elasticache.go +++ b/pkg/inventory/wiz/elasticache.go @@ -3,6 +3,7 @@ package wiz import ( "context" "fmt" + "log/slog" "strings" "github.com/block/Version-Guard/pkg/registry" @@ -15,15 +16,20 @@ type ElastiCacheInventorySource struct { reportID string registryClient registry.Client // Optional: for service attribution when tags are missing tagConfig *TagConfig // Configurable tag key mappings + logger *slog.Logger } // NewElastiCacheInventorySource creates a new Wiz-based ElastiCache inventory source with default tag configuration. // Use WithTagConfig() to customize tag key mappings. -func NewElastiCacheInventorySource(client *Client, reportID string) *ElastiCacheInventorySource { +func NewElastiCacheInventorySource(client *Client, reportID string, logger *slog.Logger) *ElastiCacheInventorySource { + if logger == nil { + logger = slog.Default() + } return &ElastiCacheInventorySource{ client: client, reportID: reportID, tagConfig: DefaultTagConfig(), + logger: logger, } } @@ -70,6 +76,7 @@ func (s *ElastiCacheInventorySource) ListResources(ctx context.Context, resource return isElastiCacheResource(cols.col(row, colHeaderNativeType)) }, s.parseElastiCacheRow, + s.logger, ) } diff --git a/pkg/inventory/wiz/elasticache_test.go b/pkg/inventory/wiz/elasticache_test.go index e6a9e04..eafb909 100644 --- a/pkg/inventory/wiz/elasticache_test.go +++ b/pkg/inventory/wiz/elasticache_test.go @@ -23,7 +23,7 @@ func TestElastiCacheInventorySource_ListResources_Success(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.ElastiCacheCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) resources, err := source.ListResources(ctx, types.ResourceTypeElastiCache) @@ -88,7 +88,7 @@ func TestElastiCacheInventorySource_ListResources_EmptyReport(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.EmptyCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "empty-report-id") + source := NewElastiCacheInventorySource(client, "empty-report-id", nil) resources, err := source.ListResources(ctx, types.ResourceTypeElastiCache) @@ -103,7 +103,7 @@ func TestElastiCacheInventorySource_ListResources_UnsupportedResourceType(t *tes mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) _, err := source.ListResources(ctx, types.ResourceTypeAurora) @@ -122,7 +122,7 @@ func TestElastiCacheInventorySource_GetResource_Found(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.ElastiCacheCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) resource, err := source.GetResource(ctx, types.ResourceTypeElastiCache, "arn:aws:elasticache:us-east-1:123456789012:cluster:legacy-redis-001") @@ -146,7 +146,7 @@ func TestElastiCacheInventorySource_GetResource_NotFound(t *testing.T) { Return(NewMockReadCloser(WizAPIFixtures.ElastiCacheCSVData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) _, err := source.GetResource(ctx, types.ResourceTypeElastiCache, "arn:aws:elasticache:us-west-2:999999999999:cluster:non-existent") @@ -160,7 +160,7 @@ func TestElastiCacheInventorySource_GetResource_NotFound(t *testing.T) { func TestElastiCacheInventorySource_Name(t *testing.T) { mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) assert.Equal(t, "wiz-elasticache", source.Name()) } @@ -168,7 +168,7 @@ func TestElastiCacheInventorySource_Name(t *testing.T) { func TestElastiCacheInventorySource_CloudProvider(t *testing.T) { mockWizClient := new(MockWizClient) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) assert.Equal(t, types.CloudProviderAWS, source.CloudProvider()) } @@ -190,7 +190,7 @@ arn:aws:elasticache:us-west-2:123456789012:user:default,default,elasticache#user Return(NewMockReadCloser(csvData), nil) client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id") + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil) resources, err := source.ListResources(ctx, types.ResourceTypeElastiCache) @@ -221,7 +221,7 @@ arn:aws:elasticache:us-west-2:999888777666:cluster:untagged-redis-001,untagged-r } client := NewClient(mockWizClient, time.Hour) - source := NewElastiCacheInventorySource(client, "elasticache-report-id").WithRegistryClient(mockRegistry) + source := NewElastiCacheInventorySource(client, "elasticache-report-id", nil).WithRegistryClient(mockRegistry) resources, err := source.ListResources(ctx, types.ResourceTypeElastiCache) diff --git a/pkg/inventory/wiz/helpers.go b/pkg/inventory/wiz/helpers.go index 746eb5d..4c33e6a 100644 --- a/pkg/inventory/wiz/helpers.go +++ b/pkg/inventory/wiz/helpers.go @@ -3,7 +3,7 @@ package wiz import ( "context" "fmt" - "log" + "log/slog" "strings" "time" @@ -94,7 +94,11 @@ func parseWizReport( requiredColumns []string, filterRow rowFilterFunc, parseRow rowParserFunc, + logger *slog.Logger, ) ([]*types.Resource, error) { + if logger == nil { + logger = slog.Default() + } // Fetch report data rows, err := client.GetReportData(ctx, reportID) if err != nil { @@ -128,8 +132,9 @@ func parseWizReport( resource, err := parseRow(ctx, cols, row) if err != nil { // Log error but continue processing other rows - // TODO: wire through proper structured logger (e.g., *slog.Logger) - log.Printf("WARN: row %d: failed to parse resource: %v", i+1, err) + logger.WarnContext(ctx, "failed to parse resource from CSV row", + "row_number", i+1, + "error", err) continue } diff --git a/server b/server deleted file mode 100755 index c72ccdc..0000000 Binary files a/server and /dev/null differ