Skip to content
Open
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
16 changes: 11 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
**Supported Resources**:
- ✅ Aurora (RDS MySQL/PostgreSQL)
- ✅ EKS (Kubernetes)
- 🔜 ElastiCache (Redis/Valkey/Memcached) - implementation available, needs integration
- 🔜 OpenSearch
- ElastiCache (Redis/Valkey/Memcached)
- OpenSearch
- 🔜 Lambda runtimes

---
Expand All @@ -31,7 +31,7 @@
**Vision:** Version Guard is a **cloud-agnostic** version drift detection platform supporting multiple cloud providers.

### Phase 1 (Implemented): AWS
- **Resources**: ✅ Aurora (RDS), ✅ EKS, 🔜 ElastiCache, 🔜 OpenSearch, 🔜 Lambda
- **Resources**: ✅ Aurora (RDS), ✅ EKS, ElastiCache, OpenSearch, 🔜 Lambda
- **Inventory**: Wiz (cross-cloud) + AWS APIs
- **EOL Data**: AWS native APIs (RDS, EKS) + endoflife.date (hybrid enrichment)

Expand Down Expand Up @@ -116,6 +116,8 @@ Version-Guard/
│ │ │ ├── aurora.go # AWS Aurora inventory
│ │ │ ├── elasticache.go # AWS ElastiCache inventory
│ │ │ ├── eks.go # AWS EKS inventory
│ │ │ ├── opensearch.go # AWS OpenSearch inventory
│ │ │ ├── helpers.go # Shared CSV parsing helpers
│ │ │ └── client.go # Wiz HTTP client
│ │ └── mock/ # Mock for tests
│ │
Expand All @@ -137,8 +139,10 @@ Version-Guard/
│ │ ├── detector.go # Detector interface
│ │ ├── aurora/
│ │ │ └── detector.go # Aurora detector
│ │ └── eks/
│ │ └── detector.go # EKS detector
│ │ ├── eks/
│ │ │ └── detector.go # EKS detector
│ │ └── opensearch/
│ │ └── detector.go # OpenSearch detector
│ │
│ ├── store/
│ │ ├── store.go # Store interface
Expand Down Expand Up @@ -205,6 +209,8 @@ type InventorySource interface {
**Implementations:**
- `wiz.AuroraInventorySource` - Wiz saved reports for Aurora
- `wiz.EKSInventorySource` - Wiz saved reports for EKS
- `wiz.ElastiCacheInventorySource` - Wiz saved reports for ElastiCache
- `wiz.OpenSearchInventorySource` - Wiz saved reports for OpenSearch
- `mock.MockInventorySource` - For testing

**How to extend:**
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ Version Guard implements a **two-stage detection pipeline**:
Currently implemented:
- **Aurora** (RDS MySQL/PostgreSQL) - AWS RDS EOL API + Wiz inventory
- **EKS** (Kubernetes) - AWS EKS API + endoflife.date (hybrid) + Wiz inventory
- **ElastiCache** (Redis/Valkey/Memcached) - endoflife.date + Wiz inventory
- **OpenSearch** - endoflife.date + Wiz inventory

Easily extensible to:
- ElastiCache (Redis/Valkey/Memcached)
- OpenSearch
- Lambda (Node.js, Python, Java)
- Cloud SQL (GCP)
- GKE (GCP)
Expand Down
19 changes: 19 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/block/Version-Guard/pkg/detector/aurora"
"github.com/block/Version-Guard/pkg/detector/eks"
"github.com/block/Version-Guard/pkg/detector/opensearch"
"github.com/block/Version-Guard/pkg/eol"
eolendoflife "github.com/block/Version-Guard/pkg/eol/endoflife"
"github.com/block/Version-Guard/pkg/inventory"
Expand Down Expand Up @@ -51,6 +52,7 @@ type ServerCLI struct {
WizAuroraReportID string `help:"Wiz saved report ID for Aurora inventory" env:"WIZ_AURORA_REPORT_ID"`
WizElastiCacheReportID string `help:"Wiz saved report ID for ElastiCache inventory" env:"WIZ_ELASTICACHE_REPORT_ID"`
WizEKSReportID string `help:"Wiz saved report ID for EKS inventory" env:"WIZ_EKS_REPORT_ID"`
WizOpenSearchReportID string `help:"Wiz saved report ID for OpenSearch inventory" env:"WIZ_OPENSEARCH_REPORT_ID"`

// AWS configuration (for EOL APIs)
AWSRegion string `help:"AWS region for EOL APIs" default:"us-west-2" env:"AWS_REGION"`
Expand Down Expand Up @@ -198,6 +200,11 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
WithTagConfig(tagConfig)
fmt.Println("✓ EKS inventory source configured (Wiz)")
}
if s.WizOpenSearchReportID != "" {
invSources[types.ResourceTypeOpenSearch] = wiz.NewOpenSearchInventorySource(wizClient, s.WizOpenSearchReportID).
WithTagConfig(tagConfig)
fmt.Println("✓ OpenSearch inventory source configured (Wiz)")
}
} else {
// No Wiz credentials — use mock inventory
fmt.Println("⚠️ No Wiz credentials configured — using mock inventory data")
Expand Down Expand Up @@ -251,6 +258,10 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
eolProviders[types.ResourceTypeElastiCache] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL)
fmt.Println("✓ ElastiCache EOL provider configured (endoflife.date API)")

