-
Notifications
You must be signed in to change notification settings - Fork 111
Expand file tree
/
Copy pathcr_config_audit.go
More file actions
415 lines (384 loc) · 17.2 KB
/
Copy pathcr_config_audit.go
File metadata and controls
415 lines (384 loc) · 17.2 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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
package graph
import (
"sort"
"strings"
)
// CRTransportSplit partitions the TRs of a single Change Request by their
// E070.TRFUNCTION code. Workbench holds K/S/T, Customizing holds W/Q, and
// anything else lands in OtherTRs so the audit can surface unusual codes
// instead of silently dropping them.
type CRTransportSplit struct {
CRID string `json:"cr_id"`
WorkbenchTRs []string `json:"workbench_trs"`
CustomizingTRs []string `json:"customizing_trs"`
OtherTRs []CRTransportOther `json:"other_trs,omitempty"`
}
// CRTransportOther captures TRs with TRFUNCTION codes we do not classify
// (e.g. C/R/E) so the user can see them in the report and decide whether
// to treat them as workbench or customizing.
type CRTransportOther struct {
TR string `json:"tr"`
Function string `json:"function"`
}
// TableCodeRef records one code→table reference discovered via CROSS /
// WBCROSSGT or the parser. Multiple refs can exist per table.
type TableCodeRef struct {
Table string `json:"table"`
FromObject string `json:"from_object"` // e.g. "CLAS:ZCL_FOO"
FromInclude string `json:"from_include"` // canonical include name from cross-ref
RefKind string `json:"ref_kind"` // "DA" / "TABL" / "CDS_FROM" / ...
Source string `json:"source"` // "CROSS" | "WBCROSSGT" | "PARSER"
}
// TableCustRow records one customizing data row transported in a TR that
// belongs to the CR. Values come from E071K.
type TableCustRow struct {
Table string `json:"table"`
TRKORR string `json:"trkorr"`
TabKey string `json:"tabkey"`
ObjFunc string `json:"objfunc"` // I = insert, U = update, D = delete
}
// TransitiveReachHop records one "reachable via an intermediate call"
// chain from an in-scope object to an orphan table. FromScope is the
// starting object that lives inside the CR's code set; Via is the
// intermediate object (typically a FUGR or CLAS whose source we never
// see directly) that actually reads the orphan table. Depth is always
// 0 or 1 for v2a.3 — 0 means the scope object reads the table directly
// but we missed it in the forward scan (e.g. the FUGR name matches
// the table name exactly); 1 means one intermediate call is in the
// chain. Full N-hop propagation is a later refinement.
//
// CallerInclude and ReaderInclude point at the SAP include names that
// closed each half of the chain: CallerInclude calls the intermediate
// (depth 1) or reads the table directly (depth 0); ReaderInclude is
// the include inside the intermediate that does the actual SELECT on
// the orphan table. These are the hooks the user needs to open ADT
// and see the exact statements that form the chain.
type TransitiveReachHop struct {
FromScope string `json:"from_scope"` // in-scope parent id, "TYPE:NAME"
Via string `json:"via"` // intermediate parent id, "TYPE:NAME"
Depth int `json:"depth"` // 0 direct-missed | 1 one hop
CallerInclude string `json:"caller_include,omitempty"` // SAP include where FromScope calls the intermediate
ReaderInclude string `json:"reader_include,omitempty"` // SAP include where the intermediate reads the table
}
// CoverageEntry is one row of the final audit report. Depending on which
// bucket it lives in (Covered / Missing / Orphan) either side may be empty.
type CoverageEntry struct {
Table string `json:"table"`
DeliveryClass string `json:"delivery_class,omitempty"`
CodeRefs []TableCodeRef `json:"code_refs,omitempty"`
CustRows []TableCustRow `json:"cust_rows,omitempty"`
// TransitiveReach: when non-empty, this orphan is actually reached
// from an in-scope object through a function-call chain. The entry
// stays in the Orphan bucket (so the user sees the warning) but the
// hops give context: the orphan data is not dead, it feeds a code
// path inside this CR indirectly.
TransitiveReach []TransitiveReachHop `json:"transitive_reach,omitempty"`
}
// ValueLevelFinding is one code-side literal lookup cross-checked against
// the set of transported row keys for its target table. It is the output
// unit of the v2a-min value-level audit: a single source statement that
// supplied literal key values, plus a verdict on whether those exact
// values are present in the CR's data side.
//
// IncompleteKey is sticky: when the code supplied fewer key fields than
// the registered business key, the match runs in subset mode and this
// flag lets the report explain that one code call may appear COVERED
// because a broader transport tuple subsumes it, not because an exact
// match exists.
type ValueLevelFinding struct {
Table string `json:"table"`
SourceObject string `json:"source_object"`
Via string `json:"via"` // "CALL_FUNCTION:APPL_LOG_INIT"
Kind string `json:"kind"` // "known_call" (v2a-min); "direct_select" later
Row int `json:"row"`
ExpectedKeys map[string]string `json:"expected_keys"`
IncompleteKey bool `json:"incomplete_key,omitempty"`
Status string `json:"status"` // "COVERED" | "MISSING"
MatchedKeyDisplay string `json:"matched_key,omitempty"`
Note string `json:"note,omitempty"`
}
// MetadataRef is a DDIC metadata object (data element, domain, check table,
// search help) reached by walking the DD03L → DD04L → DD01L → DD07L chain
// out of a table in scope. Every entry records the path that got us here,
// so the report can explain "why is this DTEL in the graph?".
type MetadataRef struct {
Kind string `json:"kind"` // "DTEL" | "DOMA" | "CHKTAB" | "SHLP" | "FIXVAL"
Name string `json:"name"` // uppercased object name
FromTable string `json:"from_table"` // the table the chain started from (first hop)
Path []string `json:"path"` // step-by-step trace, e.g. ["TABL:ZFOO","FIELD:KEY","DTEL:ZDTEL","DOMA:ZDOM"]
}
// CRConfigAuditReport is the final, rendered audit output.
type CRConfigAuditReport struct {
CRID string `json:"cr_id"`
Transports CRTransportSplit `json:"transports"`
CodeTables map[string][]TableCodeRef `json:"code_tables,omitempty"`
CustTables map[string][]TableCustRow `json:"cust_tables,omitempty"`
// Covered: table is both read by code AND carried in a CR transport.
Covered []CoverageEntry `json:"covered"`
// Missing: custom (Z/Y) table read by code but not in any CR transport —
// the primary alarm bucket.
Missing []CoverageEntry `json:"missing"`
// StandardReads: SAP-standard table read by code; listed for transparency
// but never flagged as a gap, since SAP standard doesn't travel in CRs.
StandardReads []CoverageEntry `json:"standard_reads,omitempty"`
// ApplicationReads: custom table read by code whose DDIC delivery class
// says its rows are runtime / per-system state (CONTFLAG A / L / S),
// not customising data expected to ride in the CR. Listed so the user
// can see the dependency without the audit flagging it as MISSING.
ApplicationReads []CoverageEntry `json:"application_reads,omitempty"`
// Orphan: table has rows in a CR transport but no code in the CR reads it.
Orphan []CoverageEntry `json:"orphan"`
// DeliveryClasses: table → DD02L CONTFLAG (A/C/E/G/L/S/W). Used by
// FinalizeCRConfigAuditReport to distinguish customising tables
// whose rows are expected to travel with the CR (C/E/G/W) from
// application/runtime tables whose rows are per-system state
// (A/L/S). Missing delivery class defaults to blank — treated as
// unknown, still participates in the custom-namespace check.
DeliveryClasses map[string]string `json:"delivery_classes,omitempty"`
// DDIC metadata chain findings (v1.2a+). Collected by walking every table
// in code scope through DD03L/DD04L/DD01L/DD07L. Missing = reachable from
// scope-side code but not in CR; Orphan = transported but not reachable;
// Covered = both.
MetadataReachable map[string]MetadataRef `json:"metadata_reachable,omitempty"` // Kind:Name → ref
MetadataInCR map[string]MetadataRef `json:"metadata_in_cr,omitempty"` // Kind:Name → ref
MetadataCovered []MetadataRef `json:"metadata_covered,omitempty"`
MetadataMissing []MetadataRef `json:"metadata_missing,omitempty"`
MetadataOrphan []MetadataRef `json:"metadata_orphan,omitempty"`
// Value-level findings (v2a-min). Only populated when the caller ran
// extractCodeLiterals and matchValueLevelFindings, which hinges on
// source-fetch per in-scope object. Missing entries are the primary
// alarm — they mean a CALL FUNCTION to a customizing FM expects a
// literal key whose exact value is not in any transported row.
ValueFindings []ValueLevelFinding `json:"value_findings,omitempty"`
ValueMissing []ValueLevelFinding `json:"value_missing,omitempty"`
ValueCovered []ValueLevelFinding `json:"value_covered,omitempty"`
Summary CRConfigAuditSummary `json:"summary"`
}
// CRConfigAuditSummary provides top-line numbers for the report.
type CRConfigAuditSummary struct {
WorkbenchTRs int `json:"workbench_trs"`
CustomizingTRs int `json:"customizing_trs"`
TablesReadByCode int `json:"tables_read_by_code"`
TablesCustomRead int `json:"tables_custom_read"`
TablesStandardRead int `json:"tables_standard_read"`
TablesApplicationRead int `json:"tables_application_read"`
TablesInCustTRs int `json:"tables_in_cust_trs"`
Covered int `json:"covered"`
Missing int `json:"missing"`
Orphan int `json:"orphan"`
// DDIC metadata chain (v1.2a). Counts mirror the table buckets but live
// on the metadata plane: data elements, domains, check tables, search
// helps, domain fixed-value sets.
MetadataReachable int `json:"metadata_reachable"`
MetadataInCR int `json:"metadata_in_cr"`
MetadataCovered int `json:"metadata_covered"`
MetadataMissing int `json:"metadata_missing"`
MetadataOrphan int `json:"metadata_orphan"`
// Value-level (v2a-min).
ValueFindings int `json:"value_findings"`
ValueMissing int `json:"value_missing"`
ValueCovered int `json:"value_covered"`
Aligned bool `json:"aligned"` // all Missing==0 AND MetadataMissing==0 AND ValueMissing==0
}
// FinalizeCRConfigAuditReport cross-matches CodeTables against CustTables and
// populates Covered / Missing / StandardReads / Orphan buckets plus the Summary.
// Callers must have already filled CodeTables and CustTables; this is a pure
// function with no SAP dependencies so it stays cheap to unit test.
//
// A custom-namespace (Z/Y) table that code reads but no transport carries
// lands in Missing. A SAP-standard table that code reads lands in
// StandardReads (informational, never a gap). A table transported without
// code reading it lands in Orphan. A table in both ends up in Covered.
func FinalizeCRConfigAuditReport(r *CRConfigAuditReport) {
codeTables := sortedKeys(r.CodeTables)
custTables := sortedKeys(r.CustTables)
custSet := make(map[string]bool, len(custTables))
for _, t := range custTables {
custSet[t] = true
}
codeSet := make(map[string]bool, len(codeTables))
for _, t := range codeTables {
codeSet[t] = true
}
customRead := 0
standardRead := 0
applicationRead := 0
// The DeliveryClasses map is packed "CONTFLAG|TABCLASS" — parse
// it once per table. A view or pool cannot own row data itself
// (their rows live under base tables or the storage pool), so we
// must not flag them as MISSING even if their CONTFLAG is C.
parseFlag := func(packed string) (contflag, tabclass string) {
if i := strings.IndexByte(packed, '|'); i >= 0 {
return packed[:i], packed[i+1:]
}
return packed, ""
}
// carriesOwnRowData says "this table class actually stores row
// data that can end up in E071K". Only transparent tables and
// cluster tables do; views, pools and projection views do not.
carriesOwnRowData := func(tabclass string) bool {
switch tabclass {
case "TRANSP", "CLUSTER", "":
return true
default:
return false
}
}
// isTransportableClass reports whether a DDIC delivery class
// (DD02L.CONTFLAG) means "row data is expected to ship with a
// customising transport". C/E/G/W are the transportable classes;
// A/L/S are runtime / system / temporary state that should NOT
// be flagged as MISSING when the CR carries no rows for them.
// An empty class (unknown) defaults to transportable so we stay
// on the safe side — an unknown table still gets the standard
// check and can be flagged.
isTransportableClass := func(cls string) bool {
switch cls {
case "A", "L", "S":
return false
default:
return true
}
}
for _, t := range codeTables {
contflag, tabclass := parseFlag(r.DeliveryClasses[t])
entry := CoverageEntry{
Table: t,
DeliveryClass: contflag,
CodeRefs: r.CodeTables[t],
}
if custSet[t] {
entry.CustRows = r.CustTables[t]
r.Covered = append(r.Covered, entry)
if IsStandardObject(t) {
standardRead++
} else {
customRead++
}
continue
}
if IsStandardObject(t) {
r.StandardReads = append(r.StandardReads, entry)
standardRead++
continue
}
if !carriesOwnRowData(tabclass) {
// Views, pools, projection views — no E071K rows of
// their own. Bucket them with application reads so
// the dependency is visible without a false alarm.
r.ApplicationReads = append(r.ApplicationReads, entry)
applicationRead++
continue
}
if !isTransportableClass(contflag) {
// Custom table, but its DDIC delivery class says the
// data is runtime / per-system state — not a
// customising artefact. Surface it as an Application
// read so the user sees the dependency exists, but
// do not raise it as a MISSING alarm.
r.ApplicationReads = append(r.ApplicationReads, entry)
applicationRead++
continue
}
r.Missing = append(r.Missing, entry)
customRead++
}
// parseFlag is already declared above; orphan path reuses the
// same packed "CONTFLAG|TABCLASS" format that r.DeliveryClasses
// carries per table.
for _, t := range custTables {
if codeSet[t] {
continue // already handled under Covered
}
contflag, _ := parseFlag(r.DeliveryClasses[t])
entry := CoverageEntry{
Table: t,
DeliveryClass: contflag,
CustRows: r.CustTables[t],
}
r.Orphan = append(r.Orphan, entry)
}
// Metadata cross-match: iterate reachable (from code-side tables) vs
// in-CR (from E071 R3TR rows of DTEL/DOMA/SHLP/etc.). Only run if caller
// populated the two input maps; leave buckets empty otherwise so v1.2a
// can be disabled by passing nothing.
reachableKeys := sortedKeys(r.MetadataReachable)
inCRKeys := sortedKeys(r.MetadataInCR)
reachableSet := make(map[string]bool, len(reachableKeys))
for _, k := range reachableKeys {
reachableSet[k] = true
}
inCRSet := make(map[string]bool, len(inCRKeys))
for _, k := range inCRKeys {
inCRSet[k] = true
}
// v1.2b: a DOMA always transports its fixed-value set as one
// indivisible R3TR object, so if the CR carries "DOMA:<X>" then
// "FIXVAL:<X>" is implicitly covered too. Encode that structural
// rule here rather than inflating the CR-side map with phantom
// FIXVAL entries (which would dirty MetadataOrphan for every DOMA
// without fixed values). Only the reachable side decides whether a
// FIXVAL node is interesting at all.
coveredByDoma := func(k string) bool {
if !strings.HasPrefix(k, "FIXVAL:") {
return false
}
return inCRSet["DOMA:"+strings.TrimPrefix(k, "FIXVAL:")]
}
for _, k := range reachableKeys {
ref := r.MetadataReachable[k]
if inCRSet[k] || coveredByDoma(k) {
r.MetadataCovered = append(r.MetadataCovered, ref)
} else if !IsStandardObject(ref.Name) {
r.MetadataMissing = append(r.MetadataMissing, ref)
}
}
for _, k := range inCRKeys {
if !reachableSet[k] {
r.MetadataOrphan = append(r.MetadataOrphan, r.MetadataInCR[k])
}
}
// Value-level bucketing (v2a-min). Caller populates r.ValueFindings
// before calling us; we split into Covered/Missing based on the
// per-finding Status tag the matcher set.
for _, f := range r.ValueFindings {
switch f.Status {
case "COVERED":
r.ValueCovered = append(r.ValueCovered, f)
case "MISSING":
r.ValueMissing = append(r.ValueMissing, f)
}
}
r.Summary = CRConfigAuditSummary{
WorkbenchTRs: len(r.Transports.WorkbenchTRs),
CustomizingTRs: len(r.Transports.CustomizingTRs),
TablesReadByCode: len(codeTables),
TablesCustomRead: customRead,
TablesStandardRead: standardRead,
TablesApplicationRead: applicationRead,
TablesInCustTRs: len(custTables),
Covered: len(r.Covered),
Missing: len(r.Missing),
Orphan: len(r.Orphan),
MetadataReachable: len(reachableKeys),
MetadataInCR: len(inCRKeys),
MetadataCovered: len(r.MetadataCovered),
MetadataMissing: len(r.MetadataMissing),
MetadataOrphan: len(r.MetadataOrphan),
ValueFindings: len(r.ValueFindings),
ValueCovered: len(r.ValueCovered),
ValueMissing: len(r.ValueMissing),
Aligned: len(r.Missing) == 0 &&
len(r.Orphan) == 0 &&
len(r.MetadataMissing) == 0 &&
len(r.ValueMissing) == 0,
}
}
func sortedKeys[V any](m map[string]V) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}