-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauditor.go
More file actions
216 lines (193 loc) · 6.5 KB
/
Copy pathauditor.go
File metadata and controls
216 lines (193 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Package audit scans New Relic accounts for hardcoded entity IDs and GUIDs
// that will break during account migrations.
package audit
import (
"context"
"fmt"
"io"
"sort"
"sync"
"github.com/asomensari-es/nr-tf-util/internal/ratelimit"
"github.com/asomensari-es/nr-tf-util/internal/resources"
)
// AuditFetcher scans one category of New Relic assets for hardcoded ID references.
type AuditFetcher interface {
Name() string
Fetch(ctx context.Context, ng resources.NerdGrapher, accountID int) ([]Finding, error)
}
// Finding is a single hardcoded ID reference or configuration issue found within a New Relic asset.
type Finding struct {
AssetType string // "dashboard", "nrql_condition", "workload", "service_level", "workflow"
AssetName string // human-readable asset name
AssetID string // GUID or numeric ID of the containing asset
NRQLQuery string // NRQL query where the issue was found; empty for non-query assets
Location string // e.g. "widget: Error Rate", "condition query", "static entity list"
IDType string // "entityGuid", "appId", "hostId", "entityId", "monitorId"
IDValue string // the raw hardcoded value found in the asset
EntityName string // resolved entity name; empty if unresolved
EntityType string // resolved entity type; empty if unresolved
Resolved bool // true when IDValue was matched to a live entity
Suggestion string // migration advice generated after resolution
}
// AuditResult holds all findings for one account.
type AuditResult struct {
Account resources.Account
Findings []Finding
Errors map[string]error // keyed by AuditFetcher.Name()
}
// Options controls Auditor behaviour.
type Options struct {
// Skip is a set of AuditFetcher.Name() values to exclude from the audit.
Skip map[string]bool
}
// Auditor orchestrates all audit fetchers across accounts.
type Auditor struct {
ng resources.NerdGrapher
rest LegacyConditionsClient // nil → legacy_conditions returns a static notice
limiter *ratelimit.Limiter
opts Options
out io.Writer
}
// New creates an Auditor. rest may be nil when REST API access is unavailable,
// in which case legacy_conditions emits a static notice instead of scanning.
// out receives progress lines; pass nil to silence.
func New(ng resources.NerdGrapher, rest LegacyConditionsClient, limiter *ratelimit.Limiter, opts Options, out io.Writer) *Auditor {
if out == nil {
out = io.Discard
}
return &Auditor{ng: ng, rest: rest, limiter: limiter, opts: opts, out: out}
}
// Audit runs all enabled fetchers for each account in sequence (fetchers run
// concurrently within each account). Resolution is done separately via
// Resolver.ResolveAll after this call.
func (a *Auditor) Audit(ctx context.Context, accounts []resources.Account) ([]*AuditResult, error) {
results := make([]*AuditResult, 0, len(accounts))
for _, acct := range accounts {
res, err := a.auditOne(ctx, acct)
if err != nil {
return results, fmt.Errorf("account %d (%s): %w", acct.ID, acct.Name, err)
}
results = append(results, res)
}
return results, nil
}
type fetchResult struct {
name string
findings []Finding
err error
}
// auditOne runs all enabled fetchers concurrently for a single account.
func (a *Auditor) auditOne(ctx context.Context, acct resources.Account) (*AuditResult, error) {
rlng := &rateLimitedNG{ng: a.ng, limiter: a.limiter}
fetchers := AllFetchers(a.rest)
ch := make(chan fetchResult, len(fetchers))
var wg sync.WaitGroup
for _, f := range fetchers {
if a.opts.Skip[f.Name()] {
continue
}
wg.Add(1)
go func(f AuditFetcher) {
defer wg.Done()
findings, err := f.Fetch(ctx, rlng, acct.ID)
ch <- fetchResult{name: f.Name(), findings: findings, err: err}
}(f)
}
go func() { wg.Wait(); close(ch) }()
// Buffer in map, then re-order to match AllFetchers() for deterministic output.
type entry struct {
findings []Finding
err error
}
resultMap := make(map[string]entry)
for fr := range ch {
resultMap[fr.name] = entry{findings: fr.findings, err: fr.err}
}
result := &AuditResult{Account: acct, Errors: make(map[string]error)}
for _, f := range fetchers {
if a.opts.Skip[f.Name()] {
continue
}
e, ok := resultMap[f.Name()]
if !ok {
continue
}
if e.err != nil {
result.Errors[f.Name()] = e.err
fmt.Fprintf(a.out, " %-30s ERROR: %v\n", f.Name(), e.err)
} else {
result.Findings = append(result.Findings, e.findings...)
fmt.Fprintf(a.out, " %-30s %4d finding(s)\n", f.Name(), len(e.findings))
}
}
// Sort for deterministic output: asset type → asset name → location.
sort.Slice(result.Findings, func(i, j int) bool {
a, b := result.Findings[i], result.Findings[j]
if a.AssetType != b.AssetType {
return a.AssetType < b.AssetType
}
if a.AssetName != b.AssetName {
return a.AssetName < b.AssetName
}
return a.Location < b.Location
})
return result, nil
}
// AllFetchers returns every registered AuditFetcher in display order.
// rest is passed to legacyConditionsAuditFetcher; pass nil when REST access is
// unavailable and the fetcher will emit a static notice instead.
func AllFetchers(rest LegacyConditionsClient) []AuditFetcher {
return []AuditFetcher{
&dashboardsAuditFetcher{},
&conditionsAuditFetcher{},
&workloadsAuditFetcher{},
&serviceLevelsAuditFetcher{},
&workflowsAuditFetcher{},
&syntheticsAuditFetcher{},
&uncoveredPoliciesAuditFetcher{},
&legacyConditionsAuditFetcher{rest: rest},
}
}
// rateLimitedNG wraps a NerdGrapher, acquiring one rate-limiter token before
// each query so individual fetchers stay free of rate-limit concerns.
type rateLimitedNG struct {
ng resources.NerdGrapher
limiter *ratelimit.Limiter
}
func (r *rateLimitedNG) QueryWithResponseAndContext(
ctx context.Context,
query string,
variables map[string]interface{},
response interface{},
) error {
if err := r.limiter.Wait(ctx); err != nil {
return err
}
return r.ng.QueryWithResponseAndContext(ctx, query, variables, response)
}
// paginate drives cursor-based pagination, calling fn on each page until it
// returns an empty next-cursor.
func paginate(ctx context.Context, fn func(cursor string) (string, error)) error {
cursor := ""
for {
if ctx.Err() != nil {
return ctx.Err()
}
next, err := fn(cursor)
if err != nil {
return err
}
if next == "" {
return nil
}
cursor = next
}
}
// entitySearchCursor returns nil for the first page (required by NerdGraph
// entitySearch) or the cursor string for subsequent pages.
func entitySearchCursor(cursor string) interface{} {
if cursor == "" {
return nil
}
return cursor
}