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  }