Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/checks/commands/check_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Naemon Config
| ---------------------------- | -------------------------------------------------------------------------------------- |
| calculate-subdirectory-sizes | For subdirectories that are found under the search paths, calculate the subdirectory sizes based on found files. This calculation may be expensive. Default: false |
| file | Alias for path |
| follow-symlinks | Follow symlinks of files and subdirectories while traversing. The file paths will be registered originating from search path. Default: true |
| max-depth | Maximum recursion depth. Default: no limit. '0' and '1' disable recursion and only include files/directories directly under path., '2' starts to include files/directories of subdirectories with given depth. |
| path | Path in which to search for files |
| paths | A comma separated list of paths |
Expand Down Expand Up @@ -98,3 +99,4 @@ these can be used in filters and thresholds (along with the default attributes):
| sha256_checksum | SHA256 checksum of the file |
| sha384_checksum | SHA384 checksum of the file |
| sha512_checksum | SHA512 checksum of the file |
| is_symlink | The file or its parent is a symlink |
120 changes: 115 additions & 5 deletions pkg/snclient/check_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ type FileInfoUnified struct {
Ctime time.Time // Create time
}

const (
CheckFilesDefaultFollowSymlinks = true
)

type CheckFiles struct {
paths []string
pathList CommaStringList
pattern string // constructor NewCheckFiles sets this as '*'
maxDepth int64 // constructor NewCheckFiles sets this as -1
calculateSubdirectorySizes bool // constructor NewCheckFiles sets this as false
followSymlinks bool // constructor NewCheckFiles sets this as true
}

func NewCheckFiles() CheckHandler {
Expand All @@ -43,6 +48,7 @@ func NewCheckFiles() CheckHandler {
pattern: "*",
maxDepth: int64(-1),
calculateSubdirectorySizes: false,
followSymlinks: CheckFilesDefaultFollowSymlinks,
}
}

