Skip to content

Commit 484b10a

Browse files
fix: use endoflife.date for all EOL data, fix Temporal payload limit, and improve classification (#17)
## Summary Fixes the detection pipeline to produce real, usable compliance data for EKS and ElastiCache using only [endoflife.date](https://endoflife.date) for EOL data — no AWS API credentials needed. ### What changed **Temporal payload fix** - Removed `RetrieveFindings` activity that returned all findings through Temporal gRPC (12K+ findings = 10MB, exceeds 4MB limit) - `CreateSnapshot` now reads directly from the in-memory store — findings never transit Temporal - Fails hard on empty snapshots (all workflows failed) or store read errors **EOL data from endoflife.date only** - `ProductCycle.EOL` and `.Support` changed from `string` to `any` (endoflife.date returns booleans or date strings) - Enabled EKS via endoflife.date (was blocked by `ProductsWithNonStandardSchema`) - Fixed `aurora-postgresql` mapping to `amazon-aurora-postgresql` - Added `aurora-mysql` mapping (pending [endoflife.date#9534](endoflife-date/endoflife.date#9534)) - Removed `ProductsWithNonStandardSchema` blocklist and dead code **Version matching** - EOL provider uses prefix matching: cycle `8.0` matches resource version `8.0.35` - Policy uses prefix matching with `k8s-` prefix normalization **Classification** - Extended support is now YELLOW, not RED (check order fix + policy fix) - EKS 1.30/1.32 correctly show "in extended support (6x standard cost)" - ElastiCache Redis 5.0.6 correctly shows extended support ### Validated with docker-compose (real Wiz data) | Resource | Findings | Yellow | Green | Unknown | |----------|----------|--------|-------|---------| | EKS | 155 | 90 (1.30, 1.32 in ext. support) | 65 | 0 | | ElastiCache | 3,974 | 138 (Redis 5.0.6) | 3,739 | 97 | | Aurora MySQL | 12,238 | 0 | 0 | 12,238 (no EOL source yet) | ### Single-pod assumption The in-memory store is shared between detection and snapshot activities because everything runs on a single Temporal worker. This is intentional — we don't anticipate scaling this service to multiple pods. --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent 550f2fb commit 484b10a

9 files changed

Lines changed: 170 additions & 183 deletions

File tree

README.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,44 +45,45 @@ Version Guard implements a **two-stage detection pipeline**:
4545
```
4646

4747
**Key Components:**
48-
- **Inventory Sources**: Wiz (multi-cloud scanning), mock sources for testing
49-
- **EOL Providers**: AWS APIs (RDS, EKS) + endoflife.date (fallback)
50-
- **Detectors**: Resource-specific detection logic (Aurora, EKS currently implemented)
48+
- **Inventory Sources**: [Wiz](https://wiz.io) saved reports for resource discovery (multi-cloud)
49+
- **EOL Data**: [endoflife.date](https://endoflife.date) API — no cloud provider credentials needed
5150
- **Classification**: Red (EOL/deprecated), Yellow (extended support/approaching EOL), Green (current)
5251
- **S3 Snapshots**: Versioned JSON storage for audit trail and downstream consumption
5352
- **gRPC API**: Query interface for compliance dashboards
5453

5554
## ✨ Features
5655

5756
-**Multi-Cloud Inventory**: Wiz integration for AWS, GCP, Azure resource discovery
58-
-**Hybrid EOL Data**: AWS native APIs + endoflife.date for comprehensive coverage
57+
-**Open EOL Data**: All EOL data from [endoflife.date](https://endoflife.date) — no cloud provider credentials needed
5958
-**Parallel Detection**: Temporal-based workflows for scalable scanning
6059
-**Versioned Snapshots**: S3 storage with full audit history
61-
-**gRPC Query API**: 3 endpoints for compliance scoring, finding details, fleet summaries
60+
-**Local Development**: Full docker-compose setup with MinIO (S3) and Temporal
6261
-**Extensible Architecture**: Plugin your own emitters for issue tracking, dashboards, notifications
6362

6463
## 📦 Supported Resources
6564

66-
Currently implemented:
67-
- **Aurora** (RDS MySQL/PostgreSQL) - AWS RDS EOL API + Wiz inventory
68-
- **EKS** (Kubernetes) - AWS EKS API + endoflife.date (hybrid) + Wiz inventory
69-
70-
Easily extensible to:
71-
- ElastiCache (Redis/Valkey/Memcached)
72-
- OpenSearch
73-
- Lambda (Node.js, Python, Java)
74-
- Cloud SQL (GCP)
75-
- GKE (GCP)
76-
- Azure resources
65+
| Resource | Inventory | EOL Source | Code | Status |
66+
|----------|-----------|------------|------|--------|
67+
| **EKS** (Kubernetes) | Wiz | [amazon-eks](https://endoflife.date/amazon-eks) | ✅ Implemented | ✅ Working |
68+
| **ElastiCache** (Redis/Valkey) | Wiz | [amazon-elasticache-redis](https://endoflife.date/amazon-elasticache-redis), [valkey](https://endoflife.date/valkey) | ✅ Implemented | ✅ Working |
69+
| **Aurora PostgreSQL** | Wiz | [amazon-aurora-postgresql](https://endoflife.date/amazon-aurora-postgresql) | ✅ Implemented | 🔜 Needs Wiz report with PostgreSQL data |
70+
| **Aurora MySQL** | Wiz | [amazon-aurora-mysql](https://endoflife.date/amazon-aurora-mysql) | ✅ Implemented | 🔜 EOL data pending [endoflife.date#9534](https://github.com/endoflife-date/endoflife.date/pull/9534) |
71+
| **RDS MySQL** || [amazon-rds-mysql](https://endoflife.date/amazon-rds-mysql) | ❌ Needs Wiz report | 📋 Planned |
72+
| **RDS PostgreSQL** || [amazon-rds-postgresql](https://endoflife.date/amazon-rds-postgresql) | ❌ Needs Wiz report | 📋 Planned |
73+
| **OpenSearch** || [amazon-opensearch](https://endoflife.date/amazon-opensearch) | ❌ Needs Wiz report | 📋 Planned |
74+
| **Lambda** || [aws-lambda](https://endoflife.date/aws-lambda) | ❌ Needs Wiz report | 📋 Planned |
75+
76+
Adding a new resource type requires:
77+
1. A Wiz saved report + inventory source (~100 lines)
78+
2. One line in `ProductMapping` to map the engine name to endoflife.date
7779

7880
## 🚀 Quick Start
7981

8082
### Prerequisites
8183

8284
- **Go 1.24+**
83-
- **Docker** (for local Temporal server)
84-
- **AWS credentials** (for EOL APIs - optional but recommended)
85-
- **Wiz API access** (optional - falls back to mock data)
85+
- **Docker** (for docker-compose local setup)
86+
- **Wiz API access** (optional — falls back to mock data)
8687

8788
### Installation
8889

@@ -150,11 +151,13 @@ make dev
150151
### Trigger a Scan
151152

152153
```bash
153-
# Via Temporal CLI
154-
temporal workflow start \
154+
# Via Temporal CLI (from inside the temporal container if using docker-compose)
155+
docker compose exec temporal temporal workflow start \
155156
--task-queue version-guard-detection \
156-
--type VersionGuardOrchestratorWorkflow \
157-
--input '{}'
157+
--type OrchestratorWorkflow \
158+
--input '{}' \
159+
--address localhost:7233 \
160+
--namespace version-guard-dev
158161

159162
# Or via the Temporal Web UI at http://localhost:8233 → Start Workflow
160163
```
@@ -196,7 +199,7 @@ Version Guard is configured via environment variables or CLI flags:
196199
| `TEMPORAL_NAMESPACE` | Temporal namespace | `version-guard-dev` |
197200
| `GRPC_PORT` | gRPC service port | `8080` |
198201
| `S3_BUCKET` | S3 bucket for snapshots | `version-guard-snapshots` |
199-
| `AWS_REGION` | AWS region for EOL APIs | `us-west-2` |
202+
| `AWS_REGION` | AWS region (for S3 snapshots) | `us-west-2` |
200203
| `WIZ_CLIENT_ID_SECRET` | Wiz client ID (optional) | - |
201204
| `WIZ_CLIENT_SECRET_SECRET` | Wiz client secret (optional) | - |
202205
| `TAG_APP_KEYS` | Comma-separated AWS tag keys for app/service | `app,application,service` |
@@ -338,8 +341,7 @@ Version Guard is maintained by Block, Inc. and the open-source community.
338341
Special thanks to:
339342
- [Temporal](https://temporal.io) for the workflow orchestration framework
340343
- [Wiz](https://wiz.io) for multi-cloud security scanning
341-
- [endoflife.date](https://endoflife.date) for EOL data API
342-
- AWS for native EOL APIs (RDS, EKS)
344+
- [endoflife.date](https://endoflife.date) for open EOL data
343345

344346
---
345347

cmd/server/main.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,9 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
329329
// Orchestrator workflow activities
330330
if snapshotStore != nil {
331331
orchestratorActivities := orchestrator.NewActivities(st, snapshotStore)
332-
w.RegisterActivityWithOptions(orchestratorActivities.RetrieveFindings, activity.RegisterOptions{Name: orchestrator.RetrieveFindingsActivityName})
333332
w.RegisterActivityWithOptions(orchestratorActivities.CreateSnapshot, activity.RegisterOptions{Name: orchestrator.CreateSnapshotActivityName})
334333
fmt.Println("✓ Orchestrator activities registered (with S3)")
335334
} else {
336-
// Without S3, we can still retrieve findings but can't create snapshots
337-
orchestratorActivities := orchestrator.NewActivities(st, nil)
338-
w.RegisterActivityWithOptions(orchestratorActivities.RetrieveFindings, activity.RegisterOptions{Name: orchestrator.RetrieveFindingsActivityName})
339335
fmt.Println("⚠️ Orchestrator snapshot activity not registered (no S3 store)")
340336
}
341337

pkg/eol/aws/eks.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,11 @@ func enrichFromEndOfLife(ctx context.Context, version *EKSVersion, client endofl
328328
}
329329

330330
// End of standard support from EOL field (EKS NON-STANDARD mapping!)
331-
if version.EndOfStandardDate == nil && cycle.EOL != "" && cycle.EOL != "false" {
332-
if eolDate, err := time.Parse("2006-01-02", cycle.EOL); err == nil {
333-
version.EndOfStandardDate = &eolDate
331+
if version.EndOfStandardDate == nil {
332+
if eolStr, ok := cycle.EOL.(string); ok && eolStr != "" && eolStr != "false" {
333+
if eolDate, err := time.Parse("2006-01-02", eolStr); err == nil {
334+
version.EndOfStandardDate = &eolDate
335+
}
334336
}
335337
}
336338

pkg/eol/endoflife/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ type Client interface {
3131
type ProductCycle struct {
3232
Cycle string `json:"cycle"` // Version identifier (e.g., "1.31")
3333
ReleaseDate string `json:"releaseDate"` // Release date (YYYY-MM-DD)
34-
Support string `json:"support"` // End of standard support (YYYY-MM-DD or boolean)
35-
EOL string `json:"eol"` // End of life date (YYYY-MM-DD or boolean)
34+
Support any `json:"support"` // End of standard support (YYYY-MM-DD or boolean)
35+
EOL any `json:"eol"` // End of life date (YYYY-MM-DD or boolean)
3636
ExtendedSupport any `json:"extendedSupport"` // Extended support availability (boolean or date)
3737
LTS bool `json:"lts"` // Long-term support flag
3838
Latest string `json:"latest"` // Latest patch version

pkg/eol/endoflife/provider.go

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,26 @@ import (
1313
"github.com/block/Version-Guard/pkg/types"
1414
)
1515

16-
// ProductMapping maps internal engine names to endoflife.date product identifiers
17-
//
18-
// WARNING: This provider uses STANDARD endoflife.date field semantics:
19-
// - cycle.EOL → true end of life date
20-
// - cycle.Support → end of standard support date
21-
//
22-
// Some AWS products (e.g., EKS) use NON-STANDARD schemas on endoflife.date
23-
// and MUST use dedicated providers (e.g., EKSEOLProvider) instead of this generic provider.
24-
// These products are listed here but blocked by ProductsWithNonStandardSchema below.
16+
// ProductMapping maps engine names to endoflife.date product identifiers.
17+
// All EOL data comes from endoflife.date — no cloud provider APIs needed.
2518
var ProductMapping = map[string]string{
26-
// EKS entries are mapped but BLOCKED by ProductsWithNonStandardSchema
27-
// because EKS uses non-standard schema where cycle.EOL means "end of standard support"
28-
// not "true end of life". Use pkg/eol/aws.EKSEOLProvider instead.
2919
"kubernetes": "amazon-eks",
3020
"k8s": "amazon-eks",
3121
"eks": "amazon-eks",
3222

33-
"postgres": "amazon-rds-postgresql",
34-
"postgresql": "amazon-rds-postgresql",
35-
"mysql": "amazon-rds-mysql",
36-
"aurora-mysql": "amazon-rds-mysql",
37-
"aurora-postgresql": "amazon-rds-postgresql",
23+
"postgres": "amazon-rds-postgresql",
24+
"postgresql": "amazon-rds-postgresql",
25+
"mysql": "amazon-rds-mysql",
26+
"aurora-postgresql": "amazon-aurora-postgresql",
27+
// TODO(endoflife.date#9534): aurora-mysql → amazon-aurora-mysql once the
28+
// endoflife.date PR is merged. Until then, aurora-mysql returns UNKNOWN.
29+
"aurora-mysql": "amazon-aurora-mysql",
3830
"redis": "amazon-elasticache-redis",
3931
"elasticache-redis": "amazon-elasticache-redis",
4032
"valkey": "valkey",
4133
"elasticache-valkey": "valkey",
4234
}
4335

44-
// ProductsWithNonStandardSchema lists products that MUST NOT use this generic provider
45-
// because they use non-standard field semantics on endoflife.date.
46-
// The provider will return an error if these products are requested.
47-
var ProductsWithNonStandardSchema = []string{
48-
"amazon-eks", // cycle.EOL = end of standard support (NOT true EOL!)
49-
}
50-
5136
const (
5237
providerName = "endoflife-date-api"
5338
falseBool = "false"
@@ -114,12 +99,23 @@ func (p *Provider) GetVersionLifecycle(ctx context.Context, engine, version stri
11499
return nil, err
115100
}
116101

117-
// Find the specific version
102+
// Find the specific version — try exact match first, then prefix match.
103+
// endoflife.date uses major.minor cycles (e.g., "8.0", "7") while Wiz
104+
// reports full versions (e.g., "8.0.35", "7.1.0").
105+
var bestMatch *types.VersionLifecycle
106+
bestMatchLen := 0
118107
for _, v := range versions {
119108
normalizedV := normalizeVersion(engine, v.Version)
120109
if normalizedV == version {
121110
return v, nil
122111
}
112+
if strings.HasPrefix(version, normalizedV+".") && len(normalizedV) > bestMatchLen {
113+
bestMatch = v
114+
bestMatchLen = len(normalizedV)
115+
}
116+
}
117+
if bestMatch != nil {
118+
return bestMatch, nil
123119
}
124120

125121
// Version not found - return unknown lifecycle (empty Version signals missing data)
@@ -152,17 +148,6 @@ func (p *Provider) ListAllVersions(ctx context.Context, engine string) ([]*types
152148
return nil, fmt.Errorf("unsupported engine: %s", engine)
153149
}
154150

155-
// Guard against products with non-standard schemas
156-
// These products interpret endoflife.date fields differently and need dedicated providers
157-
for _, blockedProduct := range ProductsWithNonStandardSchema {
158-
if product == blockedProduct {
159-
return nil, fmt.Errorf(
160-
"engine %s (product: %s) uses non-standard endoflife.date schema and cannot use generic provider; use dedicated provider instead (e.g., EKSEOLProvider)",
161-
engine, product,
162-
)
163-
}
164-
}
165-
166151
// Use product as cache key
167152
cacheKey := product
168153

@@ -221,15 +206,11 @@ func (p *Provider) ListAllVersions(ctx context.Context, engine string) ([]*types
221206

222207
// convertCycle converts a ProductCycle to our VersionLifecycle type
223208
//
224-
// Field Mapping (STANDARD endoflife.date schema):
209+
// Field mapping:
225210
// - cycle.ReleaseDate → ReleaseDate
226211
// - cycle.Support → DeprecationDate (end of standard support)
227-
// - cycle.EOL → EOLDate (true end of life)
212+
// - cycle.EOL → EOLDate
228213
// - cycle.ExtendedSupport → ExtendedSupportEnd
229-
//
230-
// WARNING: This assumes STANDARD field semantics. Products with non-standard schemas
231-
// (e.g., amazon-eks where cycle.EOL means "end of standard support", not true EOL)
232-
// should be blocked by ListAllVersions and use dedicated providers instead.
233214
func (p *Provider) convertCycle(engine, product string, cycle *ProductCycle) (*types.VersionLifecycle, error) {
234215
version := cycle.Cycle
235216

@@ -249,17 +230,17 @@ func (p *Provider) convertCycle(engine, product string, cycle *ProductCycle) (*t
249230

250231
// Parse EOL date (STANDARD semantics: true end of life)
251232
var eolDate *time.Time
252-
if cycle.EOL != "" && cycle.EOL != falseBool {
253-
if parsed, err := parseDate(cycle.EOL); err == nil {
233+
if dateStr := anyToDateString(cycle.EOL); dateStr != "" {
234+
if parsed, err := parseDate(dateStr); err == nil {
254235
eolDate = &parsed
255236
lifecycle.EOLDate = eolDate
256237
}
257238
}
258239

259240
// Parse support date (STANDARD semantics: end of standard support)
260241
var supportDate *time.Time
261-
if cycle.Support != "" && cycle.Support != falseBool {
262-
if parsed, err := parseDate(cycle.Support); err == nil {
242+
if dateStr := anyToDateString(cycle.Support); dateStr != "" {
243+
if parsed, err := parseDate(dateStr); err == nil {
263244
supportDate = &parsed
264245
lifecycle.DeprecationDate = supportDate
265246
}
@@ -285,38 +266,47 @@ func (p *Provider) convertCycle(engine, product string, cycle *ProductCycle) (*t
285266
}
286267
}
287268

288-
// Determine lifecycle status based on dates
269+
// Determine lifecycle status based on dates.
270+
// For products like EKS, the "support" field is absent and "eol" means
271+
// end of standard support while "extendedSupport" is the true end of life.
272+
// We treat eolDate as the standard support boundary when supportDate is nil.
289273
now := time.Now()
290274

291-
// If we have an EOL date and we're past it, mark as EOL
292-
if eolDate != nil && now.After(*eolDate) {
293-
lifecycle.IsEOL = true
294-
lifecycle.IsSupported = false
295-
lifecycle.IsDeprecated = true
296-
return lifecycle, nil
275+
// Resolve the effective standard-support-end date
276+
standardEnd := supportDate
277+
if standardEnd == nil {
278+
standardEnd = eolDate
297279
}
298280

299-
// If we have extended support end and we're past standard support
300-
if extendedSupportDate != nil && supportDate != nil && now.After(*supportDate) {
281+
// Check extended support window first — must come before the EOL check
282+
// so that resources in extended support get YELLOW, not RED.
283+
if extendedSupportDate != nil && standardEnd != nil && now.After(*standardEnd) {
301284
if now.Before(*extendedSupportDate) {
302285
// In extended support window
303286
lifecycle.IsSupported = true
304287
lifecycle.IsExtendedSupport = true
305288
lifecycle.IsDeprecated = true
306289
} else {
307-
// Past extended support
290+
// Past extended support — truly EOL
308291
lifecycle.IsEOL = true
309292
lifecycle.IsSupported = false
310293
lifecycle.IsDeprecated = true
311294
}
312295
return lifecycle, nil
313296
}
314297

315-
// If we're past support date but no extended support info
298+
// Past EOL with no extended support available
299+
if eolDate != nil && now.After(*eolDate) {
300+
lifecycle.IsEOL = true
301+
lifecycle.IsSupported = false
302+
lifecycle.IsDeprecated = true
303+
return lifecycle, nil
304+
}
305+
306+
// Past standard support but no extended support info
316307
if supportDate != nil && now.After(*supportDate) {
317308
lifecycle.IsDeprecated = true
318309
lifecycle.IsSupported = false
319-
// If we have EOL date, use it; otherwise mark as deprecated but not EOL
320310
if eolDate != nil && now.Before(*eolDate) {
321311
lifecycle.IsEOL = false
322312
}
@@ -353,6 +343,15 @@ func normalizeVersion(engine, version string) string {
353343
return version
354344
}
355345

346+
// anyToDateString extracts a date string from an any-typed field.
347+
// endoflife.date returns EOL/Support as either a date string or a boolean.
348+
func anyToDateString(v any) string {
349+
if val, ok := v.(string); ok && val != "" && val != "false" && val != "true" {
350+
return val
351+
}
352+
return ""
353+
}
354+
356355
// parseDate parses date strings from endoflife.date API
357356
// Supports formats: YYYY-MM-DD, boolean "true"/"false"
358357
func parseDate(dateStr string) (time.Time, error) {

0 commit comments

Comments
 (0)