go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/cli/printer/mql.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package printer 5 6 import ( 7 "fmt" 8 "math" 9 "strconv" 10 "strings" 11 "time" 12 13 "go.mondoo.com/cnquery/llx" 14 "go.mondoo.com/cnquery/types" 15 "go.mondoo.com/cnquery/utils/sortx" 16 ) 17 18 // Results prints a full query with all data points 19 // NOTE: ensure that results only contains results that match the bundle! 20 func (print *Printer) Results(bundle *llx.CodeBundle, results map[string]*llx.RawResult) string { 21 assessment := llx.Results2Assessment(bundle, results) 22 23 if assessment != nil { 24 return print.Assessment(bundle, assessment) 25 } 26 27 var res strings.Builder 28 i := 0 29 for _, v := range results { 30 res.WriteString(print.Result(v, bundle)) 31 if len(results) > 1 && len(results) != i+1 { 32 res.WriteString("\n") 33 } 34 i++ 35 } 36 return res.String() 37 } 38 39 // Assessment prints a complete comparable assessment 40 func (print *Printer) Assessment(bundle *llx.CodeBundle, assessment *llx.Assessment) string { 41 var res strings.Builder 42 43 var indent string 44 if len(assessment.Results) > 1 { 45 if assessment.Success { 46 res.WriteString(print.Secondary("[ok] ")) 47 } else { 48 res.WriteString(print.Failed("[failed] ")) 49 } 50 res.WriteString(bundle.Source) 51 res.WriteString("\n") 52 // once we displayed the overall status, lets indent the subs 53 indent = " " 54 } 55 56 // indent all sub-results 57 for i := range assessment.Results { 58 cur := print.assessment(bundle, assessment.Results[i], indent) 59 res.WriteString(indent) 60 res.WriteString(cur) 61 res.WriteString("\n") 62 } 63 64 return res.String() 65 } 66 67 func isBooleanOp(op string) bool { 68 return op == "&&" || op == "||" 69 } 70 71 func (print *Printer) assessment(bundle *llx.CodeBundle, assessment *llx.AssessmentItem, indent string) string { 72 var codeID string 73 if bundle.CodeV2 != nil { 74 codeID = bundle.CodeV2.Id 75 } else { 76 return "error: no code id" 77 } 78 79 nextIndent := indent + " " 80 if assessment.Error != "" { 81 var res strings.Builder 82 res.WriteString(print.Failed("[failed] ")) 83 if bundle != nil && bundle.Labels != nil { 84 label := bundle.Labels.Labels[assessment.Checksum] 85 res.WriteString(label) 86 } 87 res.WriteString("\n") 88 res.WriteString(nextIndent) 89 res.WriteString("error: ") 90 res.WriteString(assessment.Error) 91 return res.String() 92 } 93 94 if assessment.Template != "" { 95 if assessment.Success { 96 return print.Secondary("[ok] true") 97 } 98 99 data := make([]string, len(assessment.Data)) 100 for i := range assessment.Data { 101 data[i] = print.Primitive(assessment.Data[i], codeID, bundle, indent) 102 } 103 res := print.Failed("[failed] ") + 104 print.assessmentTemplate(assessment.Template, data) 105 return indentBlock(res, indent) 106 } 107 108 if isBooleanOp(assessment.Operation) { 109 if assessment.Success { 110 return print.Secondary("[ok] true") 111 } 112 113 var res strings.Builder 114 res.WriteString(print.Secondary("[failed] ")) 115 res.WriteString(print.Primitive(assessment.Actual, codeID, bundle, nextIndent)) 116 res.WriteString(" " + assessment.Operation + " ") 117 res.WriteString(print.Primitive(assessment.Expected, codeID, bundle, nextIndent)) 118 return res.String() 119 } 120 121 if assessment.Success { 122 if assessment.Actual == nil { 123 return print.Secondary("[ok] ") + " (missing result)" 124 } 125 126 return print.Secondary("[ok]") + 127 " value: " + 128 print.Primitive(assessment.Actual, codeID, bundle, indent) 129 } 130 131 var res strings.Builder 132 res.WriteString(print.Failed("[failed] ")) 133 if bundle != nil && bundle.Labels != nil { 134 label, ok := bundle.Labels.Labels[assessment.Checksum] 135 if ok { 136 res.WriteString(label) 137 } 138 } 139 res.WriteString("\n") 140 141 if assessment.Expected != nil { 142 res.WriteString(nextIndent) 143 res.WriteString("expected: " + assessment.Operation + " ") 144 res.WriteString(print.Primitive(assessment.Expected, codeID, bundle, nextIndent)) 145 res.WriteString("\n") 146 } 147 148 if assessment.Actual != nil { 149 res.WriteString(nextIndent) 150 res.WriteString("actual: ") 151 res.WriteString(print.Primitive(assessment.Actual, codeID, bundle, nextIndent)) 152 } 153 154 return res.String() 155 } 156 157 // Result prints the llx raw result into a string 158 func (print *Printer) Result(result *llx.RawResult, bundle *llx.CodeBundle) string { 159 if result == nil { 160 return "< no result? >" 161 } 162 163 return print.DataWithLabel(result.Data, result.CodeID, bundle, "") 164 } 165 166 func (print *Printer) label(ref string, bundle *llx.CodeBundle, isResource bool) string { 167 if bundle == nil { 168 return "" 169 } 170 171 labels := bundle.Labels 172 if labels == nil { 173 return fmt.Sprintf("["+print.Primary("%s")+"] ", ref) 174 } 175 176 label := labels.Labels[ref] 177 if label == "" { 178 return "" 179 } 180 181 return print.Primary(label) + ": " 182 } 183 184 func (print *Printer) array(typ types.Type, data []interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 185 if len(data) == 0 { 186 return "[]" 187 } 188 189 var res strings.Builder 190 res.WriteString("[\n") 191 192 code := bundle.CodeV2 193 ep := code.Blocks[0].Entrypoints[0] 194 chunk := code.Chunk(ep) 195 listType := "" 196 switch chunk.Id { 197 case "$one", "$all", "$none", "$any": 198 ref := chunk.Function.Binding 199 listChunk := code.Chunk(ref) 200 listType = types.Type(listChunk.Type()).Child().Label() 201 listType += " " 202 } 203 204 for i := range data { 205 res.WriteString(fmt.Sprintf( 206 indent+" %d: %s%s\n", 207 i, 208 listType, 209 print.Data(typ.Child(), data[i], codeID, bundle, indent+" "), 210 )) 211 } 212 213 res.WriteString(indent + "]") 214 return res.String() 215 } 216 217 func (print *Printer) assessmentTemplate(template string, data []string) string { 218 res := template 219 for i := range data { 220 r := "$" + strconv.Itoa(i) 221 res = strings.ReplaceAll(res, r, data[i]) 222 } 223 return res 224 } 225 226 func (print *Printer) dataAssessment(codeID string, data map[string]interface{}, bundle *llx.CodeBundle) string { 227 var assertion *llx.AssertionMessage 228 var ok bool 229 230 assertion, ok = bundle.Assertions[codeID] 231 if !ok { 232 return "" 233 } 234 235 checksums := assertion.Checksums 236 if assertion.DecodeBlock { 237 if len(assertion.Checksums) < 2 { 238 return "" 239 } 240 241 v, ok := data[assertion.Checksums[0]] 242 if !ok { 243 return "" 244 } 245 raw, ok := v.(*llx.RawData) 246 if !ok { 247 return "" 248 } 249 data, ok = raw.Value.(map[string]interface{}) 250 if !ok { 251 return "" 252 } 253 checksums = checksums[1:] 254 } 255 256 fields := make([]string, len(checksums)) 257 for i := range checksums { 258 v, ok := data[checksums[i]] 259 if !ok { 260 return "" 261 } 262 263 val := v.(*llx.RawData) 264 fields[i] = print.Data(val.Type, val.Value, "", bundle, "") 265 } 266 267 return print.assessmentTemplate(assertion.Template, fields) 268 } 269 270 func (print *Printer) refMap(typ types.Type, data map[string]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 271 if len(data) == 0 { 272 return "{}" 273 } 274 275 var res strings.Builder 276 res.WriteString("{\n") 277 278 // we need to separate entries that are unlabelled (eg part of an assertion) 279 labeledKeys := []string{} 280 keys := sortx.Keys(data) 281 for i := range keys { 282 if _, ok := bundle.Labels.Labels[keys[i]]; ok { 283 labeledKeys = append(labeledKeys, keys[i]) 284 } 285 } 286 287 for _, k := range labeledKeys { 288 if k == "_" { 289 continue 290 } 291 292 v := data[k] 293 label := print.label(k, bundle, true) 294 val := v.(*llx.RawData) 295 296 if val.Error != nil { 297 res.WriteString(indent + " " + label + print.Error(val.Error.Error()) + "\n") 298 continue 299 } 300 301 if truthy, _ := val.IsTruthy(); !truthy { 302 assertion := print.dataAssessment(k, data, bundle) 303 if assertion != "" { 304 assertion = print.Failed("[failed]") + " " + strings.Trim(assertion, "\n\t ") 305 assertion = indentBlock(assertion, indent+" ") 306 res.WriteString(indent + " " + assertion + "\n") 307 continue 308 } 309 } 310 311 data := print.Data(val.Type, val.Value, k, bundle, indent+" ") 312 res.WriteString(indent + " " + label + data + "\n") 313 } 314 315 res.WriteString(indent + "}") 316 return res.String() 317 } 318 319 func (print *Printer) stringMap(typ types.Type, data map[string]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 320 if len(data) == 0 { 321 return "{}" 322 } 323 324 var res strings.Builder 325 res.WriteString("{\n") 326 327 keys := sortx.Keys(data) 328 for _, k := range keys { 329 v := data[k] 330 val := print.Data(typ.Child(), v, k, bundle, indent+" ") 331 res.WriteString(fmt.Sprintf(indent+" %s: %s\n", k, val)) 332 } 333 334 res.WriteString(indent + "}") 335 return res.String() 336 } 337 338 func (print *Printer) dict(typ types.Type, raw interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 339 if raw == nil { 340 return print.Secondary("null") 341 } 342 343 switch data := raw.(type) { 344 case bool: 345 if data { 346 return print.Secondary("true") 347 } 348 return print.Secondary("false") 349 350 case int64: 351 return print.Secondary(fmt.Sprintf("%d", data)) 352 353 case float64: 354 return print.Secondary(fmt.Sprintf("%f", data)) 355 356 case string: 357 return print.Secondary(llx.PrettyPrintString(data)) 358 359 case time.Time: 360 return print.Secondary(data.String()) 361 362 case []interface{}: 363 if len(data) == 0 { 364 return "[]" 365 } 366 367 var res strings.Builder 368 res.WriteString("[\n") 369 370 for i := range data { 371 res.WriteString(fmt.Sprintf( 372 indent+" %d: %s\n", 373 i, 374 print.dict(typ, data[i], "", bundle, indent+" "), 375 )) 376 } 377 378 res.WriteString(indent + "]") 379 return res.String() 380 381 case map[string]interface{}: 382 if len(data) == 0 { 383 return "{}" 384 } 385 386 var res strings.Builder 387 res.WriteString("{\n") 388 389 keys := sortx.Keys(data) 390 for _, k := range keys { 391 s := print.dict(typ, data[k], "", bundle, indent+" ") 392 res.WriteString(fmt.Sprintf(indent+" %s: %s\n", k, s)) 393 } 394 395 res.WriteString(indent + "}") 396 return res.String() 397 398 default: 399 return print.Secondary(fmt.Sprintf("%+v", raw)) 400 } 401 } 402 403 func (print *Printer) intMap(typ types.Type, data map[int]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 404 var res strings.Builder 405 res.WriteString("{\n") 406 407 for i := range data { 408 value := data[i] 409 res.WriteString(fmt.Sprintf(indent+" %d: "+print.Secondary("%#v")+"\n", i, value)) 410 } 411 412 res.WriteString(indent + "}") 413 return res.String() 414 } 415 416 var codeBlockIds = map[string]struct{}{ 417 "{}": {}, 418 "if": {}, 419 } 420 421 func isCodeBlock(codeID string, bundle *llx.CodeBundle) bool { 422 if bundle == nil { 423 return false 424 } 425 426 _, ok := bundle.Labels.Labels[codeID] 427 return ok 428 } 429 430 func (print *Printer) autoExpand(blockRef uint64, data interface{}, bundle *llx.CodeBundle, indent string) string { 431 var res strings.Builder 432 433 if arr, ok := data.([]interface{}); ok { 434 if len(arr) == 0 { 435 return "[]" 436 } 437 438 prefix := indent + " " 439 res.WriteString("[\n") 440 for i := range arr { 441 c := print.autoExpand(blockRef, arr[i], bundle, prefix) 442 res.WriteString(prefix) 443 res.WriteString(strconv.Itoa(i)) 444 res.WriteString(": ") 445 res.WriteString(c) 446 res.WriteByte('\n') 447 } 448 res.WriteString(indent + "]") 449 return res.String() 450 } 451 452 if data == nil { 453 return print.Secondary("null") 454 } 455 456 m, ok := data.(map[string]interface{}) 457 if !ok { 458 return "data is not a map to auto-expand" 459 } 460 461 block := bundle.CodeV2.Block(blockRef) 462 var name string 463 464 self, ok := m["_"].(*llx.RawData) 465 if ok { 466 name = self.Type.ResourceName() 467 } else if block != nil { 468 // We end up here when we deal with array resources. In that case, 469 // the block's first element is typed as an individual resource, so we can 470 // use that. 471 typ := block.Chunks[0].Type() 472 name = typ.ResourceName() 473 } 474 if name == "" { 475 name = "<unknown>" 476 } 477 478 res.WriteString(name) 479 480 if block != nil { 481 // important to process them in this order 482 for _, ref := range block.Entrypoints { 483 checksum := bundle.CodeV2.Checksums[ref] 484 v, ok := m[checksum] 485 if !ok { 486 continue 487 } 488 vv, ok := v.(*llx.RawData) 489 if !ok { 490 continue 491 } 492 493 label := bundle.Labels.Labels[checksum] 494 val := print.Data(vv.Type, vv.Value, checksum, bundle, indent) 495 res.WriteByte(' ') 496 res.WriteString(label) 497 res.WriteByte('=') 498 res.WriteString(val) 499 } 500 } 501 502 return res.String() 503 } 504 505 func (print *Printer) Data(typ types.Type, data interface{}, codeID string, bundle *llx.CodeBundle, indent string) string { 506 if typ.NotSet() { 507 return "no data available" 508 } 509 510 if blockRef, ok := bundle.AutoExpand[codeID]; ok { 511 return print.autoExpand(blockRef, data, bundle, indent) 512 } 513 514 switch typ.Underlying() { 515 case types.Any: 516 return indent + fmt.Sprintf("any: %#v", data) 517 case types.Ref: 518 if d, ok := data.(uint64); ok { 519 return "ref" + print.Primary(fmt.Sprintf("<%d,%d>", d>>32, d&0xFFFFFFFF)) 520 } else { 521 return "ref" + print.Primary(fmt.Sprintf("%d", data.(int32))) 522 } 523 case types.Nil: 524 return print.Secondary("null") 525 case types.Bool: 526 if data == nil { 527 return print.Secondary("null") 528 } 529 if data.(bool) { 530 return print.Secondary("true") 531 } 532 return print.Secondary("false") 533 case types.Int: 534 if data == nil { 535 return print.Secondary("null") 536 } 537 if data.(int64) == math.MaxInt64 { 538 return print.Secondary("Infinity") 539 } 540 if data.(int64) == math.MinInt64 { 541 return print.Secondary("-Infinity") 542 } 543 return print.Secondary(strconv.FormatInt(data.(int64), 10)) 544 case types.Float: 545 if data == nil { 546 return print.Secondary("null") 547 } 548 if math.IsInf(data.(float64), 1) { 549 return print.Secondary("Infinity") 550 } 551 if math.IsInf(data.(float64), -1) { 552 return print.Secondary("-Infinity") 553 } 554 return print.Secondary(strconv.FormatFloat(data.(float64), 'g', -1, 64)) 555 case types.String: 556 if data == nil { 557 return print.Secondary("null") 558 } 559 return print.Secondary(llx.PrettyPrintString(data.(string))) 560 case types.Regex: 561 if data == nil { 562 return print.Secondary("null") 563 } 564 return print.Secondary(fmt.Sprintf("/%s/", data)) 565 case types.Time: 566 if data == nil { 567 return print.Secondary("null") 568 } 569 time := data.(*time.Time) 570 if time == nil { 571 return print.Secondary("null") 572 } 573 574 if *time == llx.NeverPastTime || *time == llx.NeverFutureTime { 575 return print.Secondary("Never") 576 } 577 578 if time.Unix() > 0 { 579 return print.Secondary(time.String()) 580 } 581 582 seconds := llx.TimeToDuration(time) 583 minutes := seconds / 60 584 hours := minutes / 60 585 days := hours / 24 586 587 var res strings.Builder 588 if days > 0 { 589 res.WriteString(fmt.Sprintf("%d days ", days)) 590 } 591 if hours%24 != 0 { 592 res.WriteString(fmt.Sprintf("%d hours ", hours%24)) 593 } 594 if minutes%24 != 0 { 595 res.WriteString(fmt.Sprintf("%d minutes ", minutes%60)) 596 } 597 // if we haven't printed any of the other pieces (days/hours/minutes) then print this 598 // if we have, then check if this is non-zero 599 if minutes == 0 || seconds%60 != 0 { 600 res.WriteString(fmt.Sprintf("%d seconds", seconds%60)) 601 } 602 603 return print.Secondary(res.String()) 604 case types.Dict: 605 return print.dict(typ, data, codeID, bundle, indent) 606 607 case types.Score: 608 return llx.ScoreString(data.([]byte)) 609 610 case types.Block: 611 return print.refMap(typ, data.(map[string]interface{}), codeID, bundle, indent) 612 613 case types.ArrayLike: 614 if data == nil { 615 return print.Secondary("null") 616 } 617 return print.array(typ, data.([]interface{}), codeID, bundle, indent) 618 619 case types.MapLike: 620 if data == nil { 621 return print.Secondary("null") 622 } 623 if typ.Key() == types.String { 624 return print.stringMap(typ, data.(map[string]interface{}), codeID, bundle, indent) 625 } 626 if typ.Key() == types.Int { 627 return print.intMap(typ, data.(map[int]interface{}), codeID, bundle, indent) 628 } 629 return "unable to render map, its type is not supported: " + typ.Label() + ", raw: " + fmt.Sprintf("%#v", data) 630 631 case types.ResourceLike: 632 if data == nil { 633 return print.Secondary("null") 634 } 635 636 r := data.(llx.Resource) 637 idline := r.MqlName() 638 if id := r.MqlID(); id != "" { 639 idline += " id = " + id 640 } 641 642 return idline 643 case types.FunctionLike: 644 if d, ok := data.(uint64); ok { 645 return indent + fmt.Sprintf("=> <%d,0>", d>>32) 646 } else { 647 return indent + fmt.Sprintf("=> %#v", data) 648 } 649 default: 650 return indent + fmt.Sprintf("🤷 %#v", data) 651 } 652 } 653 654 // DataWithLabel prints RawData into a string 655 func (print *Printer) DataWithLabel(r *llx.RawData, codeID string, bundle *llx.CodeBundle, indent string) string { 656 b := strings.Builder{} 657 if r.Error != nil { 658 b.WriteString(print.Error(strings.TrimSpace(r.Error.Error()))) 659 b.WriteByte('\n') 660 } 661 662 b.WriteString(print.label(codeID, bundle, r.Type.IsResource())) 663 b.WriteString(print.Data(r.Type, r.Value, codeID, bundle, indent)) 664 return b.String() 665 } 666 667 // CodeBundle prints a bundle to a string 668 func (print *Printer) CodeBundle(bundle *llx.CodeBundle) string { 669 var res strings.Builder 670 671 res.WriteString(print.CodeV2(bundle.CodeV2, bundle, "")) 672 673 for idx := range bundle.Suggestions { 674 info := bundle.Suggestions[idx] 675 res.WriteString(print.Yellow("- suggestion: "+info.Field) + "\n") 676 } 677 678 return res.String() 679 } 680 681 func (print *Printer) CodeV2(code *llx.CodeV2, bundle *llx.CodeBundle, indent string) string { 682 var res strings.Builder 683 indent += " " 684 685 for i := range code.Blocks { 686 block := code.Blocks[i] 687 688 res.WriteString(print.Secondary(fmt.Sprintf("-> block %d\n", i+1))) 689 690 res.WriteString(indent) 691 res.WriteString("entrypoints: [") 692 for idx, ep := range block.Entrypoints { 693 res.WriteString(fmt.Sprintf("<%d,%d>", ep>>32, ep&0xFFFFFFFF)) 694 if idx != len(block.Entrypoints)-1 { 695 res.WriteString(" ") 696 } 697 } 698 res.WriteString("]\n") 699 700 for j := range block.Chunks { 701 res.WriteString(" ") 702 print.chunkV2(j, block.Chunks[j], block, bundle, indent, &res) 703 } 704 } 705 706 return res.String() 707 } 708 709 func (print *Printer) chunkV2(idx int, chunk *llx.Chunk, block *llx.Block, bundle *llx.CodeBundle, indent string, w *strings.Builder) { 710 sidx := strconv.Itoa(idx+1) + ": " 711 712 switch chunk.Call { 713 case llx.Chunk_FUNCTION: 714 w.WriteString(print.Primary(sidx)) 715 case llx.Chunk_PRIMITIVE: 716 w.WriteString(print.Secondary(sidx)) 717 default: 718 w.WriteString(print.Error(sidx)) 719 } 720 721 if chunk.Id != "" { 722 w.WriteString(chunk.Id + " ") 723 } 724 725 if chunk.Primitive != nil { 726 primitive := chunk.Primitive 727 728 // special case: function arguments are supplied by the caller, so we fill 729 // in the context for all resource references since they cannot have default values 730 if idx < int(block.Parameters) && len(primitive.Value) == 0 && types.Type(primitive.Type).IsResource() { 731 primitive = &llx.Primitive{Type: primitive.Type, Value: []byte("context")} 732 } 733 734 // FIXME: this is definitely the wrong ID 735 w.WriteString(print.Primitive(primitive, bundle.CodeV2.Id, bundle, indent)) 736 } 737 738 if chunk.Function != nil { 739 w.WriteString(print.functionV2(chunk.Function, bundle.CodeV2.Id, bundle, indent)) 740 } 741 742 w.WriteString("\n") 743 } 744 745 func (print *Printer) functionV2(f *llx.Function, codeID string, bundle *llx.CodeBundle, indent string) string { 746 argsStr := "" 747 if len(f.Args) > 0 { 748 argsStr = " (" + strings.TrimSpace(print.primitives(f.Args, codeID, bundle, indent)) + ")" 749 } 750 751 return "bind: <" + strconv.Itoa(int(f.Binding>>32)) + "," + 752 strconv.Itoa(int(f.Binding&0xFFFFFFFF)) + 753 "> type:" + types.Type(f.Type).Label() + 754 argsStr 755 } 756 757 func (print *Printer) primitives(list []*llx.Primitive, codeID string, bundle *llx.CodeBundle, indent string) string { 758 if len(list) == 0 { 759 return "" 760 } 761 var res strings.Builder 762 763 if len(list) >= 1 { 764 res.WriteString(print.Primitive(list[0], codeID, bundle, indent)) 765 } 766 767 for i := 1; i < len(list); i++ { 768 res.WriteString(", ") 769 res.WriteString(strings.TrimLeft(print.Primitive(list[i], codeID, bundle, indent), " ")) 770 } 771 return res.String() 772 } 773 774 // Primitive prints the llx primitive to a string 775 func (print *Printer) Primitive(primitive *llx.Primitive, codeID string, bundle *llx.CodeBundle, indent string) string { 776 if primitive == nil { 777 return "?" 778 } 779 780 if primitive.Value == nil && primitive.Array == nil && primitive.Map == nil { 781 return "_" 782 } 783 raw := primitive.RawData() 784 return print.Data(raw.Type, raw.Value, codeID, bundle, indent) 785 } 786 787 func indentBlock(text string, indent string) string { 788 return strings.ReplaceAll(text, "\n", "\n"+indent) 789 }