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 }