go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/mock/mock.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package mock
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"errors"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/BurntSushi/toml"
    19  	"github.com/gobwas/glob"
    20  	"github.com/rs/zerolog/log"
    21  	"github.com/spf13/afero"
    22  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    23  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    24  )
    25  
    26  // Data holds the mocked data entries
    27  type TomlData struct {
    28  	Commands map[string]*Command      `toml:"commands"`
    29  	Files    map[string]*MockFileData `toml:"files"`
    30  }
    31  
    32  type Command struct {
    33  	PlatformID string `toml:"platform_id"`
    34  	Command    string `toml:"command"`
    35  	Stdout     string `toml:"stdout"`
    36  	Stderr     string `toml:"stderr"`
    37  	ExitStatus int    `toml:"exit_status"`
    38  }
    39  
    40  type MockProviderInfo struct {
    41  	ID      string `toml:"id"`
    42  	Runtime string `toml:"runtime"`
    43  }
    44  
    45  type FileInfo struct {
    46  	Mode    os.FileMode `toml:"mode"`
    47  	ModTime time.Time   `toml:"time"`
    48  	IsDir   bool        `toml:"isdir"`
    49  	Uid     int64       `toml:"uid"`
    50  	Gid     int64       `toml:"gid"`
    51  	Size    int64       `toml:"size"`
    52  }
    53  
    54  type MockFileData struct {
    55  	Path string `toml:"path"`
    56  
    57  	StatData FileInfo `toml:"stat"`
    58  	Enoent   bool     `toml:"enoent"`
    59  	// Holds the file content
    60  	Data []byte `toml:"data"`
    61  	// Plain String response (simpler user usage, will not be used for automated recording)
    62  	Content string `toml:"content"`
    63  }
    64  
    65  type Connection struct {
    66  	data    *TomlData
    67  	asset   *inventory.Asset
    68  	mutex   sync.Mutex
    69  	uid     uint32
    70  	missing map[string]map[string]bool
    71  }
    72  
    73  func New(path string, asset *inventory.Asset) (*Connection, error) {
    74  	res := &Connection{
    75  		data:  &TomlData{},
    76  		asset: asset,
    77  		missing: map[string]map[string]bool{
    78  			"file":    {},
    79  			"command": {},
    80  		},
    81  	}
    82  
    83  	if path == "" {
    84  		res.data.Commands = map[string]*Command{}
    85  		res.data.Files = map[string]*MockFileData{}
    86  		return res, nil
    87  	}
    88  
    89  	data, err := os.ReadFile(path)
    90  	if err != nil {
    91  		return nil, errors.New("could not open: " + path)
    92  	}
    93  
    94  	if _, err := toml.Decode(string(data), &res.data); err != nil {
    95  		return nil, errors.New("could not decode toml: " + err.Error())
    96  	}
    97  
    98  	// just for sanitization, make sure the path is set correctly
    99  	for path, f := range res.data.Files {
   100  		f.Path = path
   101  	}
   102  
   103  	log.Debug().Int("commands", len(res.data.Commands)).Int("files", len(res.data.Files)).Msg("mock> loaded data successfully")
   104  
   105  	for k := range res.data.Commands {
   106  		log.Trace().Str("cmd", k).Msg("load command")
   107  	}
   108  
   109  	for k := range res.data.Files {
   110  		log.Trace().Str("file", k).Msg("load file")
   111  	}
   112  
   113  	return res, nil
   114  }
   115  
   116  func (c *Connection) ID() uint32 {
   117  	return c.uid
   118  }
   119  
   120  func (c *Connection) Type() shared.ConnectionType {
   121  	return "mock"
   122  }
   123  
   124  func (c *Connection) Asset() *inventory.Asset {
   125  	return c.asset
   126  }
   127  
   128  func (c *Connection) Capabilities() shared.Capabilities {
   129  	return shared.Capability_File | shared.Capability_RunCommand
   130  }
   131  
   132  func hashCmd(message string) string {
   133  	hash := sha256.New()
   134  	hash.Write([]byte(message))
   135  	return hex.EncodeToString(hash.Sum(nil))
   136  }
   137  
   138  func (c *Connection) RunCommand(command string) (*shared.Command, error) {
   139  	found, ok := c.data.Commands[command]
   140  	if !ok {
   141  		// try to fetch command by hash (more reliable for whitespace)
   142  		hash := hashCmd(command)
   143  		found, ok = c.data.Commands[hash]
   144  	}
   145  	if !ok {
   146  		c.missing["command"][command] = true
   147  		return &shared.Command{
   148  			Command:    command,
   149  			Stdout:     bytes.NewBuffer([]byte{}),
   150  			Stderr:     bytes.NewBufferString("command not found: " + command),
   151  			ExitStatus: 1,
   152  		}, nil
   153  	}
   154  
   155  	return &shared.Command{
   156  		Command:    command,
   157  		Stdout:     bytes.NewBufferString(found.Stdout),
   158  		Stderr:     bytes.NewBufferString(found.Stderr),
   159  		ExitStatus: found.ExitStatus,
   160  	}, nil
   161  }
   162  
   163  func (c *Connection) FileInfo(path string) (shared.FileInfoDetails, error) {
   164  	found, ok := c.data.Files[path]
   165  	if !ok {
   166  		return shared.FileInfoDetails{}, errors.New("file not found: " + path)
   167  	}
   168  
   169  	stat := found.StatData
   170  	return shared.FileInfoDetails{
   171  		Size: stat.Size,
   172  		Mode: shared.FileModeDetails{
   173  			FileMode: stat.Mode,
   174  		},
   175  		Uid: stat.Uid,
   176  		Gid: stat.Gid,
   177  	}, nil
   178  }
   179  
   180  func (c *Connection) FileSystem() afero.Fs {
   181  	return c
   182  }
   183  
   184  func (c *Connection) Name() string {
   185  	return "mockfs"
   186  }
   187  
   188  func (c *Connection) Create(name string) (afero.File, error) {
   189  	return nil, errors.New("not implemented")
   190  }
   191  
   192  func (c *Connection) Mkdir(name string, perm os.FileMode) error {
   193  	return errors.New("not implemented")
   194  }
   195  
   196  func (c *Connection) MkdirAll(path string, perm os.FileMode) error {
   197  	return errors.New("not implemented")
   198  }
   199  
   200  func (c *Connection) Open(name string) (afero.File, error) {
   201  	c.mutex.Lock()
   202  	defer c.mutex.Unlock()
   203  
   204  	data, ok := c.data.Files[name]
   205  	if !ok || data.Enoent {
   206  		return nil, os.ErrNotExist
   207  	}
   208  
   209  	return &MockFile{
   210  		data: data,
   211  		fs:   c,
   212  	}, nil
   213  }
   214  
   215  func (c *Connection) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
   216  	return nil, errors.New("not implemented")
   217  }
   218  
   219  func (c *Connection) Remove(name string) error {
   220  	c.mutex.Lock()
   221  	defer c.mutex.Unlock()
   222  	delete(c.data.Files, name)
   223  	return nil
   224  }
   225  
   226  func (c *Connection) RemoveAll(path string) error {
   227  	return errors.New("not implemented")
   228  }
   229  
   230  func (c *Connection) Rename(oldname, newname string) error {
   231  	c.mutex.Lock()
   232  	defer c.mutex.Unlock()
   233  	if oldname == newname {
   234  		return nil
   235  	}
   236  
   237  	f, ok := c.data.Files[oldname]
   238  	if !ok {
   239  		return os.ErrNotExist
   240  	}
   241  
   242  	c.data.Files[newname] = f
   243  	return nil
   244  }
   245  
   246  func (c *Connection) Stat(name string) (os.FileInfo, error) {
   247  	c.mutex.Lock()
   248  	defer c.mutex.Unlock()
   249  	data, ok := c.data.Files[name]
   250  	if !ok {
   251  		return nil, os.ErrNotExist
   252  	}
   253  
   254  	f := &MockFile{
   255  		data: data,
   256  		fs:   c,
   257  	}
   258  
   259  	return f.Stat()
   260  }
   261  
   262  func (c *Connection) Lstat(name string) (os.FileInfo, error) {
   263  	return c.Stat(name)
   264  }
   265  
   266  func (c *Connection) Chmod(name string, mode os.FileMode) error {
   267  	return errors.New("not implemented")
   268  }
   269  
   270  func (c *Connection) Chtimes(name string, atime time.Time, mtime time.Time) error {
   271  	return errors.New("not implemented")
   272  }
   273  
   274  func (c *Connection) Glob(pattern string) ([]string, error) {
   275  	matches := []string{}
   276  
   277  	g, err := glob.Compile(pattern)
   278  	if err != nil {
   279  		return matches, err
   280  	}
   281  
   282  	for k := range c.data.Files {
   283  		if g.Match(k) {
   284  			matches = append(matches, k)
   285  		}
   286  	}
   287  
   288  	return matches, nil
   289  }
   290  
   291  func (c *Connection) Chown(name string, uid, gid int) error {
   292  	return errors.New("not implemented")
   293  }
   294  
   295  type ReadAtSeeker interface {
   296  	io.Reader
   297  	io.Seeker
   298  	io.ReaderAt
   299  }
   300  
   301  type MockFile struct {
   302  	data       *MockFileData
   303  	dataReader ReadAtSeeker
   304  	fs         *Connection
   305  }
   306  
   307  func (mf *MockFile) Name() string {
   308  	return mf.data.Path
   309  }
   310  
   311  func (mf *MockFile) Stat() (os.FileInfo, error) {
   312  	if mf.data.Enoent {
   313  		return nil, os.ErrNotExist
   314  	}
   315  
   316  	// fallback in case the size information is missing, eg. older mock files
   317  	var size int64
   318  	if mf.data.StatData.Size > 0 {
   319  		size = mf.data.StatData.Size
   320  	} else if mf.data.StatData.Size == 0 && len(mf.data.Data) > 0 {
   321  		size = int64(len(mf.data.Data))
   322  	} else if mf.data.StatData.Size == 0 && len(mf.data.Content) > 0 {
   323  		size = int64(len(mf.data.Content))
   324  	}
   325  
   326  	return &shared.FileInfo{
   327  		FName:    filepath.Base(mf.data.Path),
   328  		FSize:    size,
   329  		FModTime: mf.data.StatData.ModTime,
   330  		FMode:    mf.data.StatData.Mode,
   331  		FIsDir:   mf.data.StatData.IsDir,
   332  		Uid:      mf.data.StatData.Uid,
   333  		Gid:      mf.data.StatData.Gid,
   334  	}, nil
   335  }
   336  
   337  func (mf *MockFile) reader() ReadAtSeeker {
   338  	// if binary data was provided, we ignore the string data
   339  	if mf.dataReader == nil && len(mf.data.Data) > 0 {
   340  		mf.dataReader = bytes.NewReader(mf.data.Data)
   341  	} else if mf.dataReader == nil {
   342  		mf.dataReader = strings.NewReader(mf.data.Content)
   343  	}
   344  	return mf.dataReader
   345  }
   346  
   347  func (mf *MockFile) Read(p []byte) (n int, err error) {
   348  	return mf.reader().Read(p)
   349  }
   350  
   351  func (mf *MockFile) ReadAt(p []byte, off int64) (n int, err error) {
   352  	return mf.reader().ReadAt(p, off)
   353  }
   354  
   355  func (mf *MockFile) Seek(offset int64, whence int) (int64, error) {
   356  	return mf.reader().Seek(offset, whence)
   357  }
   358  
   359  func (mf *MockFile) Sync() error {
   360  	return nil
   361  }
   362  
   363  func (mf *MockFile) Truncate(size int64) error {
   364  	return errors.New("not implemented")
   365  }
   366  
   367  func (mf *MockFile) Write(p []byte) (n int, err error) {
   368  	return 0, errors.New("not implemented")
   369  }
   370  
   371  func (mf *MockFile) WriteAt(p []byte, off int64) (n int, err error) {
   372  	return 0, errors.New("not implemented")
   373  }
   374  
   375  func (mf *MockFile) WriteString(s string) (ret int, err error) {
   376  	return 0, errors.New("not implemented")
   377  }
   378  
   379  func (mf *MockFile) Exists() bool {
   380  	return !mf.data.Enoent
   381  }
   382  
   383  func (f *MockFile) Delete() error {
   384  	return errors.New("not implemented")
   385  }
   386  
   387  func (f *MockFile) Readdir(n int) ([]os.FileInfo, error) {
   388  	children := []os.FileInfo{}
   389  	path := f.data.Path
   390  	// searches for direct childs of this file
   391  	for k := range f.fs.data.Files {
   392  		if strings.HasPrefix(k, path) {
   393  			// check if it is only one layer down
   394  			filename := strings.TrimPrefix(k, path)
   395  
   396  			// path-separator is still included, remove it
   397  			filename = strings.TrimPrefix(filename, "/")
   398  			filename = strings.TrimPrefix(filename, "\\")
   399  
   400  			if filename == "" || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
   401  				continue
   402  			}
   403  
   404  			// fetch file stats
   405  			fsInfo, err := f.fs.Stat(k)
   406  			if err != nil {
   407  				return nil, errors.New("cannot find file in mock index: " + k)
   408  			}
   409  
   410  			children = append(children, fsInfo)
   411  		}
   412  		if n > 0 && len(children) > n {
   413  			return children, nil
   414  		}
   415  	}
   416  	return children, nil
   417  }
   418  
   419  func (f *MockFile) Readdirnames(n int) ([]string, error) {
   420  	children := []string{}
   421  	path := f.data.Path
   422  	// searches for direct childs of this file
   423  	for k := range f.fs.data.Files {
   424  		if strings.HasPrefix(k, path) {
   425  			// check if it is only one layer down
   426  			filename := strings.TrimPrefix(k, path)
   427  
   428  			// path-separator is still included, remove it
   429  			filename = strings.TrimPrefix(filename, "/")
   430  			filename = strings.TrimPrefix(filename, "\\")
   431  
   432  			if filename == "" || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
   433  				continue
   434  			}
   435  			children = append(children, filename)
   436  		}
   437  		if n > 0 && len(children) > n {
   438  			return children, nil
   439  		}
   440  	}
   441  	return children, nil
   442  }
   443  
   444  func (f *MockFile) Close() error {
   445  	// nothing to do
   446  	return nil
   447  }