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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package ansibleinventory
     5  
     6  import (
     7  	"errors"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/mitchellh/mapstructure"
    12  	"github.com/rs/zerolog/log"
    13  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    14  	"go.mondoo.com/cnquery/providers-sdk/v1/vault"
    15  	"sigs.k8s.io/yaml"
    16  )
    17  
    18  type Group struct {
    19  	Hosts []string
    20  }
    21  
    22  type Groups map[string]Group
    23  
    24  type Meta struct {
    25  	HostVars map[string]map[string]interface{}
    26  }
    27  
    28  type All struct {
    29  	Children []string
    30  }
    31  
    32  func Parse(data []byte) (*Inventory, error) {
    33  	inventory := Inventory{}
    34  	err := inventory.Decode(data)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	return &inventory, nil
    39  }
    40  
    41  type Inventory struct {
    42  	Meta Meta
    43  	All  All
    44  	Groups
    45  }
    46  
    47  func IsInventory(data []byte) bool {
    48  	// parse json to map[string]interface{}
    49  	var raw map[string]interface{}
    50  	err := yaml.Unmarshal(data, &raw)
    51  	if err != nil {
    52  		return false
    53  	}
    54  
    55  	// if the all key is there, its a ansible yaml
    56  	// NOTE: as this point we only support fully resolved ansible config
    57  	_, ok := raw["all"]
    58  	if ok {
    59  		return true
    60  	}
    61  	return false
    62  }
    63  
    64  func (i *Inventory) Decode(data []byte) error {
    65  	if i == nil {
    66  		return errors.New("object cannot be nil")
    67  	}
    68  
    69  	// parse json to map[string]interface{}
    70  	var raw map[string]interface{}
    71  	err := yaml.Unmarshal(data, &raw)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	var meta Meta
    77  	err = mapstructure.Decode(raw["_meta"], &meta)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	delete(raw, "_meta")
    82  	i.Meta = meta
    83  
    84  	var all All
    85  	err = mapstructure.Decode(raw["all"], &all)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	delete(raw, "all")
    90  	i.All = all
    91  
    92  	// assign all other entries to groups
    93  	var groups Groups
    94  	err = mapstructure.Decode(raw, &groups)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	i.Groups = groups
    99  
   100  	return nil
   101  }
   102  
   103  type Host struct {
   104  	Alias      string
   105  	Host       string // ansible_host
   106  	Port       string // ansible_port
   107  	User       string // ansible_user
   108  	Password   string // ansible_password
   109  	Identity   string // ansible_ssh_private_key_file
   110  	Become     bool   // ansible_become
   111  	Connection string // ansible_connection: ssh, local, docker
   112  	Groups     []string
   113  	Labels     []string
   114  }
   115  
   116  // https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
   117  func (inventory *Inventory) List(groups ...string) []*Host {
   118  	if inventory == nil {
   119  		return nil
   120  	}
   121  
   122  	list := inventory.All.Children
   123  	if len(groups) > 0 {
   124  		list = Filter(list, func(x string) bool {
   125  			for i := range groups {
   126  				if groups[i] == x {
   127  					return true
   128  				}
   129  			}
   130  			return false
   131  		})
   132  	}
   133  
   134  	hostMap := map[string]*Host{}
   135  	for i := range list {
   136  		groupname := list[i]
   137  		hosts := inventory.Groups[groupname].Hosts
   138  		for j := range hosts {
   139  			alias := hosts[j]
   140  
   141  			host := &Host{
   142  				Alias:      alias,
   143  				Host:       alias,
   144  				Connection: "ssh",
   145  			}
   146  
   147  			meta := inventory.Meta.HostVars[alias]
   148  
   149  			if d, ok := meta["ansible_host"]; ok {
   150  				host.Host = d.(string)
   151  			}
   152  
   153  			if f, ok := meta["ansible_port"]; ok {
   154  				s := strconv.FormatFloat(f.(float64), 'f', 0, 64)
   155  				host.Port = s
   156  			}
   157  
   158  			if d, ok := meta["ansible_user"]; ok {
   159  				host.User = d.(string)
   160  			}
   161  
   162  			if d, ok := meta["ansible_password"]; ok {
   163  				host.Password = d.(string)
   164  			}
   165  
   166  			if d, ok := meta["ansible_ssh_private_key_file"]; ok {
   167  				host.Identity = d.(string)
   168  			}
   169  
   170  			if d, ok := meta["ansible_connection"]; ok {
   171  				host.Connection = d.(string)
   172  			}
   173  
   174  			if d, ok := meta["tags"]; ok {
   175  				labels, ok := d.([]interface{})
   176  				if ok {
   177  					for i := range labels {
   178  						key, kok := labels[i].(string)
   179  						if kok {
   180  							host.Labels = append(host.Labels, key)
   181  						}
   182  					}
   183  				}
   184  			}
   185  
   186  			hostMap[alias] = host
   187  		}
   188  	}
   189  
   190  	res := []*Host{}
   191  
   192  	for k := range hostMap {
   193  		res = append(res, hostMap[k])
   194  	}
   195  
   196  	return res
   197  }
   198  
   199  func Filter(vs []string, f func(string) bool) []string {
   200  	vsf := make([]string, 0)
   201  	for _, v := range vs {
   202  		if f(v) {
   203  			vsf = append(vsf, v)
   204  		}
   205  	}
   206  	return vsf
   207  }
   208  
   209  func (i *Inventory) ToV1Inventory() *inventory.Inventory {
   210  	out := inventory.New()
   211  
   212  	// convert assets
   213  	hosts := i.List()
   214  	for i := range hosts {
   215  		host := hosts[i]
   216  
   217  		name := host.Host
   218  		if host.Alias != "" {
   219  			name = host.Alias
   220  		}
   221  
   222  		asset := &inventory.Asset{
   223  			Name:        name,
   224  			Connections: ansibleConnections(host),
   225  			Labels:      map[string]string{},
   226  		}
   227  
   228  		for l := range host.Labels {
   229  			key := host.Labels[l]
   230  			asset.Labels[key] = ""
   231  		}
   232  
   233  		out.Spec.Assets = append(out.Spec.Assets, asset)
   234  	}
   235  
   236  	// move credentials out into credentials section
   237  	out.PreProcess()
   238  
   239  	return out
   240  }
   241  
   242  var validConnectionTypes = []string{"ssh", "winrm", "local", "docker"}
   243  
   244  func isValidConnectionType(conn string) bool {
   245  	for i := range validConnectionTypes {
   246  		if conn == validConnectionTypes[i] {
   247  			return true
   248  		}
   249  	}
   250  	return false
   251  }
   252  
   253  // ansibleBackend maps an ansible connection to mondoo backend
   254  // https://docs.ansible.com/ansible/latest/plugins/connection.html
   255  // quickly get a list of available plugins via `ansible-doc -t connection -l`
   256  func ansibleBackend(connection string) string {
   257  	switch strings.TrimSpace(connection) {
   258  	case "local":
   259  		break
   260  	case "ssh":
   261  		break
   262  	case "winrm":
   263  		break
   264  	case "docker":
   265  		break
   266  	default:
   267  		log.Warn().Str("ansible-connection", connection).Msg("unknown connection, fallback to ssh")
   268  		return "ssh"
   269  	}
   270  	return connection
   271  }
   272  
   273  func ansibleConnections(host *Host) []*inventory.Config {
   274  	backend := ansibleBackend(host.Connection)
   275  
   276  	// in the case where the port is 0, we will fallback to default ports (eg 22)
   277  	// further down in the execution chain
   278  	port, _ := strconv.Atoi(host.Port)
   279  
   280  	res := &inventory.Config{
   281  		Type: backend,
   282  		Host: host.Host,
   283  		Port: int32(port),
   284  		Sudo: &inventory.Sudo{
   285  			Active: host.Become,
   286  		},
   287  	}
   288  
   289  	credentials := []*vault.Credential{}
   290  
   291  	if host.Password != "" {
   292  		credentials = append(credentials, &vault.Credential{
   293  			Type:     vault.CredentialType_password,
   294  			User:     host.User,
   295  			Password: host.Password,
   296  		})
   297  	}
   298  
   299  	if host.Identity != "" {
   300  		credentials = append(credentials, &vault.Credential{
   301  			Type:           vault.CredentialType_private_key,
   302  			User:           host.User,
   303  			PrivateKeyPath: host.Identity,
   304  		})
   305  	}
   306  
   307  	// fallback to ssh agent as default in case nothing was provided
   308  	if len(credentials) == 0 && backend == "ssh" {
   309  		credentials = append(credentials, &vault.Credential{
   310  			Type: vault.CredentialType_ssh_agent,
   311  			User: host.User,
   312  		})
   313  	}
   314  
   315  	res.Credentials = credentials
   316  	return []*inventory.Config{res}
   317  }