master
go 2,599 lines 80.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package topologyv1
4
5 import (
6 "encoding/json"
7 "fmt"
8 "math"
9 "slices"
10 )
11
12 type validationContext struct {
13 dictionaries map[string]any
14 actorRows int
15 linkRows int
16 evidenceRowsByType map[string]int
17 evidenceRows int
18 }
19
20 type topologyShape struct {
21 actorColumns map[string]string
22 linkColumns map[string]string
23 actorTypes map[string]struct{}
24 linkTypes map[string]struct{}
25 portTypes map[string]struct{}
26 evidenceTypes map[string]map[string]string
27 tableTypes map[string]map[string]string
28 tableTypeOwners map[string]string
29 actorTables map[string]map[string]string
30 relationshipTables map[string]map[string]string
31 overlayTemplates map[string]overlayTemplateShape
32 scaleKeys map[string]struct{}
33 }
34
35 type overlayTemplateShape struct {
36 selectorParams []string
37 }
38
39 // ValidateDecodedResponse validates a full topology v1 Function response.
40 // Metadata-only Function info responses intentionally omit data and must not be
41 // passed here.
42 func ValidateDecodedResponse(payload any) error {
43 obj, ok := payload.(map[string]any)
44 if !ok {
45 return fmt.Errorf("response is not an object")
46 }
47 data, ok := obj["data"].(map[string]any)
48 if !ok {
49 return fmt.Errorf("response.data is not an object")
50 }
51 if data["schema_version"] != SchemaVersion {
52 return fmt.Errorf("response.data.schema_version is not %q", SchemaVersion)
53 }
54
55 return ValidateDecodedData(data)
56 }
57
58 func ValidateDecodedData(data map[string]any) error {
59 dictionaries, ok := data["dictionaries"].(map[string]any)
60 if !ok {
61 return fmt.Errorf("data.dictionaries is not an object")
62 }
63
64 actorRows, err := decodedTableRows(data["actors"])
65 if err != nil {
66 return fmt.Errorf("data.actors: %w", err)
67 }
68 linkRows, err := decodedTableRows(data["links"])
69 if err != nil {
70 return fmt.Errorf("data.links: %w", err)
71 }
72
73 evidenceRowsByType, err := collectEvidenceRows(data["evidence"])
74 if err != nil {
75 return err
76 }
77 tableEvidenceSources := collectTableEvidenceSources(data["types"])
78
79 ctx := validationContext{
80 dictionaries: dictionaries,
81 actorRows: actorRows,
82 linkRows: linkRows,
83 evidenceRowsByType: evidenceRowsByType,
84 evidenceRows: -1,
85 }
86
87 if _, err := validateCompactTable("data.actors", data["actors"], ctx); err != nil {
88 return err
89 }
90 if _, err := validateCompactTable("data.links", data["links"], ctx); err != nil {
91 return err
92 }
93 if err := validateEvidenceSections(data["evidence"], ctx); err != nil {
94 return err
95 }
96 if err := validateDetailTables(data["tables"], ctx, tableEvidenceSources); err != nil {
97 return err
98 }
99 if err := validateOverlayRefs(data["overlays"], ctx); err != nil {
100 return err
101 }
102 shape, err := collectTopologyShape(data)
103 if err != nil {
104 return err
105 }
106 if err := validateOverlaySemantics(data, shape, ctx); err != nil {
107 return err
108 }
109 if err := validateTypeColumns(data, shape); err != nil {
110 return err
111 }
112 if err := validatePresentation(data, shape); err != nil {
113 return err
114 }
115 if err := validateCorrelation(data["correlation"], shape, ctx); err != nil {
116 return err
117 }
118
119 return nil
120 }
121
122 func IsDecodedData(raw any) bool {
123 data, ok := raw.(map[string]any)
124 return ok && data["schema_version"] == SchemaVersion
125 }
126
127 func LinkRowsFromDecodedData(raw any) (int, error) {
128 data, ok := raw.(map[string]any)
129 if !ok {
130 return 0, fmt.Errorf("data is not an object")
131 }
132 rows, err := decodedTableRows(data["links"])
133 if err != nil {
134 return 0, fmt.Errorf("data.links: %w", err)
135 }
136 return rows, nil
137 }
138
139 func GraphRowsFromDecodedData(raw any) (int, error) {
140 data, ok := raw.(map[string]any)
141 if !ok {
142 return 0, fmt.Errorf("data is not an object")
143 }
144 actorRows, err := decodedTableRows(data["actors"])
145 if err != nil {
146 return 0, fmt.Errorf("data.actors: %w", err)
147 }
148 linkRows, err := decodedTableRows(data["links"])
149 if err != nil {
150 return 0, fmt.Errorf("data.links: %w", err)
151 }
152 return max(actorRows, linkRows), nil
153 }
154
155 func collectEvidenceRows(raw any) (map[string]int, error) {
156 rowsByType := make(map[string]int)
157 if raw == nil {
158 return rowsByType, nil
159 }
160 sections, ok := raw.(map[string]any)
161 if !ok {
162 return nil, fmt.Errorf("data.evidence is not an object")
163 }
164 for name, rawSection := range sections {
165 section, ok := rawSection.(map[string]any)
166 if !ok {
167 return nil, fmt.Errorf("data.evidence.%s is not an object", name)
168 }
169 typ, ok := section["type"].(string)
170 if !ok || typ == "" {
171 return nil, fmt.Errorf("data.evidence.%s.type is empty", name)
172 }
173 rows, err := decodedTableRows(section["table"])
174 if err != nil {
175 return nil, fmt.Errorf("data.evidence.%s.table: %w", name, err)
176 }
177 rowsByType[name] = rows
178 rowsByType[typ] = rows
179 }
180 return rowsByType, nil
181 }
182
183 func collectTableEvidenceSources(rawTypes any) map[string]string {
184 sources := make(map[string]string)
185 types, ok := rawTypes.(map[string]any)
186 if !ok {
187 return sources
188 }
189 tableTypes, ok := types["table_types"].(map[string]any)
190 if !ok {
191 return sources
192 }
193 for name, rawTableType := range tableTypes {
194 tableType, ok := rawTableType.(map[string]any)
195 if !ok {
196 continue
197 }
198 source, ok := tableType["source_evidence"].(string)
199 if ok && source != "" {
200 sources[name] = source
201 }
202 }
203 return sources
204 }
205
206 func validateEvidenceSections(raw any, ctx validationContext) error {
207 if raw == nil {
208 return nil
209 }
210 sections, ok := raw.(map[string]any)
211 if !ok {
212 return fmt.Errorf("data.evidence is not an object")
213 }
214 for name, rawSection := range sections {
215 section, ok := rawSection.(map[string]any)
216 if !ok {
217 return fmt.Errorf("data.evidence.%s is not an object", name)
218 }
219 if _, err := validateCompactTable("data.evidence."+name+".table", section["table"], ctx); err != nil {
220 return err
221 }
222 }
223 return nil
224 }
225
226 func validateDetailTables(raw any, ctx validationContext, tableEvidenceSources map[string]string) error {
227 if raw == nil {
228 return nil
229 }
230 tables, ok := raw.(map[string]any)
231 if !ok {
232 return fmt.Errorf("data.tables is not an object")
233 }
234 for _, groupName := range []string{"actor", "relationship"} {
235 groupRaw, ok := tables[groupName]
236 if !ok {
237 continue
238 }
239 group, ok := groupRaw.(map[string]any)
240 if !ok {
241 return fmt.Errorf("data.tables.%s is not an object", groupName)
242 }
243 for name, rawTable := range group {
244 detail, ok := rawTable.(map[string]any)
245 if !ok {
246 return fmt.Errorf("data.tables.%s.%s is not an object", groupName, name)
247 }
248 tableCtx := ctx
249 tableType, _ := detail["type"].(string)
250 if sourceEvidence := tableEvidenceSources[tableType]; sourceEvidence != "" {
251 rows, ok := ctx.evidenceRowsByType[sourceEvidence]
252 if !ok {
253 return fmt.Errorf("data.tables.%s.%s references unknown source_evidence %q", groupName, name, sourceEvidence)
254 }
255 tableCtx.evidenceRows = rows
256 }
257 if _, err := validateCompactTable("data.tables."+groupName+"."+name+".table", detail["table"], tableCtx); err != nil {
258 return err
259 }
260 }
261 }
262 return nil
263 }
264
265 func validateOverlayRefs(raw any, ctx validationContext) error {
266 if raw == nil {
267 return nil
268 }
269 overlays, ok := raw.(map[string]any)
270 if !ok {
271 return fmt.Errorf("data.overlays is not an object")
272 }
273 refs, ok := overlays["refs"]
274 if !ok {
275 return nil
276 }
277 if _, err := validateCompactTable("data.overlays.refs", refs, ctx); err != nil {
278 return err
279 }
280 return nil
281 }
282
283 func validateOverlaySemantics(data map[string]any, shape topologyShape, ctx validationContext) error {
284 types, _ := data["types"].(map[string]any)
285 if err := validateLinkTypeOverlayTemplates(types["link_types"], shape.overlayTemplates); err != nil {
286 return err
287 }
288 return validateOverlayRefRows(data["overlays"], shape.overlayTemplates, ctx.dictionaries)
289 }
290
291 func validateLinkTypeOverlayTemplates(raw any, templates map[string]overlayTemplateShape) error {
292 linkTypes, ok := raw.(map[string]any)
293 if !ok {
294 return fmt.Errorf("data.types.link_types is not an object")
295 }
296 known := make(map[string]struct{}, len(templates))
297 for id := range templates {
298 known[id] = struct{}{}
299 }
300 for typeID, rawType := range linkTypes {
301 linkType, ok := rawType.(map[string]any)
302 if !ok {
303 return fmt.Errorf("data.types.link_types.%s is not an object", typeID)
304 }
305 if err := validateOptionalIDArrayRefs("data.types.link_types."+typeID+".overlay_templates", linkType["overlay_templates"], known, "overlay template"); err != nil {
306 return err
307 }
308 }
309 return nil
310 }
311
312 func validateOverlayRefRows(raw any, templates map[string]overlayTemplateShape, dictionaries map[string]any) error {
313 if raw == nil {
314 return nil
315 }
316 overlays, ok := raw.(map[string]any)
317 if !ok {
318 return fmt.Errorf("data.overlays is not an object")
319 }
320 refs, ok := overlays["refs"]
321 if !ok {
322 return nil
323 }
324
325 rows, columns, err := decodedColumnsFromCompactTable("data.overlays.refs", refs)
326 if err != nil {
327 return err
328 }
329 if rows == 0 {
330 return nil
331 }
332
333 templateColumn, ok := columns[OverlayRefsTemplateColumn]
334 if !ok {
335 return fmt.Errorf("data.overlays.refs is missing required %s column", OverlayRefsTemplateColumn)
336 }
337 if !isOverlayRefsStringColumn(templateColumn) {
338 return fmt.Errorf("data.overlays.refs.%s column must be string or string_ref", OverlayRefsTemplateColumn)
339 }
340 ownerColumn, err := validateOverlayOwnerColumns(columns)
341 if err != nil {
342 return err
343 }
344
345 for row := range rows {
346 if ownerColumn.values[row] == nil {
347 return fmt.Errorf("data.overlays.refs.%s[%d] is not a non-null owner reference", ownerColumn.id, row)
348 }
349 templateID, ok := resolveStringValue(templateColumn.values[row], templateColumn.columnType, templateColumn.dictionary, dictionaries)
350 if !ok || templateID == "" {
351 return fmt.Errorf("data.overlays.refs.template[%d] is not a non-empty string", row)
352 }
353 template, ok := templates[templateID]
354 if !ok {
355 return fmt.Errorf("data.overlays.refs.template[%d] references unknown overlay template %q", row, templateID)
356 }
357 for _, param := range template.selectorParams {
358 column, ok := columns[param]
359 if !ok {
360 return fmt.Errorf("data.overlays.refs row %d template %q is missing selector param column %q", row, templateID, param)
361 }
362 if !isOverlayRefsStringColumn(column) {
363 return fmt.Errorf("data.overlays.refs.%s column must be string or string_ref", param)
364 }
365 value, ok := resolveStringValue(column.values[row], column.columnType, column.dictionary, dictionaries)
366 if !ok || value == "" {
367 return fmt.Errorf("data.overlays.refs.%s[%d] is not a non-empty string", param, row)
368 }
369 }
370 }
371
372 return nil
373 }
374
375 func validateOverlayOwnerColumns(columns map[string]decodedCompactColumn) (decodedCompactColumn, error) {
376 owners := 0
377 ownerColumn := decodedCompactColumn{}
378 if column, ok := columns[OverlayRefsActorColumn]; ok {
379 if column.columnType != "actor_ref" {
380 return decodedCompactColumn{}, fmt.Errorf("data.overlays.refs.%s column must be actor_ref", OverlayRefsActorColumn)
381 }
382 ownerColumn = column
383 owners++
384 }
385 if column, ok := columns[OverlayRefsLinkColumn]; ok {
386 if column.columnType != "link_ref" {
387 return decodedCompactColumn{}, fmt.Errorf("data.overlays.refs.%s column must be link_ref", OverlayRefsLinkColumn)
388 }
389 ownerColumn = column
390 owners++
391 }
392 for id, column := range columns {
393 if id == OverlayRefsActorColumn || id == OverlayRefsLinkColumn {
394 continue
395 }
396 if column.columnType == "actor_ref" || column.columnType == "link_ref" {
397 return decodedCompactColumn{}, fmt.Errorf("data.overlays.refs.%s uses non-convention %s owner column", id, column.columnType)
398 }
399 }
400 if owners != 1 {
401 return decodedCompactColumn{}, fmt.Errorf("data.overlays.refs must contain exactly one owner column: actor actor_ref or link link_ref")
402 }
403 return ownerColumn, nil
404 }
405
406 func isOverlayRefsStringColumn(column decodedCompactColumn) bool {
407 return column.columnType == "string" || column.columnType == "string_ref"
408 }
409
410 type decodedCompactColumn struct {
411 id string
412 columnType string
413 dictionary string
414 values []any
415 }
416
417 func decodedColumnsFromCompactTable(path string, raw any) (int, map[string]decodedCompactColumn, error) {
418 table, ok := raw.(map[string]any)
419 if !ok {
420 return 0, nil, fmt.Errorf("%s is not an object", path)
421 }
422 rows, err := decodedTableRows(table)
423 if err != nil {
424 return 0, nil, fmt.Errorf("%s: %w", path, err)
425 }
426 rawColumns, ok := table["columns"].([]any)
427 if !ok {
428 return 0, nil, fmt.Errorf("%s.columns is not an array", path)
429 }
430 rawValues, ok := table["values"].([]any)
431 if !ok {
432 return 0, nil, fmt.Errorf("%s.values is not an array", path)
433 }
434 if len(rawColumns) != len(rawValues) {
435 return 0, nil, fmt.Errorf("%s columns/values length mismatch: %d columns, %d values", path, len(rawColumns), len(rawValues))
436 }
437
438 columns := make(map[string]decodedCompactColumn, len(rawColumns))
439 for i, rawColumn := range rawColumns {
440 column, ok := rawColumn.(map[string]any)
441 if !ok {
442 return 0, nil, fmt.Errorf("%s.columns[%d] is not an object", path, i)
443 }
444 columnID, _ := column["id"].(string)
445 if columnID == "" {
446 return 0, nil, fmt.Errorf("%s.columns[%d].id is required", path, i)
447 }
448 if _, ok := columns[columnID]; ok {
449 return 0, nil, fmt.Errorf("%s.columns[%d].id duplicates column %q", path, i, columnID)
450 }
451 columnType, _ := column["type"].(string)
452 if columnType == "" {
453 return 0, nil, fmt.Errorf("%s.columns[%d].type is required", path, i)
454 }
455 values, err := decodeColumn(path, i, rows, rawValues[i])
456 if err != nil {
457 return 0, nil, err
458 }
459 dictionary, _ := column["dictionary"].(string)
460 columns[columnID] = decodedCompactColumn{
461 id: columnID,
462 columnType: columnType,
463 dictionary: dictionary,
464 values: values,
465 }
466 }
467 return rows, columns, nil
468 }
469
470 func validateCompactTable(path string, raw any, ctx validationContext) (int, error) {
471 table, ok := raw.(map[string]any)
472 if !ok {
473 return 0, fmt.Errorf("%s is not an object", path)
474 }
475 rows, err := decodedTableRows(table)
476 if err != nil {
477 return 0, fmt.Errorf("%s: %w", path, err)
478 }
479 columns, ok := table["columns"].([]any)
480 if !ok {
481 return 0, fmt.Errorf("%s.columns is not an array", path)
482 }
483 values, ok := table["values"].([]any)
484 if !ok {
485 return 0, fmt.Errorf("%s.values is not an array", path)
486 }
487 if len(columns) != len(values) {
488 return 0, fmt.Errorf("%s columns/values length mismatch: %d columns, %d values", path, len(columns), len(values))
489 }
490
491 seenColumns := make(map[string]struct{}, len(columns))
492 for i := range columns {
493 column, ok := columns[i].(map[string]any)
494 if !ok {
495 return 0, fmt.Errorf("%s.columns[%d] is not an object", path, i)
496 }
497 columnID, _ := column["id"].(string)
498 if columnID == "" {
499 return 0, fmt.Errorf("%s.columns[%d].id is required", path, i)
500 }
501 if _, ok := seenColumns[columnID]; ok {
502 return 0, fmt.Errorf("%s.columns[%d].id duplicates column %q", path, i, columnID)
503 }
504 seenColumns[columnID] = struct{}{}
505 columnType, _ := column["type"].(string)
506 if columnType == "" {
507 return 0, fmt.Errorf("%s.columns[%d].type is required", path, i)
508 }
509 decoded, err := decodeColumn(path, i, rows, values[i])
510 if err != nil {
511 return 0, err
512 }
513 if err := validateColumnValues(fmt.Sprintf("%s.%s", path, columnID), column, columnType, decoded, ctx); err != nil {
514 return 0, err
515 }
516 }
517
518 return rows, nil
519 }
520
521 func decodeColumn(path string, columnIndex int, rows int, raw any) ([]any, error) {
522 encoding, ok := raw.(map[string]any)
523 if !ok {
524 return nil, fmt.Errorf("%s.values[%d] is not an object", path, columnIndex)
525 }
526 codec, _ := encoding["codec"].(string)
527 switch codec {
528 case "const":
529 value, ok := encoding["value"]
530 if !ok {
531 return nil, fmt.Errorf("%s.values[%d] const encoding missing value", path, columnIndex)
532 }
533 values := make([]any, rows)
534 for i := range values {
535 values[i] = value
536 }
537 return values, nil
538 case "values":
539 values, ok := encoding["values"].([]any)
540 if !ok {
541 return nil, fmt.Errorf("%s.values[%d] values encoding missing values array", path, columnIndex)
542 }
543 if len(values) != rows {
544 return nil, fmt.Errorf("%s.values[%d] decoded length mismatch: expected %d, got %d", path, columnIndex, rows, len(values))
545 }
546 return values, nil
547 case "dict":
548 dictValues, ok := encoding["values"].([]any)
549 if !ok {
550 return nil, fmt.Errorf("%s.values[%d] dict encoding missing values array", path, columnIndex)
551 }
552 indexes, ok := encoding["indexes"].([]any)
553 if !ok {
554 return nil, fmt.Errorf("%s.values[%d] dict encoding missing indexes array", path, columnIndex)
555 }
556 if len(indexes) != rows {
557 return nil, fmt.Errorf("%s.values[%d] decoded length mismatch: expected %d, got %d", path, columnIndex, rows, len(indexes))
558 }
559 values := make([]any, rows)
560 for i, rawIndex := range indexes {
561 index, ok := integerValue(rawIndex)
562 if !ok {
563 return nil, fmt.Errorf("%s.values[%d].indexes[%d] is not an integer", path, columnIndex, i)
564 }
565 if index < 0 || index >= len(dictValues) {
566 return nil, fmt.Errorf("%s.values[%d].indexes[%d] out of bounds: %d", path, columnIndex, i, index)
567 }
568 values[i] = dictValues[index]
569 }
570 return values, nil
571 default:
572 return nil, fmt.Errorf("%s.values[%d] unsupported codec %q", path, columnIndex, codec)
573 }
574 }
575
576 func validateColumnValues(path string, column map[string]any, columnType string, values []any, ctx validationContext) error {
577 nullable, _ := column["nullable"].(bool)
578 for i, value := range values {
579 if value == nil {
580 if nullable {
581 continue
582 }
583 return fmt.Errorf("%s[%d] is null but column is not nullable", path, i)
584 }
585
586 switch columnType {
587 case "string_ref", "ip_ref", "mac_ref":
588 dictName, _ := column["dictionary"].(string)
589 if dictName == "" {
590 return fmt.Errorf("%s is %s without dictionary", path, columnType)
591 }
592 dict, ok := ctx.dictionaries[dictName].([]any)
593 if !ok {
594 return fmt.Errorf("%s references missing dictionary %q", path, dictName)
595 }
596 index, ok := integerValue(value)
597 if !ok {
598 return fmt.Errorf("%s[%d] is not an integer dictionary reference", path, i)
599 }
600 if index < 0 || index >= len(dict) {
601 return fmt.Errorf("%s[%d] dictionary index out of bounds: %d", path, i, index)
602 }
603 case "actor_ref":
604 if err := validateReference(path, i, value, ctx.actorRows, "actor"); err != nil {
605 return err
606 }
607 case "link_ref":
608 if err := validateReference(path, i, value, ctx.linkRows, "link"); err != nil {
609 return err
610 }
611 case "evidence_ref":
612 index, ok := integerValue(value)
613 if !ok || index < 0 {
614 return fmt.Errorf("%s[%d] is not a non-negative evidence reference", path, i)
615 }
616 if ctx.evidenceRows >= 0 && index >= ctx.evidenceRows {
617 return fmt.Errorf("%s[%d] evidence reference out of bounds: %d", path, i, index)
618 }
619 case "array":
620 if _, ok := value.([]any); !ok {
621 return fmt.Errorf("%s[%d] is not an array", path, i)
622 }
623 case "bool":
624 if _, ok := value.(bool); !ok {
625 return fmt.Errorf("%s[%d] is not a bool", path, i)
626 }
627 case "int":
628 if _, ok := integerValue(value); !ok {
629 return fmt.Errorf("%s[%d] is not an integer", path, i)
630 }
631 case "uint":
632 n, ok := integerValue(value)
633 if !ok || n < 0 {
634 return fmt.Errorf("%s[%d] is not a non-negative integer", path, i)
635 }
636 case "float", "duration":
637 if _, ok := numberValue(value); !ok {
638 return fmt.Errorf("%s[%d] is not a number", path, i)
639 }
640 case "string", "ip", "mac", "timestamp":
641 if _, ok := value.(string); !ok {
642 return fmt.Errorf("%s[%d] is not a string", path, i)
643 }
644 case "json":
645 // Any decoded JSON value is valid for a json column.
646 default:
647 return fmt.Errorf("%s has unsupported column type %q", path, columnType)
648 }
649 }
650 return nil
651 }
652
653 func collectTopologyShape(data map[string]any) (topologyShape, error) {
654 types, ok := data["types"].(map[string]any)
655 if !ok {
656 return topologyShape{}, fmt.Errorf("data.types is not an object")
657 }
658 actorColumns, err := columnTypesFromTable(data["actors"], "data.actors")
659 if err != nil {
660 return topologyShape{}, err
661 }
662 linkColumns, err := columnTypesFromTable(data["links"], "data.links")
663 if err != nil {
664 return topologyShape{}, err
665 }
666 actorTypes, err := objectKeySet(types["actor_types"], "data.types.actor_types")
667 if err != nil {
668 return topologyShape{}, err
669 }
670 linkTypes, err := objectKeySet(types["link_types"], "data.types.link_types")
671 if err != nil {
672 return topologyShape{}, err
673 }
674 portTypes, err := optionalObjectKeySet(types["port_types"], "data.types.port_types")
675 if err != nil {
676 return topologyShape{}, err
677 }
678
679 evidenceTypes, err := columnTypesByRegistryObject(types["evidence_types"], "data.types.evidence_types")
680 if err != nil {
681 return topologyShape{}, err
682 }
683 tableTypes, err := columnTypesByRegistryObject(types["table_types"], "data.types.table_types")
684 if err != nil {
685 return topologyShape{}, err
686 }
687 tableTypeOwners, err := tableTypeOwners(types["table_types"], "data.types.table_types")
688 if err != nil {
689 return topologyShape{}, err
690 }
691 actorTables, relationshipTables, err := collectDetailTableColumnTypes(data["tables"])
692 if err != nil {
693 return topologyShape{}, err
694 }
695 scaleKeys, err := collectScaleKeys(data["presentation"])
696 if err != nil {
697 return topologyShape{}, err
698 }
699 overlayTemplates, err := collectOverlayTemplates(types["overlay_templates"])
700 if err != nil {
701 return topologyShape{}, err
702 }
703
704 return topologyShape{
705 actorColumns: actorColumns,
706 linkColumns: linkColumns,
707 actorTypes: actorTypes,
708 linkTypes: linkTypes,
709 portTypes: portTypes,
710 evidenceTypes: evidenceTypes,
711 tableTypes: tableTypes,
712 tableTypeOwners: tableTypeOwners,
713 actorTables: actorTables,
714 relationshipTables: relationshipTables,
715 overlayTemplates: overlayTemplates,
716 scaleKeys: scaleKeys,
717 }, nil
718 }
719
720 func validateTypeColumns(data map[string]any, shape topologyShape) error {
721 if _, ok := shape.actorColumns["type"]; !ok {
722 return fmt.Errorf("data.actors is missing required type column")
723 }
724 if _, ok := shape.linkColumns["type"]; !ok {
725 return fmt.Errorf("data.links is missing required type column")
726 }
727 dictionaries, _ := data["dictionaries"].(map[string]any)
728 if err := validateTypeColumnValues("data.actors", data["actors"], shape.actorTypes, "actor", dictionaries); err != nil {
729 return err
730 }
731 if err := validateTypeColumnValues("data.links", data["links"], shape.linkTypes, "link", dictionaries); err != nil {
732 return err
733 }
734 return nil
735 }
736
737 func validateTypeColumnValues(path string, raw any, known map[string]struct{}, typeName string, dictionaries map[string]any) error {
738 table, ok := raw.(map[string]any)
739 if !ok {
740 return fmt.Errorf("%s is not an object", path)
741 }
742 rows, err := decodedTableRows(table)
743 if err != nil {
744 return fmt.Errorf("%s: %w", path, err)
745 }
746 columns, ok := table["columns"].([]any)
747 if !ok {
748 return fmt.Errorf("%s.columns is not an array", path)
749 }
750 values, ok := table["values"].([]any)
751 if !ok {
752 return fmt.Errorf("%s.values is not an array", path)
753 }
754 for i, rawColumn := range columns {
755 column, ok := rawColumn.(map[string]any)
756 if !ok {
757 return fmt.Errorf("%s.columns[%d] is not an object", path, i)
758 }
759 if id, _ := column["id"].(string); id != "type" {
760 continue
761 }
762 columnType, _ := column["type"].(string)
763 dictionary, _ := column["dictionary"].(string)
764 decoded, err := decodeColumn(path, i, rows, values[i])
765 if err != nil {
766 return err
767 }
768 for row, value := range decoded {
769 id, ok := resolveStringValue(value, columnType, dictionary, dictionaries)
770 if !ok || id == "" {
771 return fmt.Errorf("%s.type[%d] is not a non-empty string", path, row)
772 }
773 if _, ok := known[id]; !ok {
774 return fmt.Errorf("%s.type[%d] references unknown %s type %q", path, row, typeName, id)
775 }
776 }
777 return nil
778 }
779 return fmt.Errorf("%s is missing required type column", path)
780 }
781
782 func validateStringColumnValuesInSet(path string, raw any, columnID string, known map[string]struct{}, typeName string, dictionaries map[string]any) error {
783 _, err := collectStringColumnValuesInSet(path, raw, columnID, known, typeName, dictionaries)
784 return err
785 }
786
787 func collectStringColumnValuesInSet(path string, raw any, columnID string, known map[string]struct{}, typeName string, dictionaries map[string]any) (map[string]struct{}, error) {
788 table, ok := raw.(map[string]any)
789 if !ok {
790 return nil, fmt.Errorf("%s is not an object", path)
791 }
792 rows, err := decodedTableRows(table)
793 if err != nil {
794 return nil, fmt.Errorf("%s: %w", path, err)
795 }
796 columns, ok := table["columns"].([]any)
797 if !ok {
798 return nil, fmt.Errorf("%s.columns is not an array", path)
799 }
800 values, ok := table["values"].([]any)
801 if !ok {
802 return nil, fmt.Errorf("%s.values is not an array", path)
803 }
804 for i, rawColumn := range columns {
805 column, ok := rawColumn.(map[string]any)
806 if !ok {
807 return nil, fmt.Errorf("%s.columns[%d] is not an object", path, i)
808 }
809 if id, _ := column["id"].(string); id != columnID {
810 continue
811 }
812 columnType, _ := column["type"].(string)
813 dictionary, _ := column["dictionary"].(string)
814 decoded, err := decodeColumn(path, i, rows, values[i])
815 if err != nil {
816 return nil, err
817 }
818 found := make(map[string]struct{}, len(decoded))
819 for row, value := range decoded {
820 id, ok := resolveStringValue(value, columnType, dictionary, dictionaries)
821 if !ok || id == "" {
822 return nil, fmt.Errorf("%s.%s[%d] is not a non-empty string", path, columnID, row)
823 }
824 if _, ok := known[id]; !ok {
825 return nil, fmt.Errorf("%s.%s[%d] references unknown %s %q", path, columnID, row, typeName, id)
826 }
827 found[id] = struct{}{}
828 }
829 return found, nil
830 }
831 return nil, fmt.Errorf("%s is missing required %s column", path, columnID)
832 }
833
834 func resolveStringValue(value any, columnType, dictionary string, dictionaries map[string]any) (string, bool) {
835 if text, ok := value.(string); ok {
836 return text, true
837 }
838 if columnType != "string_ref" && columnType != "ip_ref" && columnType != "mac_ref" {
839 return "", false
840 }
841 index, ok := integerValue(value)
842 if !ok || dictionary == "" {
843 return "", false
844 }
845 values, ok := dictionaries[dictionary].([]any)
846 if !ok || index < 0 || index >= len(values) {
847 return "", false
848 }
849 text, ok := values[index].(string)
850 return text, ok
851 }
852
853 func validatePresentation(data map[string]any, shape topologyShape) error {
854 types, _ := data["types"].(map[string]any)
855 if err := validateActorTypePresentation(types["actor_types"], shape); err != nil {
856 return err
857 }
858 if err := validateLinkTypePresentation(types["link_types"], shape); err != nil {
859 return err
860 }
861 if err := validatePortTypePresentation(types["port_types"]); err != nil {
862 return err
863 }
864 if err := validateTableTypePresentation(types["table_types"], shape); err != nil {
865 return err
866 }
867 if err := validateGraphPresentation(data["presentation"], shape); err != nil {
868 return err
869 }
870 return nil
871 }
872
873 func validateActorTypePresentation(raw any, shape topologyShape) error {
874 actorTypes, ok := raw.(map[string]any)
875 if !ok {
876 return fmt.Errorf("data.types.actor_types is not an object")
877 }
878 for typeID, rawType := range actorTypes {
879 actorType, ok := rawType.(map[string]any)
880 if !ok {
881 return fmt.Errorf("data.types.actor_types.%s is not an object", typeID)
882 }
883 presentation, ok := actorType["presentation"].(map[string]any)
884 if !ok {
885 continue
886 }
887 path := "data.types.actor_types." + typeID + ".presentation"
888 if _, ok := presentation["label"]; ok {
889 if err := validateRequiredString(path+".label", presentation["label"]); err != nil {
890 return err
891 }
892 }
893 if err := optionalEnum(path+".role", presentation["role"], "actor", "endpoint", "group"); err != nil {
894 return err
895 }
896 if err := optionalEnum(path+".icon", presentation["icon"], iconTokens...); err != nil {
897 return err
898 }
899 if err := optionalEnum(path+".color_slot", presentation["color_slot"], colorSlotTokens...); err != nil {
900 return err
901 }
902 if err := optionalEnum(path+".opacity", presentation["opacity"], opacityTokens...); err != nil {
903 return err
904 }
905 if err := validateBorderPresentation(path+".border", presentation["border"]); err != nil {
906 return err
907 }
908 if err := validateAnnotationPresentation(path+".annotation", presentation["annotation"]); err != nil {
909 return err
910 }
911 if err := validateActorSizePresentation(path+".size", presentation["size"], shape.actorColumns); err != nil {
912 return err
913 }
914 if err := validateActorLayoutPresentation(path+".layout", presentation["layout"]); err != nil {
915 return err
916 }
917 if err := validateLabelPolicy(path+".label_policy", presentation["label_policy"], shape.actorColumns); err != nil {
918 return err
919 }
920 if err := validateActorPortsPresentation(path+".ports", presentation["ports"], shape); err != nil {
921 return err
922 }
923 if err := validateHoverPresentation(path+".hover", presentation["hover"], shape.actorColumns); err != nil {
924 return err
925 }
926 if err := validateModalPresentation(path+".modal", presentation["modal"], shape); err != nil {
927 return err
928 }
929 if err := validateActorSearchPolicy("data.types.actor_types."+typeID+".search", actorType["search"], shape.actorColumns); err != nil {
930 return err
931 }
932 }
933 return nil
934 }
935
936 func validateLinkTypePresentation(raw any, shape topologyShape) error {
937 linkTypes, ok := raw.(map[string]any)
938 if !ok {
939 return fmt.Errorf("data.types.link_types is not an object")
940 }
941 for typeID, rawType := range linkTypes {
942 linkType, ok := rawType.(map[string]any)
943 if !ok {
944 return fmt.Errorf("data.types.link_types.%s is not an object", typeID)
945 }
946 if err := optionalEnum("data.types.link_types."+typeID+".semantic_role", linkType["semantic_role"], linkSemanticRoleTokens...); err != nil {
947 return err
948 }
949 presentation, ok := linkType["presentation"].(map[string]any)
950 if !ok {
951 continue
952 }
953 path := "data.types.link_types." + typeID + ".presentation"
954 if _, ok := presentation["label"]; ok {
955 if err := validateRequiredString(path+".label", presentation["label"]); err != nil {
956 return err
957 }
958 }
959 if err := optionalEnum(path+".color_slot", presentation["color_slot"], colorSlotTokens...); err != nil {
960 return err
961 }
962 if err := optionalEnum(path+".opacity", presentation["opacity"], opacityTokens...); err != nil {
963 return err
964 }
965 if err := optionalEnum(path+".line_style", presentation["line_style"], "solid", "dashed", "dotted"); err != nil {
966 return err
967 }
968 if err := optionalEnum(path+".width", presentation["width"], widthTokens...); err != nil {
969 return err
970 }
971 if err := optionalEnum(path+".curve", presentation["curve"], "straight", "clockwise", "counter_clockwise", "auto"); err != nil {
972 return err
973 }
974 if err := optionalEnum(path+".arrow", presentation["arrow"], "none", "forward", "reverse", "both", "auto"); err != nil {
975 return err
976 }
977 if err := validateLinkVariablePresentation(path+".variable", presentation["variable"], shape); err != nil {
978 return err
979 }
980 if err := validateHoverPresentation(path+".hover", presentation["hover"], shape.linkColumns); err != nil {
981 return err
982 }
983 if err := validateLinkLayoutPresentation(path+".layout", presentation["layout"]); err != nil {
984 return err
985 }
986 if err := validateModalPresentation(path+".modal", presentation["modal"], shape); err != nil {
987 return err
988 }
989 }
990 return nil
991 }
992
993 func validateLinkLayoutPresentation(path string, raw any) error {
994 if raw == nil {
995 return nil
996 }
997 layout, ok := raw.(map[string]any)
998 if !ok {
999 return fmt.Errorf("%s is not an object", path)
1000 }
1001 if err := optionalEnum(path+".strength", layout["strength"], layoutStrengthTokens...); err != nil {
1002 return err
1003 }
1004 return optionalEnum(path+".distance", layout["distance"], layoutDistanceTokens...)
1005 }
1006
1007 func validateActorLayoutPresentation(path string, raw any) error {
1008 if raw == nil {
1009 return nil
1010 }
1011 layout, ok := raw.(map[string]any)
1012 if !ok {
1013 return fmt.Errorf("%s is not an object", path)
1014 }
1015 return optionalEnum(path+".repulsion", layout["repulsion"], layoutStrengthTokens...)
1016 }
1017
1018 func validatePortTypePresentation(raw any) error {
1019 if raw == nil {
1020 return nil
1021 }
1022 portTypes, ok := raw.(map[string]any)
1023 if !ok {
1024 return fmt.Errorf("data.types.port_types is not an object")
1025 }
1026 for typeID, rawType := range portTypes {
1027 portType, ok := rawType.(map[string]any)
1028 if !ok {
1029 return fmt.Errorf("data.types.port_types.%s is not an object", typeID)
1030 }
1031 presentation, ok := portType["presentation"].(map[string]any)
1032 if !ok {
1033 continue
1034 }
1035 path := "data.types.port_types." + typeID + ".presentation"
1036 if _, ok := presentation["label"]; ok {
1037 if err := validateRequiredString(path+".label", presentation["label"]); err != nil {
1038 return err
1039 }
1040 }
1041 if err := optionalEnum(path+".color_slot", presentation["color_slot"], colorSlotTokens...); err != nil {
1042 return err
1043 }
1044 if err := optionalEnum(path+".opacity", presentation["opacity"], opacityTokens...); err != nil {
1045 return err
1046 }
1047 }
1048 return nil
1049 }
1050
1051 func validateTableTypePresentation(raw any, shape topologyShape) error {
1052 if raw == nil {
1053 return nil
1054 }
1055 tableTypes, ok := raw.(map[string]any)
1056 if !ok {
1057 return fmt.Errorf("data.types.table_types is not an object")
1058 }
1059 for typeID, rawType := range tableTypes {
1060 tableType, ok := rawType.(map[string]any)
1061 if !ok {
1062 return fmt.Errorf("data.types.table_types.%s is not an object", typeID)
1063 }
1064 presentation, ok := tableType["presentation"].(map[string]any)
1065 if !ok {
1066 continue
1067 }
1068 path := "data.types.table_types." + typeID + ".presentation"
1069 if _, ok := presentation["label"]; ok {
1070 if err := validateRequiredString(path+".label", presentation["label"]); err != nil {
1071 return err
1072 }
1073 }
1074 if err := optionalEnum(path+".default_visibility", presentation["default_visibility"], "table", "expanded", "hidden", "debug"); err != nil {
1075 return err
1076 }
1077 columns := shape.tableTypes[typeID]
1078 if err := validateModalColumns(path+".columns", presentation["columns"], columns); err != nil {
1079 return err
1080 }
1081 }
1082 return nil
1083 }
1084
1085 func validateModalPresentation(path string, raw any, shape topologyShape) error {
1086 if raw == nil {
1087 return nil
1088 }
1089 modal, ok := raw.(map[string]any)
1090 if !ok {
1091 return fmt.Errorf("%s is not an object", path)
1092 }
1093 if err := validateModalLabelsPresentation(path+".labels", modal["labels"], shape); err != nil {
1094 return err
1095 }
1096 if err := validateModalMiniTopologyPresentation(path+".mini_topology", modal["mini_topology"], shape); err != nil {
1097 return err
1098 }
1099 rawSections, hasSections := modal["sections"]
1100 if !hasSections || rawSections == nil {
1101 return nil
1102 }
1103 sections, ok := rawSections.([]any)
1104 if !ok {
1105 return fmt.Errorf("%s.sections is not an array", path)
1106 }
1107 seenIDs := make(map[string]struct{}, len(sections))
1108 for i, rawSection := range sections {
1109 section, ok := rawSection.(map[string]any)
1110 if !ok {
1111 return fmt.Errorf("%s.sections[%d] is not an object", path, i)
1112 }
1113 id, _ := section["id"].(string)
1114 if id != "" {
1115 if _, ok := seenIDs[id]; ok {
1116 return fmt.Errorf("%s.sections[%d].id duplicates modal section id %q", path, i, id)
1117 }
1118 seenIDs[id] = struct{}{}
1119 }
1120 if err := validateModalSection(fmt.Sprintf("%s.sections[%d]", path, i), section, shape); err != nil {
1121 return err
1122 }
1123 }
1124 return nil
1125 }
1126
1127 func validateModalLabelsPresentation(path string, raw any, shape topologyShape) error {
1128 if raw == nil {
1129 return nil
1130 }
1131 labels, ok := raw.(map[string]any)
1132 if !ok {
1133 return fmt.Errorf("%s is not an object", path)
1134 }
1135 table, _ := labels["table"].(string)
1136 if table == "" {
1137 table = "actor_labels"
1138 }
1139 columns := shape.actorTables[table]
1140 if columns == nil {
1141 columns = shape.tableTypes[table]
1142 }
1143 if columns == nil {
1144 return fmt.Errorf("%s.table references unknown actor labels table %q", path, table)
1145 }
1146 for field, defaultColumn := range map[string]string{
1147 "actor_column": "actor",
1148 "key_column": "key",
1149 "value_column": "value",
1150 "source_column": "source",
1151 "kind_column": "kind",
1152 "value_index_column": "value_index",
1153 } {
1154 _, hasField := labels[field]
1155 column, _ := labels[field].(string)
1156 if column == "" {
1157 column = defaultColumn
1158 }
1159 columnType, ok := columns[column]
1160 if !ok {
1161 if (field == "source_column" || field == "kind_column" || field == "value_index_column") && !hasField {
1162 continue
1163 }
1164 return fmt.Errorf("%s.%s references unknown column %q", path, field, column)
1165 }
1166 if field == "actor_column" && columnType != "actor_ref" {
1167 return fmt.Errorf("%s.%s references non-actor_ref column %q (%s)", path, field, column, columnType)
1168 }
1169 }
1170 if err := validateModalLabelIdentification(path+".identification", labels["identification"]); err != nil {
1171 return err
1172 }
1173 return nil
1174 }
1175
1176 func validateModalLabelIdentification(path string, raw any) error {
1177 if raw == nil {
1178 return nil
1179 }
1180 identification, ok := raw.(map[string]any)
1181 if !ok {
1182 return fmt.Errorf("%s is not an object", path)
1183 }
1184 if rawFields := identification["fields"]; rawFields != nil {
1185 fields, ok := rawFields.([]any)
1186 if !ok {
1187 return fmt.Errorf("%s.fields is not an array", path)
1188 }
1189 for i, rawField := range fields {
1190 field, ok := rawField.(map[string]any)
1191 if !ok {
1192 return fmt.Errorf("%s.fields[%d] is not an object", path, i)
1193 }
1194 key, ok := field["key"].(string)
1195 if !ok || key == "" {
1196 return fmt.Errorf("%s.fields[%d].key is required", path, i)
1197 }
1198 label, ok := field["label"].(string)
1199 if !ok || label == "" {
1200 return fmt.Errorf("%s.fields[%d].label is required", path, i)
1201 }
1202 if rawMaxValues, ok := field["max_values"]; ok {
1203 maxValues, ok := integerValue(rawMaxValues)
1204 if !ok || maxValues < 1 {
1205 return fmt.Errorf("%s.fields[%d].max_values must be a positive integer", path, i)
1206 }
1207 }
1208 }
1209 }
1210 return nil
1211 }
1212
1213 func validateModalMiniTopologyPresentation(path string, raw any, shape topologyShape) error {
1214 if raw == nil {
1215 return nil
1216 }
1217 mini, ok := raw.(map[string]any)
1218 if !ok {
1219 return fmt.Errorf("%s is not an object", path)
1220 }
1221 if _, present := mini["depth"]; present {
1222 depth, ok := integerValue(mini["depth"])
1223 if !ok {
1224 return fmt.Errorf("%s.depth is not an integer", path)
1225 }
1226 if depth != 1 {
1227 return fmt.Errorf("%s.depth must be 1", path)
1228 }
1229 }
1230 if err := validateKnownLinkTypeList(path+".include_link_types", mini["include_link_types"], shape); err != nil {
1231 return err
1232 }
1233 return validateKnownLinkTypeList(path+".exclude_link_types", mini["exclude_link_types"], shape)
1234 }
1235
1236 func validateKnownLinkTypeList(path string, raw any, shape topologyShape) error {
1237 if raw == nil {
1238 return nil
1239 }
1240 values, ok := raw.([]any)
1241 if !ok {
1242 return fmt.Errorf("%s is not an array", path)
1243 }
1244 for i, rawValue := range values {
1245 value, ok := rawValue.(string)
1246 if !ok || value == "" {
1247 return fmt.Errorf("%s[%d] is not a non-empty string", path, i)
1248 }
1249 if _, ok := shape.linkTypes[value]; !ok {
1250 return fmt.Errorf("%s[%d] references unknown link type %q", path, i, value)
1251 }
1252 }
1253 return nil
1254 }
1255
1256 func validateModalSection(path string, section map[string]any, shape topologyShape) error {
1257 if err := validateRequiredString(path+".id", section["id"]); err != nil {
1258 return err
1259 }
1260 if err := validateRequiredString(path+".label", section["label"]); err != nil {
1261 return err
1262 }
1263 columns, err := validateModalSource(path+".source", section["source"], shape)
1264 if err != nil {
1265 return err
1266 }
1267 if err := validateModalOwnerFilter(path+".owner_filter", section["owner_filter"], columns); err != nil {
1268 return err
1269 }
1270 if err := validateModalRowFilters(path+".row_filters", section["row_filters"], columns); err != nil {
1271 return err
1272 }
1273 rawColumns, ok := section["columns"].([]any)
1274 if !ok {
1275 return fmt.Errorf("%s.columns is not an array", path)
1276 }
1277 if len(rawColumns) == 0 {
1278 return fmt.Errorf("%s.columns must not be empty", path)
1279 }
1280 if err := validateModalColumns(path+".columns", section["columns"], columns); err != nil {
1281 return err
1282 }
1283 return validateModalSort(path+".sort", section["sort"], section["columns"])
1284 }
1285
1286 func validateModalSource(path string, raw any, shape topologyShape) (map[string]string, error) {
1287 source, ok := raw.(map[string]any)
1288 if !ok {
1289 return nil, fmt.Errorf("%s is not an object", path)
1290 }
1291 kind, err := requiredEnum(path+".kind", source["kind"], "actors", "links", "evidence", "actor_table", "relationship_table")
1292 if err != nil {
1293 return nil, err
1294 }
1295 switch kind {
1296 case "actors":
1297 return shape.actorColumns, nil
1298 case "links":
1299 return shape.linkColumns, nil
1300 case "evidence":
1301 evidence, _ := source["evidence"].(string)
1302 if evidence == "" {
1303 return nil, fmt.Errorf("%s.evidence is required when kind is evidence", path)
1304 }
1305 columns := shape.evidenceTypes[evidence]
1306 if columns == nil {
1307 return nil, fmt.Errorf("%s.evidence references unknown evidence type %q", path, evidence)
1308 }
1309 return columns, nil
1310 case "actor_table":
1311 table, _ := source["table"].(string)
1312 if table == "" {
1313 return nil, fmt.Errorf("%s.table is required when kind is actor_table", path)
1314 }
1315 columns := shape.actorTables[table]
1316 if columns == nil {
1317 columns = shape.tableTypes[table]
1318 }
1319 if columns == nil {
1320 return nil, fmt.Errorf("%s.table references unknown actor table %q", path, table)
1321 }
1322 return columns, nil
1323 case "relationship_table":
1324 table, _ := source["table"].(string)
1325 if table == "" {
1326 return nil, fmt.Errorf("%s.table is required when kind is relationship_table", path)
1327 }
1328 columns := shape.relationshipTables[table]
1329 if columns == nil {
1330 columns = shape.tableTypes[table]
1331 }
1332 if columns == nil {
1333 return nil, fmt.Errorf("%s.table references unknown relationship table %q", path, table)
1334 }
1335 return columns, nil
1336 default:
1337 return nil, fmt.Errorf("%s.kind has unsupported value %q", path, kind)
1338 }
1339 }
1340
1341 func validateModalOwnerFilter(path string, raw any, columns map[string]string) error {
1342 if raw == nil {
1343 return nil
1344 }
1345 filter, ok := raw.(map[string]any)
1346 if !ok {
1347 return fmt.Errorf("%s is not an object", path)
1348 }
1349 mode, err := requiredEnum(path+".mode", filter["mode"], "none", "actor_column", "link_column", "incident_link", "incident_evidence", "selected_link")
1350 if err != nil {
1351 return err
1352 }
1353 switch mode {
1354 case "actor_column":
1355 return validateModalColumnRef(path+".actor_column", filter["actor_column"], columns, "actor_ref", false)
1356 case "link_column", "selected_link":
1357 return validateModalColumnRef(path+".link_column", filter["link_column"], columns, "link_ref", false)
1358 case "incident_link", "incident_evidence":
1359 if err := validateModalColumnRef(path+".src_actor_column", filter["src_actor_column"], columns, "actor_ref", false); err != nil {
1360 return err
1361 }
1362 return validateModalColumnRef(path+".dst_actor_column", filter["dst_actor_column"], columns, "actor_ref", false)
1363 default:
1364 return nil
1365 }
1366 }
1367
1368 func validateModalRowFilters(path string, raw any, columns map[string]string) error {
1369 if raw == nil {
1370 return nil
1371 }
1372 filters, ok := raw.([]any)
1373 if !ok {
1374 return fmt.Errorf("%s is not an array", path)
1375 }
1376 for i, rawFilter := range filters {
1377 filter, ok := rawFilter.(map[string]any)
1378 if !ok {
1379 return fmt.Errorf("%s[%d] is not an object", path, i)
1380 }
1381 filterPath := fmt.Sprintf("%s[%d]", path, i)
1382 if err := validateModalColumnRef(filterPath+".column", filter["column"], columns, "", false); err != nil {
1383 return err
1384 }
1385 op, err := requiredEnum(filterPath+".op", filter["op"], "eq", "ne", "in", "not_in", "exists", "missing")
1386 if err != nil {
1387 return err
1388 }
1389 switch op {
1390 case "eq", "ne":
1391 if _, ok := filter["value"]; !ok {
1392 return fmt.Errorf("%s.value is required when op is %s", filterPath, op)
1393 }
1394 case "in", "not_in":
1395 values, ok := filter["values"].([]any)
1396 if !ok {
1397 return fmt.Errorf("%s.values is required when op is %s", filterPath, op)
1398 }
1399 if len(values) == 0 {
1400 return fmt.Errorf("%s.values must not be empty when op is %s", filterPath, op)
1401 }
1402 }
1403 }
1404 return nil
1405 }
1406
1407 func validateModalColumns(path string, raw any, sourceColumns map[string]string) error {
1408 if raw == nil {
1409 return nil
1410 }
1411 columns, ok := raw.([]any)
1412 if !ok {
1413 return fmt.Errorf("%s is not an array", path)
1414 }
1415 seenIDs := make(map[string]struct{}, len(columns))
1416 for i, rawColumn := range columns {
1417 column, ok := rawColumn.(map[string]any)
1418 if !ok {
1419 return fmt.Errorf("%s[%d] is not an object", path, i)
1420 }
1421 columnPath := fmt.Sprintf("%s[%d]", path, i)
1422 if err := validateRequiredString(columnPath+".id", column["id"]); err != nil {
1423 return err
1424 }
1425 id := column["id"].(string)
1426 if _, ok := seenIDs[id]; ok {
1427 return fmt.Errorf("%s[%d].id duplicates modal column id %q", path, i, id)
1428 }
1429 seenIDs[id] = struct{}{}
1430 if err := validateRequiredString(columnPath+".label", column["label"]); err != nil {
1431 return err
1432 }
1433 if err := optionalEnum(columnPath+".cell", column["cell"], "text", "number", "badge", "actor_link", "timestamp", "duration", "endpoint", "array_count", "debug_json"); err != nil {
1434 return err
1435 }
1436 if err := optionalEnum(columnPath+".visibility", column["visibility"], "table", "expanded", "hidden", "debug"); err != nil {
1437 return err
1438 }
1439 if err := optionalEnum(columnPath+".align", column["align"], "left", "center", "right"); err != nil {
1440 return err
1441 }
1442 if err := validateModalProjection(columnPath+".projection", column["projection"], sourceColumns); err != nil {
1443 return err
1444 }
1445 if err := validateModalBadgeMap(columnPath+".badge_map", column["badge_map"]); err != nil {
1446 return err
1447 }
1448 }
1449 return nil
1450 }
1451
1452 func validateModalProjection(path string, raw any, columns map[string]string) error {
1453 projection, ok := raw.(map[string]any)
1454 if !ok {
1455 return fmt.Errorf("%s is not an object", path)
1456 }
1457 kind, err := requiredEnum(path+".kind", projection["kind"],
1458 "direct", "actor_ref_label", "opposite_actor", "formatted_endpoint", "label_lookup",
1459 "json_path", "const", "coalesce", "selected_side_endpoint")
1460 if err != nil {
1461 return err
1462 }
1463 switch kind {
1464 case "direct":
1465 return validateModalColumnRef(path+".column", projection["column"], columns, "", false)
1466 case "actor_ref_label":
1467 return validateModalColumnRef(path+".actor_column", projection["actor_column"], columns, "actor_ref", false)
1468 case "opposite_actor":
1469 if err := validateModalColumnRef(path+".src_actor_column", projection["src_actor_column"], columns, "actor_ref", false); err != nil {
1470 return err
1471 }
1472 return validateModalColumnRef(path+".dst_actor_column", projection["dst_actor_column"], columns, "actor_ref", false)
1473 case "formatted_endpoint":
1474 if stringValue(projection["ip_column"]) == "" && stringValue(projection["port_column"]) == "" {
1475 return fmt.Errorf("%s requires ip_column or port_column when kind is formatted_endpoint", path)
1476 }
1477 if err := validateModalColumnRef(path+".ip_column", projection["ip_column"], columns, "", true); err != nil {
1478 return err
1479 }
1480 if err := validateModalColumnRef(path+".port_column", projection["port_column"], columns, "", true); err != nil {
1481 return err
1482 }
1483 return validateModalColumnRef(path+".protocol_column", projection["protocol_column"], columns, "", true)
1484 case "label_lookup":
1485 if err := validateModalColumnRef(path+".actor_column", projection["actor_column"], columns, "actor_ref", true); err != nil {
1486 return err
1487 }
1488 labelKey, _ := projection["label_key"].(string)
1489 if labelKey == "" {
1490 return fmt.Errorf("%s.label_key is required when kind is label_lookup", path)
1491 }
1492 return nil
1493 case "json_path":
1494 if err := validateModalColumnRef(path+".column", projection["column"], columns, "json", false); err != nil {
1495 return err
1496 }
1497 pathValue, _ := projection["path"].(string)
1498 if pathValue == "" {
1499 return fmt.Errorf("%s.path is required when kind is json_path", path)
1500 }
1501 return nil
1502 case "const":
1503 if _, ok := projection["value"]; !ok {
1504 return fmt.Errorf("%s.value is required when kind is const", path)
1505 }
1506 return nil
1507 case "coalesce":
1508 rawColumns, ok := projection["columns"].([]any)
1509 if !ok || len(rawColumns) == 0 {
1510 return fmt.Errorf("%s.columns is required when kind is coalesce", path)
1511 }
1512 for i, rawColumn := range rawColumns {
1513 if err := validateModalColumnRef(fmt.Sprintf("%s.columns[%d]", path, i), rawColumn, columns, "", false); err != nil {
1514 return err
1515 }
1516 }
1517 return nil
1518 case "selected_side_endpoint":
1519 if err := validateModalColumnRef(path+".src_actor_column", projection["src_actor_column"], columns, "actor_ref", false); err != nil {
1520 return err
1521 }
1522 if err := validateModalColumnRef(path+".dst_actor_column", projection["dst_actor_column"], columns, "actor_ref", false); err != nil {
1523 return err
1524 }
1525 if stringValue(projection["local_ip_column"]) == "" && stringValue(projection["local_port_column"]) == "" {
1526 return fmt.Errorf("%s requires local_ip_column or local_port_column when kind is selected_side_endpoint", path)
1527 }
1528 if stringValue(projection["remote_ip_column"]) == "" && stringValue(projection["remote_port_column"]) == "" {
1529 return fmt.Errorf("%s requires remote_ip_column or remote_port_column when kind is selected_side_endpoint", path)
1530 }
1531 for _, field := range []string{"local_ip_column", "local_port_column", "remote_ip_column", "remote_port_column", "protocol_column"} {
1532 if err := validateModalColumnRef(path+"."+field, projection[field], columns, "", true); err != nil {
1533 return err
1534 }
1535 }
1536 return nil
1537 default:
1538 return fmt.Errorf("%s.kind has unsupported value %q", path, kind)
1539 }
1540 }
1541
1542 func validateModalBadgeMap(path string, raw any) error {
1543 if raw == nil {
1544 return nil
1545 }
1546 badgeMap, ok := raw.(map[string]any)
1547 if !ok {
1548 return fmt.Errorf("%s is not an object", path)
1549 }
1550 for key, rawBadge := range badgeMap {
1551 badge, ok := rawBadge.(map[string]any)
1552 if !ok {
1553 return fmt.Errorf("%s.%s is not an object", path, key)
1554 }
1555 if err := optionalEnum(path+"."+key+".color_slot", badge["color_slot"], colorSlotTokens...); err != nil {
1556 return err
1557 }
1558 if err := optionalEnum(path+"."+key+".opacity", badge["opacity"], opacityTokens...); err != nil {
1559 return err
1560 }
1561 }
1562 return nil
1563 }
1564
1565 func validateModalSort(path string, raw any, rawColumns any) error {
1566 if raw == nil {
1567 return nil
1568 }
1569 sortSpec, ok := raw.(map[string]any)
1570 if !ok {
1571 return fmt.Errorf("%s is not an object", path)
1572 }
1573 column, _ := sortSpec["column"].(string)
1574 if column == "" {
1575 return fmt.Errorf("%s.column is empty", path)
1576 }
1577 if err := optionalEnum(path+".direction", sortSpec["direction"], "asc", "desc"); err != nil {
1578 return err
1579 }
1580 columns, _ := rawColumns.([]any)
1581 for _, rawColumn := range columns {
1582 columnSpec, ok := rawColumn.(map[string]any)
1583 if !ok {
1584 continue
1585 }
1586 id, _ := columnSpec["id"].(string)
1587 if id == column {
1588 return nil
1589 }
1590 }
1591 return fmt.Errorf("%s.column references unknown modal column %q", path, column)
1592 }
1593
1594 func validateModalColumnRef(path string, raw any, columns map[string]string, expectedType string, optional bool) error {
1595 column, _ := raw.(string)
1596 if column == "" {
1597 if optional {
1598 return nil
1599 }
1600 return fmt.Errorf("%s is required", path)
1601 }
1602 columnType, ok := columns[column]
1603 if !ok {
1604 return fmt.Errorf("%s references unknown source column %q", path, column)
1605 }
1606 if expectedType != "" && columnType != expectedType {
1607 return fmt.Errorf("%s references non-%s source column %q (%s)", path, expectedType, column, columnType)
1608 }
1609 return nil
1610 }
1611
1612 func validateGraphPresentation(raw any, shape topologyShape) error {
1613 if raw == nil {
1614 return nil
1615 }
1616 presentation, ok := raw.(map[string]any)
1617 if !ok {
1618 return fmt.Errorf("data.presentation is not an object")
1619 }
1620 if err := validateSelectionPresentation(presentation["selection"], shape); err != nil {
1621 return err
1622 }
1623 if err := validateLegendPresentation(presentation["legend"], shape); err != nil {
1624 return err
1625 }
1626 return nil
1627 }
1628
1629 func validateBorderPresentation(path string, raw any) error {
1630 if raw == nil {
1631 return nil
1632 }
1633 border, ok := raw.(map[string]any)
1634 if !ok {
1635 return fmt.Errorf("%s is not an object", path)
1636 }
1637 if err := optionalEnum(path+".color_slot", border["color_slot"], colorSlotTokens...); err != nil {
1638 return err
1639 }
1640 return optionalEnum(path+".style", border["style"], "solid", "dashed", "dotted")
1641 }
1642
1643 func validateAnnotationPresentation(path string, raw any) error {
1644 if raw == nil {
1645 return nil
1646 }
1647 annotation, ok := raw.(map[string]any)
1648 if !ok {
1649 return fmt.Errorf("%s is not an object", path)
1650 }
1651 if err := optionalEnum(path+".color_slot", annotation["color_slot"], colorSlotTokens...); err != nil {
1652 return err
1653 }
1654 return optionalEnum(path+".style", annotation["style"], "ring", "dot", "none")
1655 }
1656
1657 func validateActorSizePresentation(path string, raw any, actorColumns map[string]string) error {
1658 if raw == nil {
1659 return nil
1660 }
1661 size, ok := raw.(map[string]any)
1662 if !ok {
1663 return fmt.Errorf("%s is not an object", path)
1664 }
1665 mode, err := requiredEnum(path+".mode", size["mode"], "fixed", "link_count", "metric")
1666 if err != nil {
1667 return err
1668 }
1669 if mode == "metric" {
1670 column, _ := size["metric_column"].(string)
1671 if column == "" {
1672 return fmt.Errorf("%s.metric_column is required when mode is metric", path)
1673 }
1674 columnType, ok := actorColumns[column]
1675 if !ok {
1676 return fmt.Errorf("%s.metric_column references unknown actor column %q", path, column)
1677 }
1678 if !isNumericColumnType(columnType) {
1679 return fmt.Errorf("%s.metric_column references non-numeric actor column %q (%s)", path, column, columnType)
1680 }
1681 }
1682 return optionalEnum(path+".scale", size["scale"], actorSizeScaleTokens...)
1683 }
1684
1685 func validateActorSearchPolicy(path string, raw any, actorColumns map[string]string) error {
1686 if raw == nil {
1687 return nil
1688 }
1689 search, ok := raw.(map[string]any)
1690 if !ok {
1691 return fmt.Errorf("%s is not an object", path)
1692 }
1693 if enabled, ok := search["enabled"]; ok {
1694 if _, ok := enabled.(bool); !ok {
1695 return fmt.Errorf("%s.enabled is not a boolean", path)
1696 }
1697 }
1698 for _, field := range []string{"columns", "label_keys"} {
1699 rawList, ok := search[field]
1700 if !ok {
1701 continue
1702 }
1703 values, ok := rawList.([]any)
1704 if !ok {
1705 return fmt.Errorf("%s.%s is not an array", path, field)
1706 }
1707 seen := make(map[string]struct{}, len(values))
1708 for i, rawValue := range values {
1709 value, ok := rawValue.(string)
1710 if !ok || value == "" {
1711 return fmt.Errorf("%s.%s[%d] is not a non-empty string", path, field, i)
1712 }
1713 if _, ok := seen[value]; ok {
1714 return fmt.Errorf("%s.%s[%d] duplicates %q", path, field, i, value)
1715 }
1716 seen[value] = struct{}{}
1717 if field != "columns" {
1718 continue
1719 }
1720 columnType, ok := actorColumns[value]
1721 if !ok {
1722 return fmt.Errorf("%s.columns[%d] references unknown actor column %q", path, i, value)
1723 }
1724 if !isDisplayColumnType(columnType) {
1725 return fmt.Errorf("%s.columns[%d] references non-display actor column %q (%s)", path, i, value, columnType)
1726 }
1727 }
1728 }
1729 return nil
1730 }
1731
1732 func validateLabelPolicy(path string, raw any, actorColumns map[string]string) error {
1733 if raw == nil {
1734 return nil
1735 }
1736 policy, ok := raw.(map[string]any)
1737 if !ok {
1738 return fmt.Errorf("%s is not an object", path)
1739 }
1740 if err := optionalEnum(path+".fallback", policy["fallback"], "type_label", "row_index", "none"); err != nil {
1741 return err
1742 }
1743 if err := optionalEnum(path+".array", policy["array"], "reject", "first", "summarize"); err != nil {
1744 return err
1745 }
1746 columns, ok := policy["columns"].([]any)
1747 if !ok {
1748 return nil
1749 }
1750 for i, rawColumn := range columns {
1751 column, ok := rawColumn.(string)
1752 if !ok || column == "" {
1753 return fmt.Errorf("%s.columns[%d] is not a non-empty string", path, i)
1754 }
1755 columnType, ok := actorColumns[column]
1756 if !ok {
1757 return fmt.Errorf("%s.columns[%d] references unknown actor column %q", path, i, column)
1758 }
1759 if !isDisplayColumnType(columnType) {
1760 return fmt.Errorf("%s.columns[%d] references non-display actor column %q (%s)", path, i, column, columnType)
1761 }
1762 }
1763 return nil
1764 }
1765
1766 func validateActorPortsPresentation(path string, raw any, shape topologyShape) error {
1767 if raw == nil {
1768 return nil
1769 }
1770 ports, ok := raw.(map[string]any)
1771 if !ok {
1772 return fmt.Errorf("%s is not an object", path)
1773 }
1774 showBullets, _ := ports["show_bullets"].(bool)
1775 sources, ok := ports["sources"].([]any)
1776 if showBullets && (!ok || len(sources) == 0) {
1777 return fmt.Errorf("%s.sources is required when show_bullets is true", path)
1778 }
1779 if !ok {
1780 return nil
1781 }
1782 for i, rawSource := range sources {
1783 source, ok := rawSource.(map[string]any)
1784 if !ok {
1785 return fmt.Errorf("%s.sources[%d] is not an object", path, i)
1786 }
1787 if err := validatePortSourcePresentation(fmt.Sprintf("%s.sources[%d]", path, i), source, shape); err != nil {
1788 return err
1789 }
1790 }
1791 return nil
1792 }
1793
1794 func validatePortSourcePresentation(path string, source map[string]any, shape topologyShape) error {
1795 sourceKind, err := requiredEnum(path+".source", source["source"], "links", "evidence", "actor_table")
1796 if err != nil {
1797 return err
1798 }
1799 defaultType, _ := source["default_type"].(string)
1800 if defaultType != "" {
1801 if _, ok := shape.portTypes[defaultType]; !ok {
1802 return fmt.Errorf("%s.default_type references unknown port type %q", path, defaultType)
1803 }
1804 }
1805
1806 var columns map[string]string
1807 switch sourceKind {
1808 case "links":
1809 columns = shape.linkColumns
1810 case "evidence":
1811 evidence, _ := source["evidence"].(string)
1812 if evidence == "" {
1813 return fmt.Errorf("%s.evidence is required when source is evidence", path)
1814 }
1815 columns = shape.evidenceTypes[evidence]
1816 if columns == nil {
1817 return fmt.Errorf("%s.evidence references unknown evidence type %q", path, evidence)
1818 }
1819 case "actor_table":
1820 table, _ := source["table"].(string)
1821 if table == "" {
1822 return fmt.Errorf("%s.table is required when source is actor_table", path)
1823 }
1824 columns = shape.actorTables[table]
1825 if columns == nil {
1826 columns = shape.tableTypes[table]
1827 }
1828 if columns == nil {
1829 return fmt.Errorf("%s.table references unknown actor table %q", path, table)
1830 }
1831 }
1832
1833 for _, field := range []string{"actor_column", "name_column"} {
1834 column, _ := source[field].(string)
1835 if column == "" {
1836 return fmt.Errorf("%s.%s is required", path, field)
1837 }
1838 if _, ok := columns[column]; !ok {
1839 return fmt.Errorf("%s.%s references unknown source column %q", path, field, column)
1840 }
1841 }
1842 actorColumn, _ := source["actor_column"].(string)
1843 if columnType := columns[actorColumn]; columnType != "actor_ref" {
1844 return fmt.Errorf("%s.actor_column references non-actor_ref source column %q (%s)", path, actorColumn, columnType)
1845 }
1846 nameColumn, _ := source["name_column"].(string)
1847 if columnType := columns[nameColumn]; !isDisplayColumnType(columnType) {
1848 return fmt.Errorf("%s.name_column references non-display source column %q (%s)", path, nameColumn, columnType)
1849 }
1850 valueColumn, _ := source["value_column"].(string)
1851 if valueColumn != "" {
1852 columnType, ok := columns[valueColumn]
1853 if !ok {
1854 return fmt.Errorf("%s.value_column references unknown source column %q", path, valueColumn)
1855 }
1856 if !isNumericColumnType(columnType) {
1857 return fmt.Errorf("%s.value_column references non-numeric source column %q (%s)", path, valueColumn, columnType)
1858 }
1859 }
1860 for _, field := range []string{"type_column", "status_column", "mode_column", "role_column", "sources_column"} {
1861 column, _ := source[field].(string)
1862 if column == "" {
1863 continue
1864 }
1865 if _, ok := columns[column]; !ok {
1866 if sourceKind == "actor_table" {
1867 continue
1868 }
1869 return fmt.Errorf("%s.%s references unknown source column %q", path, field, column)
1870 }
1871 }
1872 return nil
1873 }
1874
1875 func validateLinkVariablePresentation(path string, raw any, shape topologyShape) error {
1876 if raw == nil {
1877 return nil
1878 }
1879 variable, ok := raw.(map[string]any)
1880 if !ok {
1881 return fmt.Errorf("%s is not an object", path)
1882 }
1883 channel, err := requiredEnum(path+".channel", variable["channel"], "width", "opacity")
1884 if err != nil {
1885 return err
1886 }
1887 scaleKey, _ := variable["scale_key"].(string)
1888 if scaleKey == "" {
1889 return fmt.Errorf("%s.scale_key is required", path)
1890 }
1891 if _, ok := shape.scaleKeys[scaleKey]; !ok {
1892 return fmt.Errorf("%s.scale_key references unknown presentation scale key %q", path, scaleKey)
1893 }
1894 valueColumn, _ := variable["value_column"].(string)
1895 if valueColumn == "" {
1896 return fmt.Errorf("%s.value_column is required", path)
1897 }
1898 columnType, ok := shape.linkColumns[valueColumn]
1899 if !ok {
1900 return fmt.Errorf("%s.value_column references unknown link column %q", path, valueColumn)
1901 }
1902 if !isNumericColumnType(columnType) {
1903 return fmt.Errorf("%s.value_column references non-numeric link column %q (%s)", path, valueColumn, columnType)
1904 }
1905 allowed := widthTokens
1906 if channel == "opacity" {
1907 allowed = opacityTokens
1908 }
1909 if err := optionalEnum(path+".min", variable["min"], allowed...); err != nil {
1910 return err
1911 }
1912 return optionalEnum(path+".max", variable["max"], allowed...)
1913 }
1914
1915 func validateHoverPresentation(path string, raw any, columns map[string]string) error {
1916 if raw == nil {
1917 return nil
1918 }
1919 hover, ok := raw.(map[string]any)
1920 if !ok {
1921 return fmt.Errorf("%s is not an object", path)
1922 }
1923 fields, ok := hover["fields"].([]any)
1924 if !ok {
1925 return nil
1926 }
1927 for i, rawField := range fields {
1928 field, ok := rawField.(map[string]any)
1929 if !ok {
1930 return fmt.Errorf("%s.fields[%d] is not an object", path, i)
1931 }
1932 key, _ := field["key"].(string)
1933 if key == "" {
1934 return fmt.Errorf("%s.fields[%d].key is empty", path, i)
1935 }
1936 columnType, ok := columns[key]
1937 if !ok {
1938 return fmt.Errorf("%s.fields[%d].key references unknown column %q", path, i, key)
1939 }
1940 if !isDisplayColumnType(columnType) {
1941 return fmt.Errorf("%s.fields[%d].key references non-display column %q (%s)", path, i, key, columnType)
1942 }
1943 }
1944 return nil
1945 }
1946
1947 func validateCorrelation(raw any, shape topologyShape, ctx validationContext) error {
1948 if raw == nil {
1949 return nil
1950 }
1951 correlation, ok := raw.(map[string]any)
1952 if !ok {
1953 return fmt.Errorf("data.correlation is not an object")
1954 }
1955 rules, ok := correlation["rules"].(map[string]any)
1956 if !ok || len(rules) == 0 {
1957 return fmt.Errorf("data.correlation.rules is empty")
1958 }
1959
1960 ruleIDs := make(map[string]struct{}, len(rules))
1961 requiredColumnsByRule := make(map[string]map[string]struct{}, len(rules))
1962 for ruleID, rawRule := range rules {
1963 ruleIDs[ruleID] = struct{}{}
1964 requiredColumns := make(map[string]struct{})
1965 requiredColumnsByRule[ruleID] = requiredColumns
1966 rule, ok := rawRule.(map[string]any)
1967 if !ok {
1968 return fmt.Errorf("data.correlation.rules.%s is not an object", ruleID)
1969 }
1970 if _, err := requiredEnum("data.correlation.rules."+ruleID+".action", rule["action"], "absorb", "link"); err != nil {
1971 return err
1972 }
1973 if err := optionalEnum("data.correlation.rules."+ruleID+".class", rule["class"], "resolve_loose_side", "replace_actor", "merge_enrich_actor"); err != nil {
1974 return err
1975 }
1976 if _, ok := integerValue(rule["priority"]); !ok {
1977 return fmt.Errorf("data.correlation.rules.%s.priority is not an integer", ruleID)
1978 }
1979 outputLinkType, ok := rule["output_link_type"].(string)
1980 if !ok || outputLinkType == "" {
1981 return fmt.Errorf("data.correlation.rules.%s.output_link_type is empty", ruleID)
1982 }
1983 if _, ok := shape.linkTypes[outputLinkType]; !ok {
1984 return fmt.Errorf("data.correlation.rules.%s.output_link_type references unknown link type %q", ruleID, outputLinkType)
1985 }
1986 if err := validateIDArrayRefs("data.correlation.rules."+ruleID+".point_actor_types", rule["point_actor_types"], shape.actorTypes, "actor type"); err != nil {
1987 return err
1988 }
1989 if err := validateOptionalIDArrayRefs("data.correlation.rules."+ruleID+".claim_actor_types", rule["claim_actor_types"], shape.actorTypes, "actor type"); err != nil {
1990 return err
1991 }
1992 if err := validateOptionalIDArrayRefs("data.correlation.rules."+ruleID+".correlation_link_types", rule["correlation_link_types"], shape.linkTypes, "link type"); err != nil {
1993 return err
1994 }
1995 key, ok := rule["key"].([]any)
1996 if !ok || len(key) == 0 {
1997 return fmt.Errorf("data.correlation.rules.%s.key is empty", ruleID)
1998 }
1999 for i, rawPart := range key {
2000 part, ok := rawPart.(map[string]any)
2001 if !ok {
2002 return fmt.Errorf("data.correlation.rules.%s.key[%d] is not an object", ruleID, i)
2003 }
2004 if column, ok := part["column"].(string); ok && column != "" {
2005 requiredColumns[column] = struct{}{}
2006 continue
2007 }
2008 if literal, ok := part["literal"].(string); ok && literal != "" {
2009 continue
2010 }
2011 return fmt.Errorf("data.correlation.rules.%s.key[%d] must define column or literal", ruleID, i)
2012 }
2013 }
2014
2015 if err := validateCorrelationTable("data.correlation.points", correlation["points"], ctx, ruleIDs, requiredColumnsByRule); err != nil {
2016 return err
2017 }
2018 return validateCorrelationTable("data.correlation.claims", correlation["claims"], ctx, ruleIDs, requiredColumnsByRule)
2019 }
2020
2021 func validateCorrelationTable(path string, raw any, ctx validationContext, ruleIDs map[string]struct{}, requiredColumnsByRule map[string]map[string]struct{}) error {
2022 if raw == nil {
2023 return nil
2024 }
2025 if _, err := validateCompactTable(path, raw, ctx); err != nil {
2026 return err
2027 }
2028 columns, err := columnTypesFromTable(raw, path)
2029 if err != nil {
2030 return err
2031 }
2032 if columnType := columns["actor"]; columnType != "actor_ref" {
2033 return fmt.Errorf("%s.actor must be actor_ref, got %q", path, columnType)
2034 }
2035 ruleColumnType := columns["rule"]
2036 if ruleColumnType != "string" && ruleColumnType != "string_ref" {
2037 return fmt.Errorf("%s.rule must be string or string_ref, got %q", path, ruleColumnType)
2038 }
2039 referencedRules, err := collectStringColumnValuesInSet(path, raw, "rule", ruleIDs, "correlation rule", ctx.dictionaries)
2040 if err != nil {
2041 return err
2042 }
2043 for ruleID := range referencedRules {
2044 for column := range requiredColumnsByRule[ruleID] {
2045 if _, ok := columns[column]; !ok {
2046 return fmt.Errorf("%s is missing correlation key column %q for rule %q", path, column, ruleID)
2047 }
2048 }
2049 }
2050 return nil
2051 }
2052
2053 func validateIDArrayRefs(path string, raw any, known map[string]struct{}, kind string) error {
2054 values, ok := raw.([]any)
2055 if !ok || len(values) == 0 {
2056 return fmt.Errorf("%s is empty", path)
2057 }
2058 for i, rawValue := range values {
2059 value, ok := rawValue.(string)
2060 if !ok || value == "" {
2061 return fmt.Errorf("%s[%d] is not a non-empty string", path, i)
2062 }
2063 if _, ok := known[value]; !ok {
2064 return fmt.Errorf("%s[%d] references unknown %s %q", path, i, kind, value)
2065 }
2066 }
2067 return nil
2068 }
2069
2070 func validateOptionalIDArrayRefs(path string, raw any, known map[string]struct{}, kind string) error {
2071 if raw == nil {
2072 return nil
2073 }
2074 values, ok := raw.([]any)
2075 if !ok {
2076 return fmt.Errorf("%s is not an array", path)
2077 }
2078 for i, rawValue := range values {
2079 value, ok := rawValue.(string)
2080 if !ok || value == "" {
2081 return fmt.Errorf("%s[%d] is not a non-empty string", path, i)
2082 }
2083 if _, ok := known[value]; !ok {
2084 return fmt.Errorf("%s[%d] references unknown %s %q", path, i, kind, value)
2085 }
2086 }
2087 return nil
2088 }
2089
2090 func validateSelectionPresentation(raw any, shape topologyShape) error {
2091 if raw == nil {
2092 return nil
2093 }
2094 selection, ok := raw.(map[string]any)
2095 if !ok {
2096 return fmt.Errorf("data.presentation.selection is not an object")
2097 }
2098 rawActorClick := selection["actor_click"]
2099 if rawActorClick == nil {
2100 return nil
2101 }
2102 actorClick, ok := rawActorClick.(map[string]any)
2103 if !ok {
2104 return fmt.Errorf("data.presentation.selection.actor_click is not an object")
2105 }
2106 mode, err := requiredEnum("data.presentation.selection.actor_click.mode", actorClick["mode"], "none", "highlight_connections", "highlight_path")
2107 if err != nil {
2108 return err
2109 }
2110 if mode != "highlight_path" {
2111 return nil
2112 }
2113 table, _ := actorClick["path_table"].(string)
2114 if table == "" {
2115 return fmt.Errorf("data.presentation.selection.actor_click.path_table is required when mode is highlight_path")
2116 }
2117 columns := shape.actorTables[table]
2118 if columns == nil {
2119 columns = shape.tableTypes[table]
2120 if columns != nil && shape.tableTypeOwners[table] != "actor" {
2121 return fmt.Errorf("data.presentation.selection.actor_click.path_table references non-actor table %q", table)
2122 }
2123 }
2124 if columns == nil {
2125 return fmt.Errorf("data.presentation.selection.actor_click.path_table references unknown actor table %q", table)
2126 }
2127 for _, field := range []string{"path_actor_column", "path_order_column"} {
2128 column, _ := actorClick[field].(string)
2129 if column == "" {
2130 return fmt.Errorf("data.presentation.selection.actor_click.%s is required when mode is highlight_path", field)
2131 }
2132 if _, ok := columns[column]; !ok {
2133 return fmt.Errorf("data.presentation.selection.actor_click.%s references unknown path table column %q", field, column)
2134 }
2135 }
2136 actorColumn, _ := actorClick["path_actor_column"].(string)
2137 if columnType := columns[actorColumn]; columnType != "actor_ref" {
2138 return fmt.Errorf("data.presentation.selection.actor_click.path_actor_column references non-actor_ref path table column %q (%s)", actorColumn, columnType)
2139 }
2140 ownerColumn, _ := actorClick["path_owner_column"].(string)
2141 if ownerColumn != "" {
2142 columnType, ok := columns[ownerColumn]
2143 if !ok {
2144 return fmt.Errorf("data.presentation.selection.actor_click.path_owner_column references unknown path table column %q", ownerColumn)
2145 }
2146 if columnType != "actor_ref" {
2147 return fmt.Errorf("data.presentation.selection.actor_click.path_owner_column references non-actor_ref path table column %q (%s)", ownerColumn, columnType)
2148 }
2149 }
2150 orderColumn, _ := actorClick["path_order_column"].(string)
2151 if columnType := columns[orderColumn]; !isNumericColumnType(columnType) {
2152 return fmt.Errorf("data.presentation.selection.actor_click.path_order_column references non-numeric path table column %q (%s)", orderColumn, columnType)
2153 }
2154 return nil
2155 }
2156
2157 func validateLegendPresentation(raw any, shape topologyShape) error {
2158 if raw == nil {
2159 return nil
2160 }
2161 legend, ok := raw.(map[string]any)
2162 if !ok {
2163 return fmt.Errorf("data.presentation.legend is not an object")
2164 }
2165 if err := validateLegendEntries("data.presentation.legend.actors", legend["actors"], shape.actorTypes); err != nil {
2166 return err
2167 }
2168 if err := validateLegendEntries("data.presentation.legend.links", legend["links"], shape.linkTypes); err != nil {
2169 return err
2170 }
2171 return validateLegendEntries("data.presentation.legend.ports", legend["ports"], shape.portTypes)
2172 }
2173
2174 func validateLegendEntries(path string, raw any, known map[string]struct{}) error {
2175 if raw == nil {
2176 return nil
2177 }
2178 entries, ok := raw.([]any)
2179 if !ok {
2180 return fmt.Errorf("%s is not an array", path)
2181 }
2182 for i, rawEntry := range entries {
2183 entry, ok := rawEntry.(map[string]any)
2184 if !ok {
2185 return fmt.Errorf("%s[%d] is not an object", path, i)
2186 }
2187 typeID, _ := entry["type"].(string)
2188 if typeID == "" {
2189 return fmt.Errorf("%s[%d].type is empty", path, i)
2190 }
2191 if _, ok := known[typeID]; !ok {
2192 return fmt.Errorf("%s[%d].type references unknown type %q", path, i, typeID)
2193 }
2194 }
2195 return nil
2196 }
2197
2198 func validateReference(path string, row int, value any, maxRows int, name string) error {
2199 index, ok := integerValue(value)
2200 if !ok {
2201 return fmt.Errorf("%s[%d] is not an integer %s reference", path, row, name)
2202 }
2203 if index < 0 || index >= maxRows {
2204 return fmt.Errorf("%s[%d] %s reference out of bounds: %d", path, row, name, index)
2205 }
2206 return nil
2207 }
2208
2209 func columnTypesFromTable(raw any, path string) (map[string]string, error) {
2210 table, ok := raw.(map[string]any)
2211 if !ok {
2212 return nil, fmt.Errorf("%s is not an object", path)
2213 }
2214 return columnTypesFromRawColumns(table["columns"], path+".columns")
2215 }
2216
2217 func columnTypesFromRawColumns(raw any, path string) (map[string]string, error) {
2218 columns, ok := raw.([]any)
2219 if !ok {
2220 return nil, fmt.Errorf("%s is not an array", path)
2221 }
2222 types := make(map[string]string, len(columns))
2223 for i, rawColumn := range columns {
2224 column, ok := rawColumn.(map[string]any)
2225 if !ok {
2226 return nil, fmt.Errorf("%s[%d] is not an object", path, i)
2227 }
2228 id, _ := column["id"].(string)
2229 columnType, _ := column["type"].(string)
2230 if id == "" {
2231 return nil, fmt.Errorf("%s[%d].id is empty", path, i)
2232 }
2233 if columnType == "" {
2234 return nil, fmt.Errorf("%s[%d].type is empty", path, i)
2235 }
2236 types[id] = columnType
2237 }
2238 return types, nil
2239 }
2240
2241 func objectKeySet(raw any, path string) (map[string]struct{}, error) {
2242 obj, ok := raw.(map[string]any)
2243 if !ok {
2244 return nil, fmt.Errorf("%s is not an object", path)
2245 }
2246 keys := make(map[string]struct{}, len(obj))
2247 for key := range obj {
2248 keys[key] = struct{}{}
2249 }
2250 return keys, nil
2251 }
2252
2253 func optionalObjectKeySet(raw any, path string) (map[string]struct{}, error) {
2254 if raw == nil {
2255 return map[string]struct{}{}, nil
2256 }
2257 return objectKeySet(raw, path)
2258 }
2259
2260 func columnTypesByRegistryObject(raw any, path string) (map[string]map[string]string, error) {
2261 out := make(map[string]map[string]string)
2262 if raw == nil {
2263 return out, nil
2264 }
2265 obj, ok := raw.(map[string]any)
2266 if !ok {
2267 return nil, fmt.Errorf("%s is not an object", path)
2268 }
2269 for id, rawType := range obj {
2270 typeObj, ok := rawType.(map[string]any)
2271 if !ok {
2272 return nil, fmt.Errorf("%s.%s is not an object", path, id)
2273 }
2274 columns, err := columnTypesFromRawColumns(typeObj["columns"], path+"."+id+".columns")
2275 if err != nil {
2276 return nil, err
2277 }
2278 out[id] = columns
2279 }
2280 return out, nil
2281 }
2282
2283 func tableTypeOwners(raw any, path string) (map[string]string, error) {
2284 out := make(map[string]string)
2285 if raw == nil {
2286 return out, nil
2287 }
2288 obj, ok := raw.(map[string]any)
2289 if !ok {
2290 return nil, fmt.Errorf("%s is not an object", path)
2291 }
2292 for id, rawType := range obj {
2293 typeObj, ok := rawType.(map[string]any)
2294 if !ok {
2295 return nil, fmt.Errorf("%s.%s is not an object", path, id)
2296 }
2297 owner, _ := typeObj["owner"].(string)
2298 if owner != "" {
2299 out[id] = owner
2300 }
2301 }
2302 return out, nil
2303 }
2304
2305 func collectDetailTableColumnTypes(raw any) (map[string]map[string]string, map[string]map[string]string, error) {
2306 actorTables := make(map[string]map[string]string)
2307 relationshipTables := make(map[string]map[string]string)
2308 if raw == nil {
2309 return actorTables, relationshipTables, nil
2310 }
2311 tables, ok := raw.(map[string]any)
2312 if !ok {
2313 return nil, nil, fmt.Errorf("data.tables is not an object")
2314 }
2315 if err := collectDetailTableGroupColumnTypes(tables["actor"], "data.tables.actor", actorTables); err != nil {
2316 return nil, nil, err
2317 }
2318 if err := collectDetailTableGroupColumnTypes(tables["relationship"], "data.tables.relationship", relationshipTables); err != nil {
2319 return nil, nil, err
2320 }
2321 return actorTables, relationshipTables, nil
2322 }
2323
2324 func collectDetailTableGroupColumnTypes(raw any, path string, out map[string]map[string]string) error {
2325 if raw == nil {
2326 return nil
2327 }
2328 group, ok := raw.(map[string]any)
2329 if !ok {
2330 return fmt.Errorf("%s is not an object", path)
2331 }
2332 for name, rawDetail := range group {
2333 detail, ok := rawDetail.(map[string]any)
2334 if !ok {
2335 return fmt.Errorf("%s.%s is not an object", path, name)
2336 }
2337 columns, err := columnTypesFromTable(detail["table"], path+"."+name+".table")
2338 if err != nil {
2339 return err
2340 }
2341 out[name] = columns
2342 }
2343 return nil
2344 }
2345
2346 func collectScaleKeys(raw any) (map[string]struct{}, error) {
2347 keys := make(map[string]struct{})
2348 if raw == nil {
2349 return keys, nil
2350 }
2351 presentation, ok := raw.(map[string]any)
2352 if !ok {
2353 return nil, fmt.Errorf("data.presentation is not an object")
2354 }
2355 scaleKeys, ok := presentation["scale_keys"].(map[string]any)
2356 if !ok {
2357 return keys, nil
2358 }
2359 for key := range scaleKeys {
2360 keys[key] = struct{}{}
2361 }
2362 return keys, nil
2363 }
2364
2365 func collectOverlayTemplates(raw any) (map[string]overlayTemplateShape, error) {
2366 templates := make(map[string]overlayTemplateShape)
2367 if raw == nil {
2368 return templates, nil
2369 }
2370 rawTemplates, ok := raw.(map[string]any)
2371 if !ok {
2372 return nil, fmt.Errorf("data.types.overlay_templates is not an object")
2373 }
2374 for templateID, rawTemplate := range rawTemplates {
2375 template, ok := rawTemplate.(map[string]any)
2376 if !ok {
2377 return nil, fmt.Errorf("data.types.overlay_templates.%s is not an object", templateID)
2378 }
2379 if _, err := requiredEnum("data.types.overlay_templates."+templateID+".provider", template["provider"], overlayProviderTokens...); err != nil {
2380 return nil, err
2381 }
2382 if err := validateStringList("data.types.overlay_templates."+templateID+".contexts", template["contexts"], true); err != nil {
2383 return nil, err
2384 }
2385 if err := validateStringList("data.types.overlay_templates."+templateID+".dimensions", template["dimensions"], true); err != nil {
2386 return nil, err
2387 }
2388 selectorParams, err := collectStringList("data.types.overlay_templates."+templateID+".selector_params", template["selector_params"], true)
2389 if err != nil {
2390 return nil, err
2391 }
2392 for i, param := range selectorParams {
2393 if isOverlayRefsConventionColumn(param) {
2394 return nil, fmt.Errorf("data.types.overlay_templates.%s.selector_params[%d] uses reserved overlay refs column %q", templateID, i, param)
2395 }
2396 }
2397 merge, ok := template["merge"].(map[string]any)
2398 if !ok {
2399 return nil, fmt.Errorf("data.types.overlay_templates.%s.merge is not an object", templateID)
2400 }
2401 if _, err := requiredEnum("data.types.overlay_templates."+templateID+".merge.refs", merge["refs"], overlayMergeRefsTokens...); err != nil {
2402 return nil, err
2403 }
2404 if _, err := requiredEnum("data.types.overlay_templates."+templateID+".merge.values", merge["values"], overlayMergeValuesTokens...); err != nil {
2405 return nil, err
2406 }
2407 templates[templateID] = overlayTemplateShape{
2408 selectorParams: selectorParams,
2409 }
2410 }
2411 return templates, nil
2412 }
2413
2414 func isOverlayRefsConventionColumn(column string) bool {
2415 switch column {
2416 case OverlayRefsTemplateColumn, OverlayRefsActorColumn, OverlayRefsLinkColumn:
2417 return true
2418 default:
2419 return false
2420 }
2421 }
2422
2423 func validateStringList(path string, raw any, optional bool) error {
2424 _, err := collectStringList(path, raw, optional)
2425 return err
2426 }
2427
2428 func collectStringList(path string, raw any, optional bool) ([]string, error) {
2429 if raw == nil {
2430 if optional {
2431 return nil, nil
2432 }
2433 return nil, fmt.Errorf("%s is not an array", path)
2434 }
2435 rawValues, ok := raw.([]any)
2436 if !ok {
2437 return nil, fmt.Errorf("%s is not an array", path)
2438 }
2439 values := make([]string, 0, len(rawValues))
2440 seen := make(map[string]struct{}, len(rawValues))
2441 for i, rawValue := range rawValues {
2442 value, ok := rawValue.(string)
2443 if !ok || value == "" {
2444 return nil, fmt.Errorf("%s[%d] is not a non-empty string", path, i)
2445 }
2446 if _, ok := seen[value]; ok {
2447 return nil, fmt.Errorf("%s[%d] duplicates value %q", path, i, value)
2448 }
2449 seen[value] = struct{}{}
2450 values = append(values, value)
2451 }
2452 return values, nil
2453 }
2454
2455 func requiredEnum(path string, raw any, allowed ...string) (string, error) {
2456 value, ok := raw.(string)
2457 if !ok || value == "" {
2458 return "", fmt.Errorf("%s is not a non-empty string", path)
2459 }
2460 if !stringInSet(value, allowed) {
2461 return "", fmt.Errorf("%s has unsupported value %q", path, value)
2462 }
2463 return value, nil
2464 }
2465
2466 func optionalEnum(path string, raw any, allowed ...string) error {
2467 if raw == nil {
2468 return nil
2469 }
2470 value, ok := raw.(string)
2471 if !ok || value == "" {
2472 return fmt.Errorf("%s is not a non-empty string", path)
2473 }
2474 if !stringInSet(value, allowed) {
2475 return fmt.Errorf("%s has unsupported value %q", path, value)
2476 }
2477 return nil
2478 }
2479
2480 func stringValue(raw any) string {
2481 value, _ := raw.(string)
2482 return value
2483 }
2484
2485 func validateRequiredString(path string, raw any) error {
2486 value, ok := raw.(string)
2487 if !ok || value == "" {
2488 return fmt.Errorf("%s is required", path)
2489 }
2490 return nil
2491 }
2492
2493 func stringInSet(value string, allowed []string) bool {
2494 return slices.Contains(allowed, value)
2495 }
2496
2497 func isNumericColumnType(columnType string) bool {
2498 return columnType == "int" || columnType == "uint" || columnType == "float" || columnType == "duration"
2499 }
2500
2501 func isDisplayColumnType(columnType string) bool {
2502 switch columnType {
2503 case "bool", "int", "uint", "float", "string", "string_ref", "timestamp", "duration", "ip", "ip_ref", "mac", "mac_ref":
2504 return true
2505 default:
2506 return false
2507 }
2508 }
2509
2510 var (
2511 colorSlotTokens = []string{
2512 "primary", "secondary", "accent", "self", "neutral", "muted", "dim", "derived",
2513 "info", "structural", "warning", "success", "danger", "blue", "green", "orange",
2514 "purple", "cyan", "yellow", "teal", "gray",
2515 }
2516 opacityTokens = []string{"normal", "muted", "faded"}
2517 widthTokens = []string{"thin", "normal", "thick", "emphasis"}
2518 layoutStrengthTokens = []string{"weakest", "weaker", "normal", "stronger", "strongest"}
2519 layoutDistanceTokens = []string{"closest", "closer", "normal", "farther", "farthest"}
2520 actorSizeScaleTokens = []string{"compact", "normal", "emphasized"}
2521 linkSemanticRoleTokens = []string{"normal", "discovery", "ownership", "traffic", "correlation", "control"}
2522 overlayProviderTokens = []string{OverlayProviderNetdataMetrics, OverlayProviderNetdataFunction, OverlayProviderExternal}
2523 overlayMergeRefsTokens = []string{OverlayMergeRefsAppend, OverlayMergeRefsSet}
2524 overlayMergeValuesTokens = []string{
2525 OverlayMergeValuesSum, OverlayMergeValuesMin, OverlayMergeValuesMax, OverlayMergeValuesAvg, OverlayMergeValuesLast, OverlayMergeValuesNone,
2526 }
2527 iconTokens = []string{
2528 "router", "switch", "firewall", "access_point", "server", "storage", "load_balancer",
2529 "printer", "phone", "ups", "camera", "process", "agent", "netdata-agent", "parent",
2530 "remote-endpoint", "local-endpoint", "segment", "self", "ip", "cloud", "container",
2531 "vm", "database", "service", "datacenter", "cluster", "host", "network", "datastore",
2532 "datastore_cluster", "resource_pool", "device", "endpoint", "correlation", "interface",
2533 "group", "unknown",
2534 }
2535 )
2536
2537 func decodedTableRows(raw any) (int, error) {
2538 table, ok := raw.(map[string]any)
2539 if !ok {
2540 return 0, fmt.Errorf("table is not an object")
2541 }
2542 rows, ok := integerValue(table["rows"])
2543 if !ok || rows < 0 {
2544 return 0, fmt.Errorf("rows is not a non-negative integer")
2545 }
2546 return rows, nil
2547 }
2548
2549 func integerValue(raw any) (int, bool) {
2550 switch value := raw.(type) {
2551 case int:
2552 return value, true
2553 case int64:
2554 return int(value), true
2555 case uint64:
2556 if value > uint64(maxInt()) {
2557 return 0, false
2558 }
2559 return int(value), true
2560 case float64:
2561 if math.Trunc(value) != value {
2562 return 0, false
2563 }
2564 return int(value), true
2565 case json.Number:
2566 n, err := value.Int64()
2567 if err != nil {
2568 return 0, false
2569 }
2570 return int(n), true
2571 default:
2572 return 0, false
2573 }
2574 }
2575
2576 func numberValue(raw any) (float64, bool) {
2577 switch value := raw.(type) {
2578 case int:
2579 return float64(value), true
2580 case int64:
2581 return float64(value), true
2582 case uint64:
2583 return float64(value), true
2584 case float64:
2585 return value, true
2586 case json.Number:
2587 n, err := value.Float64()
2588 if err != nil {
2589 return 0, false
2590 }
2591 return n, true
2592 default:
2593 return 0, false
2594 }
2595 }
2596
2597 func maxInt() int {
2598 return int(^uint(0) >> 1)
2599 }