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 }