go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/recording.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package providers 5 6 import ( 7 "encoding/json" 8 "errors" 9 "os" 10 "sort" 11 12 "github.com/rs/zerolog/log" 13 "go.mondoo.com/cnquery/llx" 14 "go.mondoo.com/cnquery/providers-sdk/v1/inventory" 15 "go.mondoo.com/cnquery/types" 16 "go.mondoo.com/cnquery/utils/multierr" 17 ) 18 19 type Recording interface { 20 Save() error 21 EnsureAsset(asset *inventory.Asset, provider string, connectionID uint32, conf *inventory.Config) 22 AddData(connectionID uint32, resource string, id string, field string, data *llx.RawData) 23 GetData(connectionID uint32, resource string, id string, field string) (*llx.RawData, bool) 24 GetResource(connectionID uint32, resource string, id string) (map[string]*llx.RawData, bool) 25 } 26 27 type recording struct { 28 Assets []assetRecording `json:"assets"` 29 Path string `json:"-"` 30 // assets is used for fast connection to asset lookup 31 assets map[uint32]*assetRecording `json:"-"` 32 prettyPrintJSON bool `json:"-"` 33 } 34 35 // ReadOnly converts the recording into a read-only recording 36 func (r *recording) ReadOnly() *readOnlyRecording { 37 return &readOnlyRecording{r} 38 } 39 40 type assetRecording struct { 41 Asset assetInfo `json:"asset"` 42 Connections []connectionRecording `json:"connections"` 43 Resources []resourceRecording `json:"resources"` 44 45 connections map[string]*connectionRecording `json:"-"` 46 resources map[string]*resourceRecording `json:"-"` 47 } 48 49 type assetInfo struct { 50 ID string `json:"id"` 51 PlatformIDs []string `json:"platformIDs,omitempty"` 52 Name string `json:"name,omitempty"` 53 Arch string `json:"arch,omitempty"` 54 Title string `json:"title,omitempty"` 55 Family []string `json:"family,omitempty"` 56 Build string `json:"build,omitempty"` 57 Version string `json:"version,omitempty"` 58 Kind string `json:"kind,omitempty"` 59 Runtime string `json:"runtime,omitempty"` 60 Labels map[string]string `json:"labels,omitempty"` 61 } 62 63 type connectionRecording struct { 64 Url string `json:"url"` 65 ProviderID string `json:"provider"` 66 Connector string `json:"connector"` 67 Version string `json:"version"` 68 id uint32 `json:"-"` 69 } 70 71 type resourceRecording struct { 72 Resource string 73 ID string 74 Fields map[string]*llx.RawData 75 } 76 77 type NullRecording struct{} 78 79 func (n NullRecording) Save() error { 80 return nil 81 } 82 83 func (n NullRecording) EnsureAsset(asset *inventory.Asset, provider string, connectionID uint32, conf *inventory.Config) { 84 } 85 86 func (n NullRecording) AddData(connectionID uint32, resource string, id string, field string, data *llx.RawData) { 87 } 88 89 func (n NullRecording) GetData(connectionID uint32, resource string, id string, field string) (*llx.RawData, bool) { 90 return nil, false 91 } 92 93 func (n NullRecording) GetResource(connectionID uint32, resource string, id string) (map[string]*llx.RawData, bool) { 94 return nil, false 95 } 96 97 type readOnlyRecording struct { 98 *recording 99 } 100 101 func (n *readOnlyRecording) Save() error { 102 return nil 103 } 104 105 func (n *readOnlyRecording) EnsureAsset(asset *inventory.Asset, provider string, connectionID uint32, conf *inventory.Config) { 106 // For read-only recordings we are still loading from file, so that means 107 // we are severly lacking connection IDs. 108 found, _ := n.findAssetConnID(asset, conf) 109 if found != -1 { 110 n.assets[connectionID] = &n.Assets[found] 111 } 112 } 113 114 func (n *readOnlyRecording) AddData(connectionID uint32, resource string, id string, field string, data *llx.RawData) { 115 } 116 117 type RecordingOptions struct { 118 DoRecord bool 119 PrettyPrintJSON bool 120 } 121 122 // NewRecording loads and creates a new recording based on user settings. 123 // If no recording is available and users don't wish to record, it throws an error. 124 // If users don't wish to record and no recording is available, it will return 125 // the null-recording. 126 func NewRecording(path string, opts RecordingOptions) (Recording, error) { 127 if path == "" { 128 // we don't want to record and we don't want to load a recording path... 129 // so there is nothing to do, so return nil 130 if !opts.DoRecord { 131 return NullRecording{}, nil 132 } 133 // for all remaining cases we do want to record and we want to check 134 // if the recording exists at the default location 135 path = "recording.json" 136 } 137 138 if _, err := os.Stat(path); err == nil { 139 res, err := LoadRecordingFile(path) 140 if err != nil { 141 return nil, multierr.Wrap(err, "failed to load recording") 142 } 143 res.Path = path 144 145 if opts.DoRecord { 146 res.prettyPrintJSON = opts.PrettyPrintJSON 147 return res, nil 148 } 149 return &readOnlyRecording{res}, nil 150 151 } else if errors.Is(err, os.ErrNotExist) { 152 if opts.DoRecord { 153 res := &recording{ 154 Path: path, 155 prettyPrintJSON: opts.PrettyPrintJSON, 156 } 157 res.refreshCache() // only for initialization 158 return res, nil 159 } 160 return nil, errors.New("failed to load recording: '" + path + "' does not exist") 161 162 } else { 163 // Schrodinger's file, may be permissions or something else... 164 return nil, multierr.Wrap(err, "failed to access recording in '"+path+"'") 165 } 166 } 167 168 func LoadRecordingFile(path string) (*recording, error) { 169 raw, err := os.ReadFile(path) 170 if err != nil { 171 return nil, err 172 } 173 174 var res recording 175 err = json.Unmarshal(raw, &res) 176 if err != nil { 177 return nil, err 178 } 179 180 pres := &res 181 pres.refreshCache() 182 183 if err = pres.reconnectResources(); err != nil { 184 return nil, err 185 } 186 187 return pres, err 188 } 189 190 func (r *recording) Save() error { 191 r.finalize() 192 193 var raw []byte 194 var err error 195 if r.prettyPrintJSON { 196 raw, err = json.MarshalIndent(r, "", " ") 197 } else { 198 raw, err = json.Marshal(r) 199 } 200 if err != nil { 201 return multierr.Wrap(err, "failed to marshal json for recording") 202 } 203 204 if err := os.WriteFile(r.Path, raw, 0o644); err != nil { 205 return multierr.Wrap(err, "failed to store recording") 206 } 207 208 log.Info().Msg("stored recording in " + r.Path) 209 return nil 210 } 211 212 func (r *recording) refreshCache() { 213 r.assets = make(map[uint32]*assetRecording, len(r.Assets)) 214 for i := range r.Assets { 215 asset := &r.Assets[i] 216 asset.resources = make(map[string]*resourceRecording, len(asset.Resources)) 217 asset.connections = make(map[string]*connectionRecording, len(asset.Connections)) 218 219 for j := range asset.Resources { 220 resource := &asset.Resources[j] 221 asset.resources[resource.Resource+"\x00"+resource.ID] = resource 222 } 223 224 for j := range asset.Connections { 225 conn := &asset.Connections[j] 226 asset.connections[conn.Url] = conn 227 228 // only connection ID's != 0 are valid IDs. We get lots of 0 when we 229 // initially load this object, so we won't know yet which asset belongs 230 // to which connection. 231 if conn.id != 0 { 232 r.assets[conn.id] = asset 233 } 234 } 235 } 236 } 237 238 func (r *recording) reconnectResources() error { 239 var err error 240 for i := range r.Assets { 241 asset := r.Assets[i] 242 for j := range asset.Resources { 243 if err = r.reconnectResource(&asset, &asset.Resources[j]); err != nil { 244 return err 245 } 246 } 247 } 248 return nil 249 } 250 251 func (r *recording) reconnectResource(asset *assetRecording, resource *resourceRecording) error { 252 var err error 253 for k, v := range resource.Fields { 254 if v.Error != nil { 255 // in this case we have neither type information nor a value 256 resource.Fields[k].Error = v.Error 257 continue 258 } 259 260 typ := types.Type(v.Type) 261 resource.Fields[k].Value, err = tryReconnect(typ, v.Value, resource) 262 if err != nil { 263 return err 264 } 265 } 266 return nil 267 } 268 269 func tryReconnect(typ types.Type, v interface{}, resource *resourceRecording) (interface{}, error) { 270 var err error 271 272 if typ.IsArray() { 273 arr, ok := v.([]interface{}) 274 if !ok { 275 return nil, errors.New("failed to reconnect array type") 276 } 277 ct := typ.Child() 278 for i := range arr { 279 arr[i], err = tryReconnect(ct, arr[i], resource) 280 if err != nil { 281 return nil, err 282 } 283 } 284 return arr, nil 285 } 286 287 if typ.IsMap() { 288 m, ok := v.(map[string]interface{}) 289 if !ok { 290 return nil, errors.New("failed to reconnect map type") 291 } 292 ct := typ.Child() 293 for i := range m { 294 m[i], err = tryReconnect(ct, m[i], resource) 295 if err != nil { 296 return nil, err 297 } 298 } 299 return m, nil 300 } 301 302 if !typ.IsResource() || v == nil { 303 return v, nil 304 } 305 306 return reconnectResource(v, resource) 307 } 308 309 func reconnectResource(v interface{}, resource *resourceRecording) (interface{}, error) { 310 vals, ok := v.(map[string]interface{}) 311 if !ok { 312 return nil, errors.New("error in recording: resource '" + resource.Resource + "' (ID:" + resource.ID + ") has incorrect reference") 313 } 314 name, ok := vals["Name"].(string) 315 if !ok { 316 return nil, errors.New("error in recording: resource '" + resource.Resource + "' (ID:" + resource.ID + ") has incorrect type in Name field") 317 } 318 id, ok := vals["ID"].(string) 319 if !ok { 320 return nil, errors.New("error in recording: resource '" + resource.Resource + "' (ID:" + resource.ID + ") has incorrect type in ID field") 321 } 322 323 // TODO: Not sure yet if we need to check the recording for the reference. 324 // Unless it is used by the code, we may get away with it. 325 // if _, ok = asset.resources[name+"\x00"+id]; !ok { 326 // return errors.New("cannot find resource '" + resource.Resource + "' (ID:" + resource.ID + ") in recording") 327 // } 328 329 return &llx.MockResource{Name: name, ID: id}, nil 330 } 331 332 func (r *recording) finalize() { 333 for i := range r.Assets { 334 asset := &r.Assets[i] 335 asset.Resources = make([]resourceRecording, len(asset.resources)) 336 asset.Connections = make([]connectionRecording, len(asset.connections)) 337 338 i := 0 339 for _, v := range asset.resources { 340 asset.Resources[i] = *v 341 i++ 342 } 343 344 sort.Slice(asset.Resources, func(i, j int) bool { 345 a := asset.Resources[i] 346 b := asset.Resources[j] 347 if a.Resource == b.Resource { 348 return a.ID < b.ID 349 } 350 return a.Resource < b.Resource 351 }) 352 353 i = 0 354 for _, v := range asset.connections { 355 asset.Connections[i] = *v 356 i++ 357 } 358 } 359 } 360 361 func (r *recording) findAssetConnID(asset *inventory.Asset, conf *inventory.Config) (int, string) { 362 var id string 363 if asset.Mrn != "" { 364 id = asset.Mrn 365 } else if asset.Id != "" { 366 id = asset.Id 367 } 368 369 found := -1 370 371 if id != "" { 372 for i := range r.Assets { 373 if r.Assets[i].Asset.ID == id { 374 found = i 375 break 376 } 377 } 378 if found != -1 { 379 return found, id 380 } 381 } 382 383 if asset.Platform != nil { 384 for i := range r.Assets { 385 if r.Assets[i].Asset.Title == asset.Platform.Title { 386 found = i 387 break 388 } 389 } 390 if found != -1 { 391 return found, r.Assets[found].Asset.ID 392 } 393 } 394 395 return found, id 396 } 397 398 func (r *recording) EnsureAsset(asset *inventory.Asset, providerID string, connectionID uint32, conf *inventory.Config) { 399 found, _ := r.findAssetConnID(asset, conf) 400 401 if found == -1 { 402 id := asset.Mrn 403 if id == "" { 404 id = asset.Id 405 } 406 if id == "" { 407 id = asset.Platform.Title 408 } 409 r.Assets = append(r.Assets, assetRecording{ 410 Asset: assetInfo{ 411 ID: id, 412 PlatformIDs: asset.PlatformIds, 413 Name: asset.Platform.Name, 414 Arch: asset.Platform.Arch, 415 Title: asset.Platform.Title, 416 Family: asset.Platform.Family, 417 Build: asset.Platform.Build, 418 Version: asset.Platform.Version, 419 Kind: asset.Platform.Kind, 420 Runtime: asset.Platform.Runtime, 421 Labels: asset.Platform.Labels, 422 }, 423 connections: map[string]*connectionRecording{}, 424 resources: map[string]*resourceRecording{}, 425 }) 426 found = len(r.Assets) - 1 427 } 428 429 assetObj := &r.Assets[found] 430 431 url := conf.ToUrl() 432 assetObj.connections[url] = &connectionRecording{ 433 Url: url, 434 ProviderID: providerID, 435 Connector: conf.Type, 436 id: conf.Id, 437 } 438 r.assets[connectionID] = assetObj 439 } 440 441 func (r *recording) AddData(connectionID uint32, resource string, id string, field string, data *llx.RawData) { 442 asset, ok := r.assets[connectionID] 443 if !ok { 444 log.Error().Uint32("connectionID", connectionID).Msg("cannot store recording, cannot find connection ID") 445 return 446 } 447 448 obj, exist := asset.resources[resource+"\x00"+id] 449 if !exist { 450 obj = &resourceRecording{ 451 Resource: resource, 452 ID: id, 453 Fields: map[string]*llx.RawData{}, 454 } 455 asset.resources[resource+"\x00"+id] = obj 456 } 457 458 if field != "" { 459 obj.Fields[field] = data 460 } 461 } 462 463 func (r *recording) GetData(connectionID uint32, resource string, id string, field string) (*llx.RawData, bool) { 464 asset, ok := r.assets[connectionID] 465 if !ok { 466 return nil, false 467 } 468 469 obj, exist := asset.resources[resource+"\x00"+id] 470 if !exist { 471 return nil, false 472 } 473 474 if field == "" { 475 return &llx.RawData{Type: types.Resource(resource), Value: id}, true 476 } 477 478 data, ok := obj.Fields[field] 479 if !ok && field == "id" { 480 return llx.StringData(id), true 481 } 482 483 return data, ok 484 } 485 486 func (r *recording) GetResource(connectionID uint32, resource string, id string) (map[string]*llx.RawData, bool) { 487 asset, ok := r.assets[connectionID] 488 if !ok { 489 return nil, false 490 } 491 492 obj, exist := asset.resources[resource+"\x00"+id] 493 if !exist { 494 return nil, false 495 } 496 497 return obj.Fields, true 498 } 499 500 func (a assetInfo) ToInventory() *inventory.Asset { 501 return &inventory.Asset{ 502 Id: a.ID, 503 PlatformIds: a.PlatformIDs, 504 Platform: &inventory.Platform{ 505 Name: a.Name, 506 Arch: a.Arch, 507 Title: a.Title, 508 Family: a.Family, 509 Build: a.Build, 510 Version: a.Version, 511 Kind: a.Kind, 512 Runtime: a.Runtime, 513 Labels: a.Labels, 514 }, 515 } 516 } 517 518 func RawDataArgsToResultArgs(args map[string]*llx.RawData) (map[string]*llx.Result, error) { 519 all := make(map[string]*llx.Result, len(args)) 520 var err multierr.Errors 521 for k, v := range args { 522 res := v.Result() 523 if res.Error != "" { 524 err.Add(errors.New("failed to convert '" + k + "': " + res.Error)) 525 } else { 526 all[k] = res 527 } 528 } 529 530 return all, err.Deduplicate() 531 } 532 533 func PrimitiveArgsToResultArgs(args map[string]*llx.Primitive) map[string]*llx.Result { 534 res := make(map[string]*llx.Result, len(args)) 535 for k, v := range args { 536 res[k] = &llx.Result{Data: v} 537 } 538 return res 539 }