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  }