Skip to content

Commit 60b96bb

Browse files
reviedclaude
andcommitted
refactor: use shared parseWizReport helper for OpenSearch inventory
Address review feedback to leverage the shared CSV parsing infrastructure instead of duplicating the column-index logic. Also updates README with currently supported resource types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4c86440 commit 60b96bb

2 files changed

Lines changed: 30 additions & 114 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ Version Guard implements a **two-stage detection pipeline**:
6666
Currently implemented:
6767
- **Aurora** (RDS MySQL/PostgreSQL) - AWS RDS EOL API + Wiz inventory
6868
- **EKS** (Kubernetes) - AWS EKS API + endoflife.date (hybrid) + Wiz inventory
69+
- **ElastiCache** (Redis/Valkey/Memcached) - endoflife.date + Wiz inventory
70+
- **OpenSearch** - endoflife.date + Wiz inventory
6971

7072
Easily extensible to:
71-
- ElastiCache (Redis/Valkey/Memcached)
72-
- OpenSearch
7373
- Lambda (Node.js, Python, Java)
7474
- Cloud SQL (GCP)
7575
- GKE (GCP)

pkg/inventory/wiz/opensearch.go

Lines changed: 28 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ import (
44
"context"
55
"fmt"
66
"strings"
7-
"time"
8-
9-
"github.com/aws/aws-sdk-go-v2/aws/arn"
10-
"github.com/pkg/errors"
117

128
"github.com/block/Version-Guard/pkg/registry"
139
"github.com/block/Version-Guard/pkg/types"
@@ -32,7 +28,6 @@ func NewOpenSearchInventorySource(client *Client, reportID string) *OpenSearchIn
3228
}
3329

