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 }