Expand All @@ -62,6 +68,8 @@ func (l *CheckFiles) Build() *CheckData {
"max-depth": {value: &l.maxDepth, description: "Maximum recursion depth. Default: no limit. '0' and '1' disable recursion and only include files/directories directly under path." +
", '2' starts to include files/directories of subdirectories with given depth. "},
"timezone": {description: "Sets the timezone for time metrics (default is local time)"},
"follow-symlinks": {value: &l.followSymlinks, description: fmt.Sprintf("Follow symlinks of files and subdirectories while traversing. "+
"The file paths will be registered originating from search path. Default: %t", CheckFilesDefaultFollowSymlinks)},
"calculate-subdirectory-sizes": {value: &l.calculateSubdirectorySizes, description: "For subdirectories that are found under the search paths, " +
"calculate the subdirectory sizes based on found files. This calculation may be expensive. Default: false"},
},
Expand Down Expand Up @@ -93,6 +101,7 @@ func (l *CheckFiles) Build() *CheckData {
{name: "sha256_checksum", description: "SHA256 checksum of the file"},
{name: "sha384_checksum", description: "SHA384 checksum of the file"},
{name: "sha512_checksum", description: "SHA512 checksum of the file"},
{name: "is_symlink", description: "The file or its parent is a symlink", unit: UBool},
},
exampleDefault: `
Alert if there are logs older than 1 hour in /tmp:
Expand All @@ -119,10 +128,20 @@ func (l *CheckFiles) Check(_ context.Context, _ *Agent, check *CheckData, _ []Ar
checkPath = l.normalizePath(checkPath)
log.Tracef("normalized checkPath: %s", checkPath)

err := filepath.WalkDir(checkPath, func(path string, dirEntry fs.DirEntry, err error) error {
return l.addFile(check, path, checkPath, dirEntry, err)
})
if err != nil {
walker := &fileWalker{cf: l, check: check, checkPath: checkPath}

realRoot := checkPath
rootIsSymlink := false
if l.followSymlinks {
if lstat, lerr := os.Lstat(checkPath); lerr == nil && lstat.Mode()&fs.ModeSymlink != 0 {
rootIsSymlink = true
}
if resolved, evalErr := filepath.EvalSymlinks(checkPath); evalErr == nil {
realRoot = resolved
}
}

if err := walker.walk(realRoot, checkPath, rootIsSymlink); err != nil {
return nil, fmt.Errorf("error walking directory %s: %s", checkPath, err.Error())
}
}
Expand Down Expand Up @@ -153,7 +172,97 @@ func (l *CheckFiles) Check(_ context.Context, _ *Agent, check *CheckData, _ []Ar
return check.Finalize()
}

func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry fs.DirEntry, err error) error {
// fileWalker walks one check path while following symlinks.
// it registers new paths under the checkPath, even if the links take it outside of it
// the depth is calculated with the checkpath being the root
type fileWalker struct {
cf *CheckFiles
check *CheckData
checkPath string // original check path this walk started from
visited []string // real directories already walked , normally or via a symlink
}

// walk walks realRoot on disk but registers paths under displayRoot
// realRoot is the canonical path on the filesystem
// isSymlink marks whether realRoot was reached via a symlink.
//
//nolint:wrapcheck // filepath walker functions need to return an error, wrapping and appending a header to each call would return a large error message
func (w *fileWalker) walk(realRoot, displayRoot string, usedSymlink bool) error {
return filepath.WalkDir(realRoot, func(path string, dirEntry fs.DirEntry, err error) error {
// map the current root under display root
displayPath := path
if rel, relErr := filepath.Rel(realRoot, path); relErr == nil {
displayPath = filepath.Join(displayRoot, rel)
}

// at the real root, isSymlink is not determined through recursion.
// it is passed as usedSymlink before calling walk() for the first time
isSymlink := false
if path == realRoot {
isSymlink = usedSymlink
} else if dirEntry != nil && dirEntry.Type()&fs.ModeSymlink != 0 {
isSymlink = true
}

if err != nil {
return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, usedSymlink, isSymlink, err)
}

// filepath.WalkDir does not follow symlinks, handle it manually
if dirEntry.Type()&fs.ModeSymlink != 0 {
if w.cf.followSymlinks {
return w.followSymlink(path, displayPath)
}

// if we do not follow symlinks, record the symlink as a plain entry, but do not proceed
return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, usedSymlink, isSymlink, nil)
}

// remember every real directory we walk into.
// a symlink that later resolves back into already walked territory is recoded as well
// this prevents loops and duplicated subtrees.
if w.cf.followSymlinks && dirEntry.IsDir() {
w.visited = append(w.visited, path)
}

return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, usedSymlink, isSymlink, err)
})
}

// followSymlink resolves the symlink at linkPath
// if its a file, add it as a file entry
// if its a directory, descend into it and continue the search
// keep a list of visited directories to prevent infinite recursion
func (w *fileWalker) followSymlink(linkPath, displayPath string) error {
// os.Stat follows the link and therefore also resolves relative targets correctly
info, err := os.Stat(linkPath)
if err != nil {
// broken symlink (dangling target, loop, ...): record it as an errored file
return w.cf.addFile(w.check, displayPath, w.checkPath, nil, true, true, err)
}

if !info.IsDir() {
return w.cf.addFile(w.check, displayPath, w.checkPath, fs.FileInfoToDirEntry(info), true, true, nil)
}

// resolve to the canonical real path to detect loops
resolvedSymlink, err := filepath.EvalSymlinks(linkPath)
if err != nil {
return w.cf.addFile(w.check, displayPath, w.checkPath, nil, true, true, err)
}

if slices.Contains(w.visited, resolvedSymlink) {
log.Tracef("not descending into symlink %s, target %s already visited", linkPath, resolvedSymlink)

// record the symlink itself, but do not walk into it
return w.cf.addFile(w.check, displayPath, w.checkPath, fs.FileInfoToDirEntry(info), true, true, nil)
}

// the resolved target is registered by walkFollowingLinks once it is entered
return w.walk(resolvedSymlink, displayPath, true)
}

func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry fs.DirEntry, _, isSymlink bool, err error) error {
// if the search path is a directory e.g '/usr/bin' , the program assumes you are looking for files/subdirectories under it
// therefore it does not add the search path directory to the entry list
// if it is a file like /usr/bin/bash however, it will add that
Expand All @@ -171,6 +280,7 @@ func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry
"fullname": path,
"type": "file",
"check_path": checkPath,
"is_symlink": fmt.Sprintf("%t", isSymlink),
}

matchAndAdd := func() {
Expand Down
Loading