3430
// WithRegistryClient adds optional registry integration for service attribution.
35-
// When tags are missing, the registry will be queried to map AWS account → service.
3631
func (s *OpenSearchInventorySource) WithRegistryClient(registryClient registry.Client) *OpenSearchInventorySource {
3732
s.registryClient = registryClient
3833
return s
@@ -57,58 +52,37 @@ func (s *OpenSearchInventorySource) CloudProvider() types.CloudProvider {
5752
return types.CloudProviderAWS
5853
}
5954

55+
var openSearchRequiredColumns = []string{
56+
colHeaderExternalID,
57+
colHeaderName,
58+
colHeaderNativeType,
59+
colHeaderAccountID,
60+
colHeaderVersion,
61+
colHeaderRegion,
62+
colHeaderTags,
63+
colHeaderEngineKind,
64+
}
65+
6066
// ListResources fetches all OpenSearch resources from Wiz
61-
//
62-
//nolint:dupl // follows established inventory source pattern (aurora, elasticache)
6367
func (s *OpenSearchInventorySource) ListResources(ctx context.Context, resourceType types.ResourceType) ([]*types.Resource, error) {
6468
if resourceType != types.ResourceTypeOpenSearch {
6569
return nil, fmt.Errorf("unsupported resource type: %s (only OPENSEARCH supported)", resourceType)
6670
}
6771

68-
// Fetch report data
69-
rows, err := s.client.GetReportData(ctx, s.reportID)
70-
if err != nil {
71-
return nil, errors.Wrap(err, "failed to fetch Wiz report data")
72-
}
73-
74-
if len(rows) < 2 {
75-
// Empty report (only header row)
76-
return []*types.Resource{}, nil
77-
}
78-
79-
// Skip header row, parse data rows
80-
var resources []*types.Resource
81-
for i, row := range rows[1:] {
82-
if len(row) < colMinRequired {
83-
// Skip malformed rows
84-
continue
85-
}
86-
87-
// Filter for OpenSearch domain resources only
88-
nativeType := row[colNativeType]
89-
if !isOpenSearchResource(nativeType) {
90-
continue
91-
}
92-
93-
resource, err := s.parseOpenSearchRow(ctx, row)
94-
if err != nil {
95-
// Log error but continue processing other rows
96-
// TODO: add proper logging
97-
_ = fmt.Sprintf("row %d: failed to parse OpenSearch resource: %v", i+1, err)
98-
continue
99-
}
100-
101-
if resource != nil {
102-
resources = append(resources, resource)
103-
}
104-
}
105-
106-
return resources, nil
72+
return parseWizReport(
73+
ctx,
74+
s.client,
75+
s.reportID,
76+
openSearchRequiredColumns,
77+
func(cols columnIndex, row []string) bool {
78+
return isOpenSearchResource(cols.col(row, colHeaderNativeType))
79+
},
80+
s.parseOpenSearchRow,
81+
)
10782
}
10883

10984
// GetResource fetches a specific OpenSearch resource by ARN
11085
func (s *OpenSearchInventorySource) GetResource(ctx context.Context, resourceType types.ResourceType, id string) (*types.Resource, error) {
111-
// For Wiz source, we fetch all and filter
11286
resources, err := s.ListResources(ctx, resourceType)
11387
if err != nil {
11488
return nil, err
@@ -123,73 +97,15 @@ func (s *OpenSearchInventorySource) GetResource(ctx context.Context, resourceTyp
12397
return nil, fmt.Errorf("resource not found: %s", id)
12498
}
12599

126-
// parseOpenSearchRow parses a single CSV row into a Resource
127-
func (s *OpenSearchInventorySource) parseOpenSearchRow(ctx context.Context, row []string) (*types.Resource, error) {
128-
resourceARN := row[colARN]
129-
if resourceARN == "" {
130-
return nil, fmt.Errorf("missing ARN")
131-
}
132-
133-
// Parse ARN
134-
parsedARN, err := arn.Parse(resourceARN)
100+
// parseOpenSearchRow parses a single CSV row into a Resource.
101+
// Uses the shared parseAWSResourceRow helper, then normalizes the version
102+
// to strip the "OpenSearch_" prefix that Wiz sometimes includes.
103+
func (s *OpenSearchInventorySource) parseOpenSearchRow(ctx context.Context, cols columnIndex, row []string) (*types.Resource, error) {
104+
resource, err := parseAWSResourceRow(ctx, cols, row, types.ResourceTypeOpenSearch, normalizeOpenSearchKind, s.tagConfig, s.registryClient)
135105
if err != nil {
136-
return nil, errors.Wrapf(err, "invalid ARN: %s", resourceARN)
137-
}
138-
139-
// Extract metadata
140-
resourceName := row[colResourceName]
141-
accountID := row[colAWSAccountID]
142-
if accountID == "" {
143-
accountID = parsedARN.AccountID
144-
}
145-
146-
engine := normalizeOpenSearchKind(row[colEngineKind])
147-
version := normalizeOpenSearchVersion(row[colEngineVersion])
148-
region := row[colRegion]
149-
150-
// Parse tags
151-
tagsJSON := row[colTags]
152-
tags, err := ParseTags(tagsJSON)
153-
if err != nil {
154-
// Non-fatal, just use empty tags
155-
tags = make(map[string]string)
156-
}
157-
158-
// Extract service name from tags (using configurable tag keys)
159-
service := s.tagConfig.GetAppTag(tags)
160-
if service == "" {
161-
// Try registry lookup by AWS account (if registry is configured)
162-
if s.registryClient != nil {
163-
if serviceInfo, err := s.registryClient.GetServiceByAWSAccount(ctx, accountID, region); err == nil {
164-
service = serviceInfo.ServiceName
165-
}
166-
// Ignore registry errors - fall through to name parsing
167-
}
168-
169-
// Final fallback: extract from resource name or ARN
170-
if service == "" {
171-
service = extractServiceFromName(resourceName)
172-
}
173-
}
174-
175-
// Extract brand (using configurable tag keys)
176-
brand := s.tagConfig.GetBrandTag(tags)
177-
178-
resource := &types.Resource{
179-
ID: resourceARN,
180-
Name: resourceName,
181-
Type: types.ResourceTypeOpenSearch,
182-
CloudProvider: types.CloudProviderAWS,
183-
Service: service,
184-
CloudAccountID: accountID,
185-
CloudRegion: region,
186-
Brand: brand,
187-
CurrentVersion: version,
188-
Engine: engine,
189-
Tags: tags,
190-
DiscoveredAt: time.Now(),
106+
return nil, err
191107
}
192-
108+
resource.CurrentVersion = normalizeOpenSearchVersion(resource.CurrentVersion)
193109
return resource, nil
194110
}
195111

0 commit comments

Comments
 (0)