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 }