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
90 changes: 54 additions & 36 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

## πŸ“Š Implementation Status

**Production-Tested Resources** (config-driven, zero code changes needed):
- βœ… **Aurora MySQL** - Production tested (config ready, awaiting endoflife.date data)
- βœ… **Aurora PostgreSQL** - Config ready, requires separate Wiz report ID
- βœ… **EKS** - Production tested (policy classification working)
- βœ… **ElastiCache (Redis/Valkey/Memcached)** - Production tested

**Planned Resources** (add ~15 lines to `pkg/config/defaults/resources.yaml`):
- βœ… **OpenSearch** - Production tested (auto-detects legacy Elasticsearch versions)
- πŸ“‹ RDS MySQL/PostgreSQL
- πŸ“‹ Lambda runtimes
**Production-Tested Resources** (config-driven, zero code changes needed; all
ten ship in `pkg/config/defaults/resources.yaml` and run live at Block):

- βœ… **Aurora MySQL** β€” production tested via the
[`deploy/endoflife-override`](./deploy/endoflife-override/) shim while
[endoflife.date#9534](https://github.com/endoflife-date/endoflife.date/pull/9534)
is still open upstream
- βœ… **Aurora PostgreSQL**
- βœ… **EKS** β€” policy classification working (standard / extended support split)
- βœ… **ElastiCache (Redis / Valkey / Memcached)**
- βœ… **OpenSearch** β€” auto-detects legacy Elasticsearch versions
- βœ… **RDS MySQL**
- βœ… **RDS PostgreSQL**
- βœ… **Lambda runtimes** β€” JSON-extracted from `graphEntity.properties.runtime`

---

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

### Phase 1 (Implemented): AWS
- **Resources**: βœ… Aurora MySQL (production tested), βœ… Aurora PostgreSQL (config ready), βœ… EKS (production tested), βœ… ElastiCache (production tested), βœ… OpenSearch (production tested), πŸ“‹ RDS, πŸ“‹ Lambda
- **Resources** (all production tested at Block): βœ… Aurora MySQL, βœ… Aurora PostgreSQL, βœ… EKS, βœ… ElastiCache (Redis/Valkey/Memcached), βœ… OpenSearch, βœ… RDS MySQL, βœ… RDS PostgreSQL, βœ… Lambda
- **Inventory**: Wiz saved reports (primary) + Custom sources (extensible)
- **EOL Data**: endoflife.date API (404 graceful degradation for products not yet listed)

Expand Down Expand Up @@ -180,37 +184,38 @@ export WIZ_REPORT_IDS='{
```
Version-Guard/
β”œβ”€β”€ cmd/
β”‚ β”œβ”€β”€ server/main.go # Server with Temporal worker + HTTP admin
β”‚ β”œβ”€β”€ server/main.go # Server: Temporal worker + HTTP admin
β”‚ └── cli/main.go # CLI tool for operators
β”‚
β”œβ”€β”€ config/
β”‚ └── resources.yaml # Config-driven resource definitions
β”‚
β”œβ”€β”€ pkg/
β”‚ β”œβ”€β”€ types/
β”‚ β”‚ β”œβ”€β”€ resource.go # Core types: Resource, Finding
β”‚ β”‚ β”œβ”€β”€ status.go # Status enum (Red/Yellow/Green)
β”‚ β”‚ β”œβ”€β”€ snapshot.go # Snapshot + SnapshotSummary types
β”‚ β”‚ β”œβ”€β”€ status.go # Status enum (Red/Yellow/Green/Unknown)
β”‚ β”‚ └── cloud.go # CloudProvider enum
β”‚ β”‚
β”‚ β”œβ”€β”€ config/
β”‚ β”‚ β”œβ”€β”€ types.go # Configuration schema
β”‚ β”‚ └── loader.go # Config loader and validator
β”‚ β”‚ β”œβ”€β”€ loader.go # Config loader and validator
β”‚ β”‚ β”œβ”€β”€ transforms.go # Transforms DSL (see TRANSFORMS.md)
β”‚ β”‚ └── defaults/resources.yaml # Embedded default resource catalog
β”‚ β”‚
β”‚ β”œβ”€β”€ inventory/
β”‚ β”‚ β”œβ”€β”€ inventory.go # InventorySource interface
β”‚ β”‚ β”œβ”€β”€ wiz/ # Wiz implementation (multi-cloud)
β”‚ β”‚ β”‚ β”œβ”€β”€ generic.go # Generic config-driven inventory source
β”‚ β”‚ β”‚ β”œβ”€β”€ client.go # Wiz HTTP client
β”‚ β”‚ β”‚ └── helpers.go # CSV parsing, tag extraction
β”‚ β”‚ β”‚ β”œβ”€β”€ client.go / http_client.go # Wiz API + HTTP layer
β”‚ β”‚ β”‚ β”œβ”€β”€ helpers.go / tags.go # CSV parsing, tag extraction
β”‚ β”‚ β”‚ └── transforms.go # Transforms applier (uses pkg/config DSL)
β”‚ β”‚ └── mock/ # Mock for tests
β”‚ β”‚
β”‚ β”œβ”€β”€ eol/
β”‚ β”‚ β”œβ”€β”€ provider.go # EOLProvider interface
β”‚ β”‚ β”œβ”€β”€ endoflife/
β”‚ β”‚ β”‚ β”œβ”€β”€ client.go # endoflife.date HTTP client
β”‚ β”‚ β”‚ β”œβ”€β”€ provider.go # endoflife.date provider
β”‚ β”‚ β”‚ β”œβ”€β”€ adapters.go # Schema adapters (standard, EKS)
β”‚ β”‚ β”‚ └── ADAPTERS.md # Why EKS needs its own adapter (gotcha doc)
β”‚ β”‚ β”‚ β”œβ”€β”€ adapters.go # Schema adapters (standard, declarative)
β”‚ β”‚ β”‚ └── ADAPTERS.md # Lifecycle schema reference (standard vs declarative)
β”‚ β”‚ └── mock/ # Mock for tests
β”‚ β”‚
β”‚ β”œβ”€β”€ policy/
Expand All @@ -219,35 +224,36 @@ Version-Guard/
β”‚ β”‚
β”‚ β”œβ”€β”€ store/
β”‚ β”‚ β”œβ”€β”€ store.go # Store interface
β”‚ β”‚ └── memory/
β”‚ β”‚ └── store.go # In-memory implementation
β”‚ β”‚ └── memory/store.go # In-memory implementation
β”‚ β”‚
β”‚ β”œβ”€β”€ snapshot/
β”‚ β”‚ β”œβ”€β”€ builder.go # Snapshot JSON builder
β”‚ β”‚ └── store.go # S3 storage operations
β”‚ β”‚ β”œβ”€β”€ store.go # S3 snapshot store
β”‚ β”‚ └── memory_store.go # In-process snapshot store (laptop / CI)
β”‚ β”‚
β”‚ β”œβ”€β”€ workflow/
β”‚ β”‚ β”œβ”€β”€ detection/
β”‚ β”‚ β”‚ β”œβ”€β”€ workflow.go # Detection workflow (single resource type)
β”‚ β”‚ β”‚ └── activities.go # Inventory, EOL, detection activities
β”‚ β”‚ └── orchestrator/
β”‚ β”‚ β”œβ”€β”€ workflow.go # Main orchestrator (fan-out)
β”‚ β”‚ └── activities.go # Snapshot storage activity
β”‚ β”‚ β”œβ”€β”€ activities.go # Snapshot storage activity
β”‚ β”‚ └── notify.go # Optional out-of-process emitter webhook
β”‚ β”‚
β”‚ β”œβ”€β”€ scan/
β”‚ β”‚ β”œβ”€β”€ scan.go # Scan trigger (HTTP + CLI)
β”‚ β”‚ β”œβ”€β”€ scan.go # Scan trigger (shared by HTTP + CLI)
β”‚ β”‚ └── handler.go # HTTP handler for POST /scan
β”‚ β”‚
β”‚ β”œβ”€β”€ schedule/schedule.go # Temporal Schedule create-or-update wiring
β”‚ β”‚
β”‚ β”œβ”€β”€ emitters/
β”‚ β”‚ β”œβ”€β”€ emitters.go # Emitter interfaces (for custom implementations)
β”‚ β”‚ └── examples/
β”‚ β”‚ └── logging_emitter.go # Example logging emitter
β”‚ β”‚ └── examples/logging_emitter.go # Reference logging emitter
β”‚ β”‚
β”‚ └── registry/
β”‚ └── client.go # Service attribution interface
β”‚ └── registry/client.go # Service attribution interface (optional)
β”‚
└── docs/
└── examples/ # Usage examples
β”œβ”€β”€ charts/version-guard/ # Helm chart
└── deploy/ # Dockerfile + endoflife.date override shim
```

---
Expand Down Expand Up @@ -466,22 +472,32 @@ s3://your-bucket/snapshots/

### Snapshot Schema

Snapshots are produced via Go's `encoding/json` defaults. Top-level fields on
`Snapshot` and `SnapshotSummary` carry explicit `snake_case` JSON tags
(see [pkg/types/snapshot.go](./pkg/types/snapshot.go)); per-`Finding` fields
have no JSON tags and therefore serialize as PascalCase. The `findings_by_type`
map is keyed by the resource **config ID** (e.g. `aurora-mysql`, `eks`,
`elasticache-redis`) β€” the uppercase `ResourceType` constants in
`pkg/types/resource.go` are test fixtures only.

```json
{
"snapshot_id": "scan-2026-04-09-123456",
"version": "v3",
"generated_at": "2026-04-09T12:34:56Z",
"scan_start_time": "2026-04-09T12:00:00Z",
"scan_end_time": "2026-04-09T12:34:56Z",
"scan_duration_sec": 2096,
"findings_by_type": {
"AURORA": [...],
"EKS": [...]
"aurora-mysql": [{"ResourceID": "...", "Status": "RED", "Message": "..."}],
"eks": [{"ResourceID": "...", "Status": "YELLOW", "Message": "..."}]
},
"summary": {
"total_resources": 150,
"red_count": 12,
"yellow_count": 35,
"green_count": 103,
"unknown_count": 0,
"compliance_percentage": 68.7,
"by_service": {...},
"by_cloud_provider": {...}
Expand All @@ -500,8 +516,10 @@ def handler(event, context):
data = json.loads(snapshot['Body'].read())

# Send to your issue tracker, dashboard, etc.
for finding in data['findings_by_type']['AURORA']:
if finding['status'] == 'RED':
# findings_by_type is keyed by resource config ID; Finding fields are
# PascalCase (no JSON tags on pkg/types.Finding).
for finding in data['findings_by_type'].get('aurora-mysql', []):
if finding['Status'] == 'RED':
create_jira_ticket(finding)
```

Expand Down
86 changes: 57 additions & 29 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,33 +106,44 @@ make dev
- Use table-driven tests where appropriate
- Mock external dependencies

Example:
Example (table-driven, mirrors the style used across `pkg/policy`,
`pkg/config`, and `pkg/eol/endoflife`):

```go
func TestDetector_Detect(t *testing.T) {
func TestPolicy_Classify(t *testing.T) {
tests := []struct {
name string
input *types.Resource
want *types.Finding
wantErr bool
name string
resource *types.Resource
lifecycle *types.VersionLifecycle
want types.Status
}{
{
name: "detects red status",
name: "past EOL classifies RED",
// ...
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// ...
got := policy.NewDefaultPolicy().Classify(tt.resource, tt.lifecycle)
if got != tt.want {
t.Fatalf("Classify() = %s, want %s", got, tt.want)
}
})
}
}
```

### Integration Tests

- Tag integration tests with `// +build integration`
- Require actual external dependencies (Wiz, AWS, etc.)
- Document setup requirements
- Tag integration tests with the modern build constraint β€”
`//go:build integration` on the first line, optionally followed by the legacy
`// +build integration` for older toolchains. See
`pkg/eol/endoflife/integration_test.go` for the reference pattern.
- Require actual external dependencies (Wiz, endoflife.date, AWS, etc.) and
are excluded from `make test` by default; run them with
`make test-integration`.
- Document setup requirements (env vars, credentials) in the test file's
header comment.

## Using AI Skills to Add Resources

Expand Down Expand Up @@ -203,31 +214,48 @@ changes**.
```
Version-Guard/
β”œβ”€β”€ cmd/
β”‚ β”œβ”€β”€ server/ # Main server binary
β”‚ β”œβ”€β”€ server/ # Main server binary (worker + HTTP admin)
β”‚ └── cli/ # CLI tool
β”œβ”€β”€ pkg/
β”‚ β”œβ”€β”€ types/ # Core data structures
β”‚ β”œβ”€β”€ policy/ # Classification policies
β”‚ β”œβ”€β”€ inventory/ # Inventory sources (Wiz, mock)
β”‚ β”œβ”€β”€ types/ # Core data structures (Resource, Finding, Snapshot, Status)
β”‚ β”œβ”€β”€ config/ # YAML loader, transforms DSL, embedded defaults
β”‚ β”‚ └── defaults/ # Canonical resources.yaml shipped with the binary
β”‚ β”œβ”€β”€ policy/ # Classification policy (RED/YELLOW/GREEN/UNKNOWN)
β”‚ β”œβ”€β”€ inventory/ # Inventory sources (Wiz generic source + mock)
β”‚ β”œβ”€β”€ eol/ # EOL data providers (endoflife.date + schema adapters)
β”‚ β”œβ”€β”€ store/ # Finding storage
β”‚ β”œβ”€β”€ snapshot/ # S3 snapshot management
β”‚ β”œβ”€β”€ workflow/ # Temporal workflows
β”‚ β”œβ”€β”€ scan/ # Scan trigger (HTTP + CLI)
β”‚ └── emitters/ # Emitter interfaces + examples
β”œβ”€β”€ docs/ # Documentation
└── .github/ # GitHub workflows
β”‚ β”œβ”€β”€ registry/ # Optional registry client for service lookups
β”‚ β”œβ”€β”€ store/ # Finding storage interface (in-memory implementation)
β”‚ β”œβ”€β”€ snapshot/ # Snapshot builder + store (S3 / in-memory backends)
β”‚ β”œβ”€β”€ workflow/ # Temporal workflows (orchestrator + detection)
β”‚ β”œβ”€β”€ scan/ # Scan trigger (shared by HTTP /scan and CLI)
β”‚ β”œβ”€β”€ schedule/ # Temporal Schedule create-or-update wiring
β”‚ └── emitters/ # Emitter interfaces + reference logging emitter
β”œβ”€β”€ charts/version-guard/ # Helm chart
β”œβ”€β”€ deploy/ # Dockerfile + endoflife.date override shim
└── .github/ # GitHub Actions workflows
```

## Release Process

Releases are managed by maintainers:

1. Update version in relevant files
2. Update CHANGELOG.md
3. Create a git tag: `git tag -a v1.0.0 -m "Release v1.0.0"`
4. Push tag: `git push origin v1.0.0`
5. GitHub Actions will build and publish release artifacts
Releases are managed by maintainers. The container image and Helm chart are
cut from a single `vX.Y.Z` git tag by the
[`Docker & Helm` workflow](./.github/workflows/docker.yml):

1. If the chart has changed, bump `version` (and `appVersion` if the image
changed) in
[charts/version-guard/Chart.yaml](./charts/version-guard/Chart.yaml).
Land the bump on `main` as a `chore(release)` PR.
2. Tag and push: `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push origin vX.Y.Z`.
The tag's version **must equal** `charts/version-guard/Chart.yaml`'s
`version` whenever the chart changed since the previous tag β€” the workflow
fails the release otherwise.
3. The workflow then:
- builds and pushes a multi-arch container image to
`ghcr.io/block/Version-Guard` (semver + `latest` tags), and
- if the chart changed since the previous `v*` tag, packages and pushes
the Helm chart to `oci://ghcr.io/block/charts`.

There is no `CHANGELOG.md` β€” release notes live on the GitHub Releases page.

## Questions?

Expand Down
Loading
Loading