go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/user.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package resources 5 6 import ( 7 "errors" 8 "os" 9 "path" 10 "strconv" 11 "strings" 12 "sync" 13 14 "github.com/spf13/afero" 15 "go.mondoo.com/cnquery/llx" 16 "go.mondoo.com/cnquery/providers-sdk/v1/plugin" 17 "go.mondoo.com/cnquery/providers/os/connection/shared" 18 "go.mondoo.com/cnquery/providers/os/resources/users" 19 ) 20 21 func (x *mqlUser) id() (string, error) { 22 var id string 23 if len(x.Sid.Data) > 0 { 24 id = x.Sid.Data 25 } else { 26 id = strconv.FormatInt(x.Uid.Data, 10) 27 } 28 29 return "user/" + id + "/" + x.Name.Data, nil 30 } 31 32 func initUser(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { 33 if len(args) != 1 { 34 return args, nil, nil 35 } 36 37 // if user is only initialized with a name or uid, we will try to look it up 38 rawName, nameOk := args["name"] 39 rawUID, idOk := args["uid"] 40 if !nameOk && !idOk { 41 return args, nil, nil 42 } 43 44 raw, err := CreateResource(runtime, "users", nil) 45 if err != nil { 46 return nil, nil, errors.New("cannot get list of users: " + err.Error()) 47 } 48 users := raw.(*mqlUsers) 49 50 if err := users.refreshCache(nil); err != nil { 51 return nil, nil, err 52 } 53 54 if nameOk { 55 name, ok := rawName.Value.(string) 56 if !ok { 57 return nil, nil, errors.New("cannot detect user, invalid type for name (expected string)") 58 } 59 user, ok := users.usersByName[name] 60 if !ok { 61 return nil, nil, errors.New("cannot find user with name '" + name + "'") 62 } 63 return nil, user, nil 64 } 65 66 if idOk { 67 id, ok := rawUID.Value.(int64) 68 if !ok { 69 return nil, nil, errors.New("cannot detect user, invalid type for name (expected int)") 70 } 71 user, ok := users.usersByID[id] 72 if !ok { 73 return nil, nil, errors.New("cannot find user with UID '" + strconv.Itoa(int(id)) + "'") 74 } 75 return nil, user, nil 76 } 77 78 return nil, nil, errors.New("cannot find user, no search criteria provided") 79 } 80 81 func (x *mqlUser) group(gid int64) (*mqlGroup, error) { 82 raw, err := CreateResource(x.MqlRuntime, "groups", nil) 83 if err != nil { 84 return nil, errors.New("cannot get groups info for user: " + err.Error()) 85 } 86 groups := raw.(*mqlGroups) 87 return groups.findID(gid) 88 } 89 90 func (u *mqlUser) authorizedkeys(home string) (*mqlAuthorizedkeys, error) { 91 // TODO: we may need to handle ".ssh/authorized_keys2" too 92 authorizedKeysPath := path.Join(home, ".ssh", "authorized_keys") 93 ak, err := NewResource(u.MqlRuntime, "authorizedkeys", map[string]*llx.RawData{ 94 "path": llx.StringData(authorizedKeysPath), 95 }) 96 if err != nil { 97 return nil, err 98 } 99 return ak.(*mqlAuthorizedkeys), nil 100 } 101 102 type mqlUsersInternal struct { 103 lock sync.Mutex 104 usersByID map[int64]*mqlUser 105 usersByName map[string]*mqlUser 106 } 107 108 func (x *mqlUsers) list() ([]interface{}, error) { 109 x.lock.Lock() 110 defer x.lock.Unlock() 111 112 // in the unlikely case that we get called twice into the same method, 113 // any subsequent calls are locked until user detection finishes; at this point 114 // we only need to return a non-nil error field and it will pull the data from cache 115 if x.usersByID != nil { 116 return nil, nil 117 } 118 119 conn := x.MqlRuntime.Connection.(shared.Connection) 120 um, err := users.ResolveManager(conn) 121 if um == nil || err != nil { 122 return nil, errors.New("cannot find users manager") 123 } 124 125 users, err := um.List() 126 if err != nil { 127 return nil, errors.New("could not retrieve users list") 128 } 129 130 var res []interface{} 131 for i := range users { 132 user := users[i] 133 nu, err := CreateResource(x.MqlRuntime, "user", map[string]*llx.RawData{ 134 "name": llx.StringData(user.Name), 135 "uid": llx.IntData(user.Uid), 136 "gid": llx.IntData(user.Gid), 137 "sid": llx.StringData(user.Sid), 138 "home": llx.StringData(user.Home), 139 "shell": llx.StringData(user.Shell), 140 "enabled": llx.BoolData(user.Enabled), 141 }) 142 if err != nil { 143 return nil, err 144 } 145 146 res = append(res, nu) 147 } 148 149 return res, x.refreshCache(res) 150 } 151 152 func (x *mqlUsers) refreshCache(all []interface{}) error { 153 if all == nil { 154 raw := x.GetList() 155 if raw.Error != nil { 156 return raw.Error 157 } 158 all = raw.Data 159 } 160 161 x.usersByID = map[int64]*mqlUser{} 162 x.usersByName = map[string]*mqlUser{} 163 164 for i := range all { 165 u := all[i].(*mqlUser) 166 x.usersByID[u.Uid.Data] = u 167 x.usersByName[u.Name.Data] = u 168 } 169 170 return nil 171 } 172 173 func (x *mqlUsers) findID(id int64) (*mqlUser, error) { 174 if x := x.GetList(); x.Error != nil { 175 return nil, x.Error 176 } 177 178 res, ok := x.mqlUsersInternal.usersByID[id] 179 if !ok { 180 return nil, errors.New("cannot find user for uid " + strconv.Itoa(int(id))) 181 } 182 return res, nil 183 } 184 185 func (u *mqlUser) sshkeys() ([]interface{}, error) { 186 res := []interface{}{} 187 188 userSshPath := path.Join(u.Home.Data, ".ssh") 189 190 conn := u.MqlRuntime.Connection.(shared.Connection) 191 afutil := afero.Afero{Fs: conn.FileSystem()} 192 193 // check if use ssh directory exists 194 exists, err := afutil.Exists(userSshPath) 195 if err != nil { 196 return nil, err 197 } 198 if !exists { 199 return res, nil 200 } 201 202 filter := []string{"config"} 203 204 // walk dir and search for all private keys 205 potentialPrivateKeyFiles := []string{} 206 err = afutil.Walk(userSshPath, func(path string, f os.FileInfo, err error) error { 207 if f == nil || f.IsDir() { 208 return nil 209 } 210 211 // eg. matches google_compute_known_hosts and known_hosts 212 if strings.HasSuffix(f.Name(), ".pub") || strings.HasSuffix(f.Name(), "known_hosts") { 213 return nil 214 } 215 216 for i := range filter { 217 if f.Name() == filter[i] { 218 return nil 219 } 220 } 221 222 potentialPrivateKeyFiles = append(potentialPrivateKeyFiles, path) 223 return nil 224 }) 225 if err != nil { 226 return nil, err 227 } 228 229 // iterate over files and check if the content is there 230 for i := range potentialPrivateKeyFiles { 231 path := potentialPrivateKeyFiles[i] 232 data, err := os.ReadFile(path) 233 if err != nil { 234 return nil, err 235 } 236 237 content := string(data) 238 239 // check if content contains PRIVATE KEY 240 isPrivateKey := strings.Contains(content, "PRIVATE KEY") 241 // check if the key is encrypted ENCRYPTED 242 isEncrypted := strings.Contains(content, "ENCRYPTED") 243 244 if isPrivateKey { 245 upk, err := CreateResource(u.MqlRuntime, "privatekey", map[string]*llx.RawData{ 246 "pem": llx.StringData(content), 247 "encrypted": llx.BoolData(isEncrypted), 248 "path": llx.StringData(path), 249 }) 250 if err != nil { 251 return nil, err 252 } 253 res = append(res, upk.(*mqlPrivatekey)) 254 } 255 } 256 257 return res, nil 258 }