go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/mqlc/labels.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package mqlc
     5  
     6  import (
     7  	"errors"
     8  	"regexp"
     9  	"strconv"
    10  
    11  	"go.mondoo.com/cnquery/llx"
    12  	"go.mondoo.com/cnquery/types"
    13  	"golang.org/x/text/transform"
    14  	"golang.org/x/text/unicode/norm"
    15  )
    16  
    17  func createLabel(res *llx.CodeBundle, ref uint64, schema llx.Schema) (string, error) {
    18  	code := res.CodeV2
    19  	chunk := code.Chunk(ref)
    20  
    21  	if chunk.Call == llx.Chunk_PRIMITIVE {
    22  		// In the case of refs, we want to check for the name of the variable,
    23  		// which is what every final ref should lead to
    24  		if chunk.Primitive.Type == string(types.Ref) {
    25  			if deref, ok := chunk.Primitive.RefV2(); ok {
    26  				ref = deref
    27  			}
    28  		}
    29  
    30  		// TODO: better labels if we don't have it as a var
    31  		label := res.Vars[ref]
    32  
    33  		return label, nil
    34  	}
    35  
    36  	id := chunk.Id
    37  	if chunk.Function == nil {
    38  		return id, nil
    39  	}
    40  
    41  	// TODO: workaround to get past the builtin global call
    42  	// this needs proper handling for global calls
    43  	if chunk.Function.Binding == 0 && id != "if" && id != "createResource" {
    44  		return id, nil
    45  	}
    46  
    47  	var parentLabel string
    48  	var err error
    49  	if id == "createResource" {
    50  		if ref, ok := chunk.Function.Args[0].RefV2(); ok {
    51  			parentLabel, err = createLabel(res, ref, schema)
    52  			if err != nil {
    53  				return "", err
    54  			}
    55  		}
    56  	} else if chunk.Function.Binding != 0 {
    57  		parentLabel, err = createLabel(res, chunk.Function.Binding, schema)
    58  		if err != nil {
    59  			return "", err
    60  		}
    61  	}
    62  
    63  	var label string
    64  	switch id {
    65  	case "[]":
    66  		if len(chunk.Function.Args) != 1 {
    67  			panic("don't know how to extract label data from array access without args")
    68  		}
    69  
    70  		arg := chunk.Function.Args[0].RawData()
    71  		idx := arg.Value
    72  
    73  		switch arg.Type {
    74  		case types.Int:
    75  			label = "[" + strconv.FormatInt(idx.(int64), 10) + "]"
    76  		case types.String:
    77  			if chunk.Function.Type == string(types.Dict) && isAccessor(idx.(string)) {
    78  				label = idx.(string)
    79  			} else {
    80  				label = "[" + idx.(string) + "]"
    81  			}
    82  		default:
    83  			panic("cannot label array index of type " + arg.Type.Label())
    84  		}
    85  		if parentLabel != "" {
    86  			if label != "" && label[0] == '[' {
    87  				label = parentLabel + label
    88  			} else {
    89  				label = parentLabel + "." + label
    90  			}
    91  		}
    92  	case "{}", "${}":
    93  		label = parentLabel
    94  	case "createResource":
    95  		label = parentLabel + "." + string(chunk.Type())
    96  	case "if":
    97  		label = "if"
    98  	default:
    99  		if x, ok := llx.ComparableLabel(id); ok {
   100  			arg := chunk.Function.Args[0].LabelV2(code)
   101  			label = parentLabel + " " + x + " " + arg
   102  		} else if parentLabel == "" {
   103  			label = id
   104  		} else {
   105  			label = parentLabel + "." + id
   106  		}
   107  	}
   108  
   109  	// TODO: figure out why this string includes control characters in the first place
   110  	return stripCtlAndExtFromUnicode(label), nil
   111  }
   112  
   113  var reAccessor = regexp.MustCompile(`^[\p{L}\d_]+$`)
   114  
   115  func isAccessor(s string) bool {
   116  	return reAccessor.MatchString(s)
   117  }
   118  
   119  // Unicode normalization and filtering, see http://blog.golang.org/normalization and
   120  // http://godoc.org/golang.org/x/text/unicode/norm for more details.
   121  func stripCtlAndExtFromUnicode(str string) string {
   122  	isOk := func(r rune) bool {
   123  		return r < 32 || r >= 127
   124  	}
   125  	// The isOk filter is such that there is no need to chain to norm.NFC
   126  	t := transform.Chain(norm.NFKD, transform.RemoveFunc(isOk))
   127  	str, _, _ = transform.String(t, str)
   128  	return str
   129  }
   130  
   131  // UpdateLabels for the given code under the schema
   132  func UpdateLabels(res *llx.CodeBundle, schema llx.Schema) error {
   133  	if res == nil || res.CodeV2 == nil {
   134  		return errors.New("cannot create labels without code")
   135  	}
   136  
   137  	for i := range res.CodeV2.Blocks {
   138  		err := updateLabels(res, res.CodeV2.Blocks[i], schema)
   139  		if err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	// not needed anymore since we have all the info in labels now
   145  	res.Vars = nil
   146  
   147  	return nil
   148  }
   149  
   150  func updateLabels(res *llx.CodeBundle, block *llx.Block, schema llx.Schema) error {
   151  	datapoints := block.Datapoints
   152  	code := res.CodeV2
   153  	labels := res.Labels.Labels
   154  
   155  	// We don't want assertions to become labels. Their data should not be printed
   156  	// regularly but instead be processed through the assertion itself
   157  	if code.Assertions != nil {
   158  		assertionPoints := map[uint64]struct{}{}
   159  		for _, assertion := range code.Assertions {
   160  			for j := range assertion.Refs {
   161  				assertionPoints[assertion.Refs[j]] = struct{}{}
   162  			}
   163  		}
   164  
   165  		filtered := []uint64{}
   166  		for i := range datapoints {
   167  			ref := datapoints[i]
   168  			if _, ok := assertionPoints[ref]; ok {
   169  				continue
   170  			}
   171  			filtered = append(filtered, ref)
   172  		}
   173  		datapoints = filtered
   174  	}
   175  
   176  	labelrefs := append(block.Entrypoints, datapoints...)
   177  
   178  	var err error
   179  	for _, entrypoint := range labelrefs {
   180  		checksum, ok := code.Checksums[entrypoint]
   181  		if !ok {
   182  			return errors.New("failed to create labels, cannot find checksum for this entrypoint " + strconv.FormatUint(uint64(entrypoint), 10))
   183  		}
   184  
   185  		if _, ok := labels[checksum]; ok {
   186  			continue
   187  		}
   188  
   189  		labels[checksum], err = createLabel(res, entrypoint, schema)
   190  
   191  		if err != nil {
   192  			return err
   193  		}
   194  	}
   195  
   196  	// any more checksums that might have been set need to be removed, since we don't need them
   197  	// TODO: there must be a way to do this without having to create the label first
   198  	if code.Assertions != nil {
   199  		for _, assertion := range code.Assertions {
   200  			if !assertion.DecodeBlock {
   201  				continue
   202  			}
   203  			for i := 0; i < len(assertion.Checksums); i++ {
   204  				delete(labels, assertion.Checksums[i])
   205  			}
   206  		}
   207  	}
   208  
   209  	return nil
   210  }