Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Binaries
bin/
server
*.exe
*.dll
*.so
Expand Down
16 changes: 15 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand All @@ -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
Expand Down
25 changes: 19 additions & 6 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"log/slog"
"os"
"os/signal"
"strings"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)")
}
Expand Down Expand Up @@ -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
Expand All @@ -261,6 +272,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
invSources[types.ResourceTypeAurora],
eolProviders[types.ResourceTypeAurora],
policyEngine,
logger,
)
fmt.Println("✓ Aurora detector initialized")
}
Expand All @@ -271,6 +283,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
invSources[types.ResourceTypeEKS],
eolProviders[types.ResourceTypeEKS],
policyEngine,
logger,
)
fmt.Println("✓ EKS detector initialized")
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/detector/aurora/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aurora
import (
"context"
"fmt"
"log/slog"
"time"

"github.com/pkg/errors"
Expand All @@ -18,18 +19,24 @@ type Detector struct {
inventory inventory.InventorySource
eol eol.Provider
policy policy.VersionPolicy
logger *slog.Logger
}

// NewDetector creates a new Aurora detector
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,
}
}

Expand Down
9 changes: 7 additions & 2 deletions pkg/detector/aurora/detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestDetector_Detect_EOLVersion(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -119,6 +120,7 @@ func TestDetector_Detect_CurrentVersion(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -176,6 +178,7 @@ func TestDetector_Detect_ExtendedSupport(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -257,6 +260,7 @@ func TestDetector_Detect_MultipleResources(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -311,6 +315,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand All @@ -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"
Expand All @@ -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()

Expand Down
3 changes: 2 additions & 1 deletion pkg/detector/aurora/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func TestFullFlow_MultipleResourcesWithDifferentStatuses(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Execute: Run the full detection flow
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 10 additions & 3 deletions pkg/detector/eks/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package eks

import (
"context"
"log"
"log/slog"
"time"

"github.com/pkg/errors"
Expand All @@ -18,18 +18,24 @@ type Detector struct {
inventory inventory.InventorySource
eol eol.Provider
policy policy.VersionPolicy
logger *slog.Logger
}

// NewDetector creates a new EKS detector
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,
}
}

Expand Down Expand Up @@ -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
}

Expand Down
9 changes: 7 additions & 2 deletions pkg/detector/eks/detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestDetector_Detect_EOLVersion(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -119,6 +120,7 @@ func TestDetector_Detect_CurrentVersion(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -176,6 +178,7 @@ func TestDetector_Detect_ExtendedSupport(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -257,6 +260,7 @@ func TestDetector_Detect_MultipleResources(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand Down Expand Up @@ -311,6 +315,7 @@ func TestDetector_Detect_EmptyInventory(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Run detection
Expand All @@ -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"
Expand All @@ -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()

Expand Down
1 change: 1 addition & 0 deletions pkg/detector/eks/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func TestFullFlow_MultipleResourcesWithDifferentStatuses(t *testing.T) {
mockInventory,
mockEOL,
policy.NewDefaultPolicy(),
nil, // logger
)

// Execute: Run detection
Expand Down
4 changes: 2 additions & 2 deletions pkg/eol/endoflife/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading