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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package explorer
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/rs/zerolog/log"
    14  	"go.mondoo.com/cnquery"
    15  	"go.mondoo.com/cnquery/checksums"
    16  	llx "go.mondoo.com/cnquery/llx"
    17  	"go.mondoo.com/cnquery/mqlc"
    18  	"go.mondoo.com/cnquery/mrn"
    19  	"go.mondoo.com/cnquery/types"
    20  	"go.mondoo.com/cnquery/utils/multierr"
    21  	"go.mondoo.com/cnquery/utils/sortx"
    22  	"google.golang.org/protobuf/proto"
    23  )
    24  
    25  // Compile a given query and return the bundle. Both v1 and v2 versions are compiled.
    26  // Both versions will be given the same code id.
    27  func (m *Mquery) Compile(props map[string]*llx.Primitive, schema llx.Schema) (*llx.CodeBundle, error) {
    28  	if m.Mql == "" {
    29  		if m.Query == "" {
    30  			return nil, errors.New("query is not implemented '" + m.Mrn + "'")
    31  		}
    32  		m.Mql = m.Query
    33  		m.Query = ""
    34  	}
    35  
    36  	v2Code, err := mqlc.Compile(m.Mql, props, mqlc.NewConfig(schema, cnquery.DefaultFeatures))
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	return v2Code, nil
    42  }
    43  
    44  func RefreshMRN(ownerMRN string, existingMRN string, resource string, uid string) (string, error) {
    45  	// NOTE: asset bundles may not have an owner set, therefore we skip if the query already has an mrn
    46  	if existingMRN != "" {
    47  		if !mrn.IsValid(existingMRN) {
    48  			return "", errors.New("invalid MRN: " + existingMRN)
    49  		}
    50  		return existingMRN, nil
    51  	}
    52  
    53  	if ownerMRN == "" {
    54  		return "", errors.New("cannot refresh MRN if the owner MRN is empty")
    55  	}
    56  
    57  	if uid == "" {
    58  		return "", errors.New("cannot refresh MRN with an empty UID")
    59  	}
    60  
    61  	mrn, err := mrn.NewChildMRN(ownerMRN, resource, uid)
    62  	if err != nil {
    63  		return "", err
    64  	}
    65  
    66  	return mrn.String(), nil
    67  }
    68  
    69  // RefreshMRN computes a MRN from the UID or validates the existing MRN.
    70  // Both of these need to fit the ownerMRN. It also removes the UID.
    71  func (m *Mquery) RefreshMRN(ownerMRN string) error {
    72  	nu, err := RefreshMRN(ownerMRN, m.Mrn, MRN_RESOURCE_QUERY, m.Uid)
    73  	if err != nil {
    74  		log.Error().Err(err).Str("owner", ownerMRN).Str("uid", m.Uid).Msg("failed to refresh mrn")
    75  		return multierr.Wrap(err, "failed to refresh mrn for query "+m.Title)
    76  	}
    77  
    78  	m.Mrn = nu
    79  	m.Uid = ""
    80  
    81  	for i := range m.Props {
    82  		if err := m.Props[i].RefreshMRN(ownerMRN); err != nil {
    83  			return err
    84  		}
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  // RefreshMRN computes a MRN from the UID or validates the existing MRN.
    91  // Both of these need to fit the ownerMRN. It also removes the UID.
    92  func (m *ObjectRef) RefreshMRN(ownerMRN string) error {
    93  	nu, err := RefreshMRN(ownerMRN, m.Mrn, MRN_RESOURCE_QUERY, m.Uid)
    94  	if err != nil {
    95  		log.Error().Err(err).Str("owner", ownerMRN).Str("uid", m.Uid).Msg("failed to refresh mrn")
    96  		return multierr.Wrap(err, "failed to refresh mrn for query reference "+m.Uid)
    97  	}
    98  
    99  	m.Mrn = nu
   100  	m.Uid = ""
   101  	return nil
   102  }
   103  
   104  // RefreshChecksum of a query without re-compiling anything. Properties cannot
   105  // be nil. Make sure everything has been compiled beforehand.
   106  //
   107  // Note: this will use whatever type and codeID we have in the query and
   108  // just compute a checksum from the rest.
   109  //
   110  // queries is an optional lookup that is necessary for composed queries,
   111  // since their internal checksum is not stored in this query.
   112  func (m *Mquery) RefreshChecksum(
   113  	ctx context.Context,
   114  	schema llx.Schema,
   115  	getQuery func(ctx context.Context, mrn string) (*Mquery, error),
   116  ) error {
   117  	c := checksums.New.
   118  		Add(m.Mql).
   119  		Add(m.CodeId).
   120  		Add(m.Mrn).
   121  		Add(m.Context).
   122  		Add(m.Type).
   123  		Add(m.Title).Add("v2").
   124  		AddUint(m.Impact.Checksum())
   125  
   126  	for i := range m.Props {
   127  		prop := m.Props[i]
   128  		if _, err := prop.RefreshChecksumAndType(schema); err != nil {
   129  			return err
   130  		}
   131  		if prop.Checksum == "" {
   132  			return errors.New("referenced property '" + prop.Mrn + "' checksum is empty")
   133  		}
   134  		c = c.Add(prop.Checksum)
   135  	}
   136  
   137  	for i := range m.Variants {
   138  		ref := m.Variants[i]
   139  		if q, err := getQuery(context.Background(), ref.Mrn); err == nil {
   140  			if err := q.RefreshChecksum(ctx, schema, getQuery); err != nil {
   141  				return err
   142  			}
   143  			if q.Checksum == "" {
   144  				return errors.New("referenced query '" + ref.Mrn + "'checksum is empty")
   145  			}
   146  			c = c.Add(q.Checksum)
   147  		} else {
   148  			return errors.New("cannot find dependent composed query '" + ref.Mrn + "'")
   149  		}
   150  	}
   151  
   152  	// TODO: filters don't support properties yet
   153  	if m.Filters != nil {
   154  		keys := sortx.Keys(m.Filters.Items)
   155  		for _, k := range keys {
   156  			query := m.Filters.Items[k]
   157  			if query.Checksum == "" {
   158  				// FIXME: we don't want this here, it should not be tied to the query
   159  				log.Warn().
   160  					Str("mql", m.Mql).
   161  					Str("filter", query.Mql).
   162  					Msg("refresh checksum on filter of query , which should have been pre-compiled")
   163  				query.RefreshAsFilter(m.Mrn, schema)
   164  				if query.Checksum == "" {
   165  					return errors.New("cannot refresh checksum for query, its filters were not compiled")
   166  				}
   167  			}
   168  			c = c.Add(query.Checksum)
   169  		}
   170  	}
   171  
   172  	if m.Docs != nil {
   173  		c = c.
   174  			Add(m.Docs.Desc).
   175  			Add(m.Docs.Audit)
   176  
   177  		if m.Docs.Remediation != nil {
   178  			for i := range m.Docs.Remediation.Items {
   179  				doc := m.Docs.Remediation.Items[i]
   180  				c = c.Add(doc.Id).Add(doc.Desc)
   181  			}
   182  		}
   183  
   184  		for i := range m.Docs.Refs {
   185  			c = c.
   186  				Add(m.Docs.Refs[i].Title).
   187  				Add(m.Docs.Refs[i].Url)
   188  		}
   189  	}
   190  
   191  	keys := sortx.Keys(m.Tags)
   192  	for _, k := range keys {
   193  		c = c.
   194  			Add(k).
   195  			Add(m.Tags[k])
   196  	}
   197  
   198  	m.Checksum = c.String()
   199  	return nil
   200  }
   201  
   202  // RefreshChecksumAndType by compiling the query and updating the Checksum field
   203  func (m *Mquery) RefreshChecksumAndType(queries map[string]*Mquery, props map[string]PropertyRef, schema llx.Schema) (*llx.CodeBundle, error) {
   204  	return m.refreshChecksumAndType(queries, props, schema)
   205  }
   206  
   207  type QueryMap map[string]*Mquery
   208  
   209  func (m QueryMap) GetQuery(ctx context.Context, mrn string) (*Mquery, error) {
   210  	if m == nil {
   211  		return nil, errors.New("query not found: " + mrn)
   212  	}
   213  
   214  	res, ok := m[mrn]
   215  	if !ok {
   216  		return nil, errors.New("query not found: " + mrn)
   217  	}
   218  	return res, nil
   219  }
   220  
   221  func (m *Mquery) refreshChecksumAndType(queries map[string]*Mquery, props map[string]PropertyRef, schema llx.Schema) (*llx.CodeBundle, error) {
   222  	localProps := map[string]*llx.Primitive{}
   223  	for i := range m.Props {
   224  		prop := m.Props[i]
   225  
   226  		if prop.Mrn == "" {
   227  			return nil, errors.New("missing MRN (or UID) for property in query " + m.Mrn)
   228  		}
   229  
   230  		v, ok := props[prop.Mrn]
   231  		if !ok {
   232  			return nil, errors.New("cannot find property " + prop.Mrn + " in query " + m.Mrn)
   233  		}
   234  
   235  		localProps[v.Name] = &llx.Primitive{
   236  			Type: v.Property.Type,
   237  		}
   238  
   239  		prop.Checksum = v.Checksum
   240  		prop.CodeId = v.CodeId
   241  		prop.Type = v.Type
   242  	}
   243  
   244  	// If this is a variant, we won't compile anything, since there is no MQL snippets
   245  	if len(m.Variants) != 0 {
   246  		if m.Mql != "" {
   247  			log.Warn().Str("msn", m.Mrn).Msg("a composed query is trying to define an mql snippet, which will be ignored")
   248  		}
   249  		return nil, m.RefreshChecksum(context.Background(), schema, QueryMap(queries).GetQuery)
   250  	}
   251  
   252  	bundle, err := m.Compile(localProps, schema)
   253  	if err != nil {
   254  		return bundle, multierr.Wrap(err, "failed to compile query '"+m.Mql+"'")
   255  	}
   256  
   257  	if bundle.GetCodeV2().GetId() == "" {
   258  		return bundle, errors.New("failed to compile query: received empty result values")
   259  	}
   260  
   261  	// We think its ok to always use the new code id
   262  	m.CodeId = bundle.CodeV2.Id
   263  
   264  	// the compile step also dedents the code
   265  	m.Mql = bundle.Source
   266  
   267  	// TODO: record multiple entrypoints and types
   268  	// TODO(jaym): is it possible that the 2 could produce different types
   269  	if entrypoints := bundle.CodeV2.Entrypoints(); len(entrypoints) == 1 {
   270  		ep := entrypoints[0]
   271  		chunk := bundle.CodeV2.Chunk(ep)
   272  		typ := chunk.Type()
   273  		m.Type = string(typ)
   274  	} else {
   275  		m.Type = string(types.Any)
   276  	}
   277  
   278  	return bundle, m.RefreshChecksum(context.Background(), schema, QueryMap(queries).GetQuery)
   279  }
   280  
   281  // RefreshAsFilter filters treats this query as an asset filter and sets its Mrn, Title, and Checksum
   282  func (m *Mquery) RefreshAsFilter(mrn string, schema llx.Schema) (*llx.CodeBundle, error) {
   283  	bundle, err := m.refreshChecksumAndType(nil, nil, schema)
   284  	if err != nil {
   285  		return bundle, err
   286  	}
   287  	if bundle == nil {
   288  		return nil, errors.New("filters require MQL snippets (no compiled code generated)")
   289  	}
   290  
   291  	if mrn != "" {
   292  		m.Mrn = mrn + "/filter/" + m.CodeId
   293  	}
   294  
   295  	if m.Title == "" {
   296  		m.Title = m.Query
   297  	}
   298  
   299  	return bundle, nil
   300  }
   301  
   302  // Sanitize ensure the content is in good shape and removes leading and trailing whitespace
   303  func (m *Mquery) Sanitize() {
   304  	if m == nil {
   305  		return
   306  	}
   307  
   308  	if m.Docs != nil {
   309  		m.Docs.Desc = strings.TrimSpace(m.Docs.Desc)
   310  		m.Docs.Audit = strings.TrimSpace(m.Docs.Audit)
   311  
   312  		if m.Docs.Remediation != nil {
   313  			for i := range m.Docs.Remediation.Items {
   314  				doc := m.Docs.Remediation.Items[i]
   315  				doc.Desc = strings.TrimSpace(doc.Desc)
   316  			}
   317  		}
   318  
   319  		for i := range m.Docs.Refs {
   320  			r := m.Docs.Refs[i]
   321  			r.Title = strings.TrimSpace(r.Title)
   322  			r.Url = strings.TrimSpace(r.Url)
   323  		}
   324  	}
   325  
   326  	if m.Tags != nil {
   327  		sanitizedTags := map[string]string{}
   328  		for k, v := range m.Tags {
   329  			sk := strings.TrimSpace(k)
   330  			sv := strings.TrimSpace(v)
   331  			sanitizedTags[sk] = sv
   332  		}
   333  		m.Tags = sanitizedTags
   334  	}
   335  }
   336  
   337  // Merge a given query with a base query and create a new query object as a
   338  // result of it. Anything that is not set in the query, is pulled from the base.
   339  func (m *Mquery) Merge(base *Mquery) *Mquery {
   340  	// TODO: lots of potential to speed things up here
   341  	res := proto.Clone(m).(*Mquery)
   342  	res.AddBase(base)
   343  	return res
   344  }
   345  
   346  // AddBase adds a base query into the query object. Anything that is not set
   347  // in the query, is pulled from the base.
   348  func (m *Mquery) AddBase(base *Mquery) {
   349  	if m.Mql == "" {
   350  		// MQL, type and codeID go hand in hand, so make sure to always pull them
   351  		// fully when doing this.
   352  		m.Mql = base.Mql
   353  		m.CodeId = base.CodeId
   354  		m.Type = base.Type
   355  	}
   356  	if m.Type == "" {
   357  		m.Type = base.Type
   358  	}
   359  	if m.Context == "" {
   360  		m.Context = base.Context
   361  	}
   362  	if m.Title == "" {
   363  		m.Title = base.Title
   364  	}
   365  	if m.Docs == nil {
   366  		m.Docs = base.Docs
   367  	} else if base.Docs != nil {
   368  		if m.Docs.Desc == "" {
   369  			m.Docs.Desc = base.Docs.Desc
   370  		}
   371  		if m.Docs.Audit == "" {
   372  			m.Docs.Audit = base.Docs.Audit
   373  		}
   374  		if m.Docs.Remediation == nil {
   375  			m.Docs.Remediation = base.Docs.Remediation
   376  		}
   377  		if m.Docs.Refs == nil {
   378  			m.Docs.Refs = base.Docs.Refs
   379  		}
   380  	}
   381  	if m.Desc == "" {
   382  		m.Desc = base.Desc
   383  	}
   384  	if m.Impact == nil {
   385  		m.Impact = base.Impact
   386  	} else {
   387  		m.Impact.AddBase(base.Impact)
   388  	}
   389  	if m.Tags == nil {
   390  		m.Tags = base.Tags
   391  	}
   392  	if m.Filters == nil {
   393  		m.Filters = base.Filters
   394  	}
   395  	if m.Props == nil {
   396  		m.Props = base.Props
   397  	}
   398  	if m.Variants == nil {
   399  		m.Variants = base.Variants
   400  	}
   401  }
   402  
   403  func (r *Remediation) UnmarshalJSON(data []byte) error {
   404  	var res string
   405  	if err := json.Unmarshal(data, &res); err == nil {
   406  		r.Items = []*TypedDoc{{Id: "default", Desc: res}}
   407  		return nil
   408  	}
   409  
   410  	if err := json.Unmarshal(data, &r.Items); err == nil {
   411  		return nil
   412  	}
   413  
   414  	// prevent recursive calls into UnmarshalJSON with a placeholder type
   415  	type tmp Remediation
   416  	return json.Unmarshal(data, (*tmp)(r))
   417  }
   418  
   419  func (r *Remediation) MarshalJSON() ([]byte, error) {
   420  	if r == nil {
   421  		return []byte{}, nil
   422  	}
   423  	return json.Marshal(r.Items)
   424  }
   425  
   426  func ChecksumFilters(queries []*Mquery, schema llx.Schema) (string, error) {
   427  	for i := range queries {
   428  		if _, err := queries[i].refreshChecksumAndType(nil, nil, schema); err != nil {
   429  			return "", multierr.Wrap(err, "failed to compile query")
   430  		}
   431  	}
   432  
   433  	sort.Slice(queries, func(i, j int) bool {
   434  		return queries[i].CodeId < queries[j].CodeId
   435  	})
   436  
   437  	afc := checksums.New
   438  	for i := range queries {
   439  		afc = afc.Add(queries[i].CodeId)
   440  	}
   441  
   442  	return afc.String(), nil
   443  }