// SPDX-License-Identifier: GPL-3.0-or-later package fim import ( "crypto/sha256" "encoding/hex" "io" "io/fs" "os" "path/filepath" "syscall" ) // DefaultPaths mirrors Python's DEFAULT_FIM_PATHS: security-critical files the // package manager does not track, so package verification cannot cover them. var DefaultPaths = []string{ "/usr/local/bin", "/usr/local/sbin", "/usr/local/lib", "/etc/systemd/system", "/etc/ld.so.preload", "/etc/ld.so.conf", "/etc/ld.so.conf.d", "/etc/pam.d", "/etc/ssh/sshd_config", "/etc/sudoers", "/etc/sudoers.d", "/etc/profile", "/etc/profile.d", "/etc/bash.bashrc", "/etc/crontab", "/etc/cron.d", "/etc/cron.daily", "/etc/cron.hourly", "/etc/passwd", "/etc/shadow", "/etc/group", "/root/.ssh/authorized_keys", "/root/.bashrc", "/root/.profile", } // SelfPaths mirrors Python's SELF_PATHS. Sentinel's own footprint is always // monitored so tampering with the agent trips the agent. var SelfPaths = []string{ "/usr/bin/enodia-sentinel", "/usr/local/bin/enodia-sentinel", "/usr/bin/sentinel-redteam", "/usr/local/bin/sentinel-redteam", "/usr/lib/enodia-sentinel", "/usr/local/lib/enodia-sentinel", "/etc/enodia-sentinel.toml", "/etc/systemd/system/enodia-sentinel.service", "/etc/systemd/system/enodia-sentinel-web.service", "/usr/lib/systemd/system/enodia-sentinel.service", "/usr/lib/systemd/system/enodia-sentinel-web.service", "/etc/pacman.d/hooks/enodia-sentinel-fim.hook", "/usr/share/libalpm/hooks/enodia-sentinel-fim.hook", } const hashBufferSize = 1 << 16 // PathList returns the configured or default monitored paths plus Sentinel's // own footprint, matching Python's Config.fim_path_list. func PathList(configured []string) []string { base := configured if len(base) == 0 { base = DefaultPaths } paths := make([]string, 0, len(base)+len(SelfPaths)) paths = append(paths, base...) paths = append(paths, SelfPaths...) return paths } // HashFile streams a file's SHA-256. The second return is false when the file // cannot be read, which the caller records as an absent hash rather than an // alert: an unreadable file is not evidence of tampering by itself. func HashFile(path string) (string, bool) { file, err := os.Open(path) if err != nil { return "", false } defer file.Close() digest := sha256.New() if _, err := io.CopyBuffer(digest, file, make([]byte, hashBufferSize)); err != nil { return "", false } return hex.EncodeToString(digest.Sum(nil)), true } // entryFor builds one integrity record, or false for paths that carry no // meaningful integrity state (sockets, devices, fifos). func entryFor(path string) (Entry, bool) { info, err := os.Lstat(path) if err != nil { return Entry{}, false } entry := Entry{Size: info.Size()} if stat, ok := info.Sys().(*syscall.Stat_t); ok { // Permission bits alone would drop setuid/setgid/sticky, so take the // same 0o7777 mask Python's stat.S_IMODE applies. entry.Mode = int(stat.Mode & 0o7777) entry.UID = int(stat.Uid) entry.GID = int(stat.Gid) } switch { case info.Mode()&fs.ModeSymlink != 0: target, err := os.Readlink(path) if err != nil { target = "" } entry.Link = &target case info.Mode().IsRegular(): if hash, ok := HashFile(path); ok { entry.SHA256 = &hash } default: return Entry{}, false } return entry, true } // ScanPaths maps path -> entry for every regular file and symlink under the // given roots. Unreadable subtrees are skipped rather than failing the sweep. func ScanPaths(paths []string) map[string]Entry { entries := make(map[string]Entry) for _, root := range paths { info, err := os.Lstat(root) if err != nil { continue } // Python tests islink() before isdir(), so a symlink handed in // directly is recorded as a link even when it points at a directory. if !info.IsDir() { if entry, ok := entryFor(root); ok { entries[root] = entry } continue } walkRoot(root, entries) } return entries } func walkRoot(root string, entries map[string]Entry) { _ = filepath.WalkDir(root, func(path string, dirEntry fs.DirEntry, err error) error { if err != nil { // Matches os.walk(onerror=...) swallowing permission errors. return nil } if dirEntry.IsDir() { return nil } // os.walk classifies with is_dir(), which follows symlinks: a symlink // to a directory is listed as a directory and never scanned as a file. if dirEntry.Type()&fs.ModeSymlink != 0 { if target, err := os.Stat(path); err == nil && target.IsDir() { return nil } } if entry, ok := entryFor(path); ok { entries[path] = entry } return nil }) }