go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers-sdk/v1/inventory/inventory.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package inventory
     5  
     6  import (
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/cockroachdb/errors"
    12  	"github.com/rs/zerolog/log"
    13  	"github.com/segmentio/ksuid"
    14  	"go.mondoo.com/cnquery/providers-sdk/v1/vault"
    15  	"google.golang.org/protobuf/proto"
    16  	"sigs.k8s.io/yaml"
    17  )
    18  
    19  //go:generate protoc --proto_path=../../../:. --go_out=. --go_opt=paths=source_relative --rangerrpc_out=. inventory.proto
    20  
    21  const (
    22  	InventoryFilePath = "mondoo.app/source-file"
    23  )
    24  
    25  var ErrProviderTypeDoesNotMatch = errors.New("provider type does not match")
    26  
    27  type Option func(*Inventory)
    28  
    29  // passes a list of asset into the Inventory Manager
    30  func WithAssets(assetList ...*Asset) Option {
    31  	return func(inventory *Inventory) {
    32  		inventory.AddAssets(assetList...)
    33  	}
    34  }
    35  
    36  func New(opts ...Option) *Inventory {
    37  	inventory := &Inventory{
    38  		Metadata: &ObjectMeta{},
    39  		Spec:     &InventorySpec{},
    40  	}
    41  
    42  	for _, option := range opts {
    43  		option(inventory)
    44  	}
    45  
    46  	return inventory
    47  }
    48  
    49  // InventoryFromYAML create an inventory from yaml contents
    50  func InventoryFromYAML(data []byte) (*Inventory, error) {
    51  	res := New()
    52  	err := yaml.Unmarshal(data, res)
    53  
    54  	// FIXME: DEPRECATED, remove in v10.0 (or later) vv
    55  	// This is only used to migrate the old "backend" field.
    56  	if err == nil && res.Spec != nil {
    57  		for _, asset := range res.Spec.Assets {
    58  			for _, conn := range asset.Connections {
    59  				if conn.Type == "" {
    60  					log.Warn().Msg("no connection `type` provided in inventory, falling back to deprecated `backend` field")
    61  					conn.Type = ConnBackendToType(conn.Backend)
    62  				}
    63  			}
    64  		}
    65  	}
    66  	// ^^
    67  
    68  	return res, err
    69  }
    70  
    71  // InventoryFromFile loads an inventory from file system
    72  func InventoryFromFile(path string) (*Inventory, error) {
    73  	absPath, err := filepath.Abs(path)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	inventoryData, err := os.ReadFile(absPath)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	inventory, err := InventoryFromYAML(inventoryData)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	inventory.ensureRequireMetadataStructs()
    89  	inventory.Metadata.Labels[InventoryFilePath] = absPath
    90  
    91  	return inventory, nil
    92  }
    93  
    94  func (p *Inventory) ensureRequireMetadataStructs() {
    95  	if p.Metadata == nil {
    96  		p.Metadata = &ObjectMeta{}
    97  	}
    98  
    99  	if p.Metadata.Labels == nil {
   100  		p.Metadata.Labels = map[string]string{}
   101  	}
   102  }
   103  
   104  // ToYAML returns the inventory as yaml
   105  func (p *Inventory) ToYAML() ([]byte, error) {
   106  	return yaml.Marshal(p)
   107  }
   108  
   109  // PreProcess extracts all the embedded credentials from the assets and migrates those to in the
   110  // dedicated credentials section. The pre-processed content is optimized for runtime access.
   111  // Re-generating yaml, results into a different yaml output. While the results are identical,
   112  // the yaml file is not.
   113  func (p *Inventory) PreProcess() error {
   114  	if p.Spec == nil {
   115  		p.Spec = &InventorySpec{}
   116  	}
   117  
   118  	if p.Spec.Credentials == nil {
   119  		p.Spec.Credentials = map[string]*vault.Credential{}
   120  	}
   121  
   122  	// we are going to use the labels in metadata, ensure the structs are in place
   123  	p.ensureRequireMetadataStructs()
   124  
   125  	// extract embedded credentials from assets into dedicated section
   126  	for i := range p.Spec.Assets {
   127  		asset := p.Spec.Assets[i]
   128  
   129  		for j := range asset.Connections {
   130  			c := asset.Connections[j]
   131  			for k := range c.Credentials {
   132  				cred := c.Credentials[k]
   133  				if cred != nil && cred.SecretId != "" {
   134  					// clean credentials
   135  					// if a secret id with content is provided, we discard the content and always prefer the secret id
   136  					cleanSecrets(cred)
   137  				} else {
   138  					// create secret id and add id to the credential
   139  					secretId := ksuid.New().String()
   140  					cred.SecretId = secretId
   141  					// add a cloned credential to the map
   142  					copy := cloneCred(cred)
   143  					p.Spec.Credentials[secretId] = copy
   144  
   145  					// replace current credential the secret id, essentially we just remove all the content
   146  					cleanCred(cred)
   147  				}
   148  			}
   149  		}
   150  	}
   151  
   152  	// iterate over all credentials and load private keys references
   153  	for k := range p.Spec.Credentials {
   154  		cred := p.Spec.Credentials[k]
   155  
   156  		// ensure the secret id is correct
   157  		cred.SecretId = k
   158  		cred.PreProcess()
   159  
   160  		// TODO: we may want to load it but we probably need
   161  		// a local file watcher to detect changes
   162  		if cred.PrivateKeyPath != "" {
   163  			path := cred.PrivateKeyPath
   164  
   165  			// special handling for relative filenames, instead of loading
   166  			// private keys from relative to the work directory, we want to
   167  			// load the files relative to the source inventory
   168  			if !filepath.IsAbs(cred.PrivateKeyPath) {
   169  				// we handle credentials relative to the inventory file
   170  				fileLoc, ok := p.Metadata.Labels[InventoryFilePath]
   171  				if ok {
   172  					path = filepath.Join(filepath.Dir(fileLoc), path)
   173  				} else {
   174  					absPath, err := filepath.Abs(path)
   175  					if err != nil {
   176  						return err
   177  					}
   178  					path = absPath
   179  				}
   180  			}
   181  
   182  			data, err := os.ReadFile(path)
   183  			if err != nil {
   184  				return errors.New("cannot read credential: " + path)
   185  			}
   186  			cred.Secret = data
   187  
   188  			// only set the credential type if it is not set, pkcs12 also uses the private key path
   189  			if cred.Type == vault.CredentialType_undefined {
   190  				cred.Type = vault.CredentialType_private_key
   191  			}
   192  		}
   193  	}
   194  	return nil
   195  }
   196  
   197  func (p *Inventory) MarkConnectionsInsecure() {
   198  	for i := range p.Spec.Assets {
   199  		asset := p.Spec.Assets[i]
   200  		for j := range asset.Connections {
   201  			asset.Connections[j].Insecure = true
   202  		}
   203  	}
   204  }
   205  
   206  func cleanCred(c *vault.Credential) {
   207  	c.User = ""
   208  	c.Type = vault.CredentialType_undefined
   209  	cleanSecrets(c)
   210  }
   211  
   212  func cleanSecrets(c *vault.Credential) {
   213  	c.Secret = []byte{}
   214  	c.PrivateKey = ""
   215  	c.PrivateKeyPath = ""
   216  	c.Password = ""
   217  }
   218  
   219  func cloneCred(c *vault.Credential) *vault.Credential {
   220  	m := proto.Clone(c)
   221  	return m.(*vault.Credential)
   222  }
   223  
   224  // Validate ensures consistency within the inventory.
   225  // The implementation expects that PreProcess was executed before.
   226  // - it checks that all secret ids are either part of the credential map or a vault is defined
   227  // - it checks that all credentials have a secret id
   228  func (p *Inventory) Validate() error {
   229  	var err error
   230  	for i := range p.Spec.Assets {
   231  		a := p.Spec.Assets[i]
   232  		for j := range a.Connections {
   233  			conn := a.Connections[j]
   234  			for k := range conn.Credentials {
   235  				cred := conn.Credentials[k]
   236  				err = isValidCredentialRef(cred)
   237  				if err != nil {
   238  					return err
   239  				}
   240  			}
   241  		}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func (p *Inventory) AddAssets(assetList ...*Asset) {
   248  	if p.Spec == nil {
   249  		p.Spec = &InventorySpec{}
   250  	}
   251  	for i := range assetList {
   252  		p.Spec.Assets = append(p.Spec.Assets, assetList[i])
   253  	}
   254  }
   255  
   256  func (p *Inventory) ApplyLabels(labels map[string]string) {
   257  	for i := range p.Spec.Assets {
   258  		a := p.Spec.Assets[i]
   259  
   260  		if a.Labels == nil {
   261  			a.Labels = map[string]string{}
   262  		}
   263  
   264  		for k := range labels {
   265  			a.Labels[k] = labels[k]
   266  		}
   267  	}
   268  }
   269  
   270  func (p *Inventory) ApplyCategory(category AssetCategory) {
   271  	for i := range p.Spec.Assets {
   272  		a := p.Spec.Assets[i]
   273  		a.Category = category
   274  	}
   275  }
   276  
   277  // isValidCredentialRef ensures an asset credential is defined properly
   278  // The implementation assumes the credentials have been offloaded to the
   279  // credential map before via PreProcess
   280  func isValidCredentialRef(cred *vault.Credential) error {
   281  	if cred.SecretId == "" {
   282  		return errors.New("credential is missing the secret_id")
   283  	}
   284  
   285  	// credential references have no type defined
   286  	if cred.Type != vault.CredentialType_undefined {
   287  		return errors.New("credential reference has a wrong type defined")
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  // often used family names
   294  var (
   295  	FAMILY_UNIX    = "unix"
   296  	FAMILY_DARWIN  = "darwin"
   297  	FAMILY_LINUX   = "linux"
   298  	FAMILY_BSD     = "bsd"
   299  	FAMILY_WINDOWS = "windows"
   300  )
   301  
   302  func (p *Platform) IsFamily(family string) bool {
   303  	for i := range p.Family {
   304  		if p.Family[i] == family {
   305  			return true
   306  		}
   307  	}
   308  	return false
   309  }
   310  
   311  func (p *Platform) PrettyTitle() string {
   312  	prettyTitle := p.Title
   313  
   314  	// extend the title only for OS and k8s objects
   315  	if !(p.IsFamily("k8s-workload") || p.IsFamily("os")) {
   316  		return prettyTitle
   317  	}
   318  
   319  	var runtimeNiceName string
   320  	runtimeName := p.Runtime
   321  	if runtimeName != "" {
   322  		switch runtimeName {
   323  		case "aws-ec2-instance":
   324  			runtimeNiceName = "AWS EC2 Instance"
   325  		case "azure-vm":
   326  			runtimeNiceName = "Azure Virtual Machine"
   327  		case "docker-container":
   328  			runtimeNiceName = "Docker Container"
   329  		case "docker-image":
   330  			runtimeNiceName = "Docker Image"
   331  		case "gcp-vm":
   332  			runtimeNiceName = "GCP Virtual Machine"
   333  		case "k8s-cluster":
   334  			runtimeNiceName = "Kubernetes Cluster"
   335  		case "k8s-manifest":
   336  			runtimeNiceName = "Kubernetes Manifest File"
   337  		case "vsphere-host":
   338  			runtimeNiceName = "vSphere Host"
   339  		case "vsphere-vm":
   340  			runtimeNiceName = "vSphere Virtual Machine"
   341  		}
   342  	} else {
   343  		runtimeKind := p.Kind
   344  		switch runtimeKind {
   345  		case "baremetal":
   346  			runtimeNiceName = "bare metal"
   347  		case "container":
   348  			runtimeNiceName = "Container"
   349  		case "container-image":
   350  			runtimeNiceName = "Container Image"
   351  		case "virtualmachine":
   352  			runtimeNiceName = "Virtual Machine"
   353  		case "virtualmachine-image":
   354  			runtimeNiceName = "Virtual Machine Image"
   355  		}
   356  	}
   357  	// e.g. ", Kubernetes Cluster" and also "Kubernetes, Kubernetes Cluster" do not look nice, so prevent them
   358  	if prettyTitle == "" || strings.Contains(runtimeNiceName, prettyTitle) {
   359  		return runtimeNiceName
   360  	}
   361  
   362  	// do not add runtime name when the title is already obvious, e.g. "Network API, Network"
   363  	if !strings.Contains(prettyTitle, runtimeNiceName) {
   364  		prettyTitle += ", " + runtimeNiceName
   365  	}
   366  
   367  	return prettyTitle
   368  }
   369  
   370  type cloneSettings struct {
   371  	noDiscovery bool
   372  }
   373  
   374  type CloneOption interface {
   375  	Apply(*cloneSettings)
   376  }
   377  
   378  // WithoutDiscovery removes the discovery flags in the opts to ensure the same discovery does not run again
   379  func WithoutDiscovery() CloneOption {
   380  	return withoutDiscovery{}
   381  }
   382  
   383  type withoutDiscovery struct{}
   384  
   385  func (w withoutDiscovery) Apply(o *cloneSettings) { o.noDiscovery = true }
   386  
   387  func (cfg *Config) Clone(opts ...CloneOption) *Config {
   388  	if cfg == nil {
   389  		return nil
   390  	}
   391  
   392  	cloneSettings := &cloneSettings{}
   393  	for _, option := range opts {
   394  		option.Apply(cloneSettings)
   395  	}
   396  
   397  	clonedObject := proto.Clone(cfg).(*Config)
   398  
   399  	if cloneSettings.noDiscovery {
   400  		clonedObject.Discover = &Discovery{}
   401  	}
   402  
   403  	return clonedObject
   404  }
   405  
   406  func (c *Config) ToUrl() string {
   407  	schema := c.Type
   408  	if _, ok := c.Options["tls"]; ok {
   409  		schema = "tls"
   410  	}
   411  
   412  	host := c.Host
   413  	if strings.HasPrefix(host, "sha256:") {
   414  		host = strings.Replace(host, "sha256:", "", -1)
   415  	}
   416  
   417  	path := c.Path
   418  	if path != "" {
   419  		if path[0] != '/' {
   420  			path = "/" + path
   421  		}
   422  	}
   423  
   424  	return schema + "://" + host + path
   425  }