// OpenSearch EOL provider
eolProviders[types.ResourceTypeOpenSearch] = eolendoflife.NewProvider(eolHTTPClient, cacheTTL)
fmt.Println("✓ OpenSearch EOL provider configured (endoflife.date API)")

// Initialize policy engine
policyEngine := policy.NewDefaultPolicy()

Expand All @@ -264,6 +275,14 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
)
fmt.Println("✓ Aurora detector initialized")
}
if invSources[types.ResourceTypeOpenSearch] != nil && eolProviders[types.ResourceTypeOpenSearch] != nil {
detectors[types.ResourceTypeOpenSearch] = opensearch.NewDetector(
invSources[types.ResourceTypeOpenSearch],
eolProviders[types.ResourceTypeOpenSearch],
policyEngine,
)
fmt.Println("✓ OpenSearch detector initialized")
}
// Note: ElastiCache detector not yet implemented in open-source version
// You can implement it by following the pattern in pkg/detector/aurora/
if invSources[types.ResourceTypeEKS] != nil && eolProviders[types.ResourceTypeEKS] != nil {
Expand Down
115 changes: 115 additions & 0 deletions pkg/detector/opensearch/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package opensearch

import (
"context"
"fmt"
"time"

"github.com/pkg/errors"

"github.com/block/Version-Guard/pkg/eol"
"github.com/block/Version-Guard/pkg/inventory"
"github.com/block/Version-Guard/pkg/policy"
"github.com/block/Version-Guard/pkg/types"
)

// Detector implements version drift detection for OpenSearch clusters
type Detector struct {
inventory inventory.InventorySource
eol eol.Provider
policy policy.VersionPolicy
}

// NewDetector creates a new OpenSearch detector
func NewDetector(
inventory inventory.InventorySource,
eol eol.Provider,
policy policy.VersionPolicy,
) *Detector {
return &Detector{
inventory: inventory,
eol: eol,
policy: policy,
}
}

// Name returns the name of this detector
func (d *Detector) Name() string {
return "opensearch-detector"
}

// ResourceType returns the resource type this detector handles
func (d *Detector) ResourceType() types.ResourceType {
return types.ResourceTypeOpenSearch
}

// Detect scans OpenSearch resources and detects version drift
func (d *Detector) Detect(ctx context.Context) ([]*types.Finding, error) {
// Step 1: Fetch inventory
resources, err := d.inventory.ListResources(ctx, types.ResourceTypeOpenSearch)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch OpenSearch inventory")
}

if len(resources) == 0 {
// No resources to scan
return []*types.Finding{}, nil
}

// Step 2: For each resource, fetch EOL data and classify
var findings []*types.Finding
for _, resource := range resources {
finding, err := d.detectResource(ctx, resource)
if err != nil {
// Log error but continue with other resources
// TODO: add proper logging
_ = fmt.Sprintf("failed to detect drift for %s: %v", resource.ID, err)
continue
}

if finding != nil {
findings = append(findings, finding)
}
}

return findings, nil
}

// detectResource detects drift for a single resource
func (d *Detector) detectResource(ctx context.Context, resource *types.Resource) (*types.Finding, error) {
// Fetch EOL data for this version
lifecycle, err := d.eol.GetVersionLifecycle(ctx, resource.Engine, resource.CurrentVersion)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch EOL data for %s %s",
resource.Engine, resource.CurrentVersion)
}

// Classify using policy
status := d.policy.Classify(resource, lifecycle)

// Generate message and recommendation
message := d.policy.GetMessage(resource, lifecycle, status)
recommendation := d.policy.GetRecommendation(resource, lifecycle, status)

// Create finding
finding := &types.Finding{
ResourceID: resource.ID,
ResourceName: resource.Name,
ResourceType: resource.Type,
Service: resource.Service,
CloudAccountID: resource.CloudAccountID,
CloudRegion: resource.CloudRegion,
CloudProvider: resource.CloudProvider,
Brand: resource.Brand,
CurrentVersion: resource.CurrentVersion,
Engine: resource.Engine,
Status: status,
Message: message,
Recommendation: recommendation,
EOLDate: lifecycle.EOLDate,
DetectedAt: time.Now(),
UpdatedAt: time.Now(),
}

return finding, nil
}
Loading
Loading