You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
4.2 KiB
136 lines
4.2 KiB
package monitor
|
|
|
|
import (
|
|
"execguard/core/alert"
|
|
"execguard/core/configure"
|
|
"execguard/core/sys_database"
|
|
"database/sql"
|
|
"golang.org/x/sys/unix"
|
|
"path/filepath"
|
|
"encoding/binary"
|
|
"sync"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"bytes"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
const sizeofFanotifyEventMetadata = int(unsafe.Sizeof(unix.FanotifyEventMetadata{}))
|
|
|
|
var (
|
|
config configure.Config
|
|
initMode bool
|
|
initFile string
|
|
updateFile string
|
|
migrateMode bool
|
|
dbMutex sync.Mutex
|
|
alertCache sync.Map
|
|
)
|
|
|
|
func SetModes(mode bool, file string, update string, migrate bool) {
|
|
initMode = mode
|
|
initFile = file
|
|
updateFile = update
|
|
migrateMode = migrate
|
|
}
|
|
|
|
func SetGlobalConfig(c configure.Config) {
|
|
config = c
|
|
}
|
|
|
|
func MonitorExecutions(db *sql.DB, log log.Logger, mailPath string) error {
|
|
fd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE)
|
|
if err != nil {
|
|
return fmt.Errorf("fanotify init failed: %w", err)
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
success := false
|
|
for _, dir := range config.ProtectedDirs {
|
|
if err := unix.FanotifyMark(fd, unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, unix.FAN_OPEN_EXEC_PERM, unix.AT_FDCWD, dir); err != nil {
|
|
log.Printf("Failed to mark %s: %v", dir, err)
|
|
} else {
|
|
success = true
|
|
}
|
|
}
|
|
if !success {
|
|
return fmt.Errorf("failed to mark any protected directories")
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("Recovered from panic in monitorExecutions: %v", r)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
n, err := unix.Read(fd, buf)
|
|
if err != nil {
|
|
return fmt.Errorf("fanotify read failed: %w", err)
|
|
}
|
|
|
|
for offset := 0; offset < n; {
|
|
if n-offset < sizeofFanotifyEventMetadata {
|
|
break
|
|
}
|
|
|
|
meta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buf[offset]))
|
|
if meta.Event_len == 0 {
|
|
break
|
|
}
|
|
|
|
resp := unix.FanotifyResponse{Fd: meta.Fd, Response: unix.FAN_ALLOW}
|
|
shouldClose := true
|
|
|
|
if meta.Mask&unix.FAN_OPEN_EXEC_PERM != 0 {
|
|
fdpath := fmt.Sprintf("/proc/self/fd/%d", meta.Fd)
|
|
path, err := os.Readlink(fdpath)
|
|
if err == nil {
|
|
absPath, _ := filepath.Abs(path)
|
|
absPath, _ = filepath.EvalSymlinks(absPath)
|
|
if info, statErr := os.Stat(absPath); statErr == nil &&
|
|
info.Mode().IsRegular() && (info.Mode().Perm()&0111 != 0) {
|
|
|
|
if initMode {
|
|
go func(p string, fd int) {
|
|
sys_database.AddToAllowed(db, log, p)
|
|
unix.Close(fd)
|
|
}(absPath, int(meta.Fd))
|
|
shouldClose = false
|
|
} else {
|
|
if !sys_database.IsAllowed(db, log, absPath) {
|
|
log.Printf("Blocked execution attempt: %s", absPath)
|
|
|
|
if _, seen := alertCache.LoadOrStore(absPath, struct{}{}); !seen {
|
|
go alert.SendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath), db, log)
|
|
time.AfterFunc(10*time.Minute, func() {
|
|
alertCache.Delete(absPath)
|
|
})
|
|
}
|
|
|
|
resp.Response = unix.FAN_DENY
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var respBuf bytes.Buffer
|
|
if err := binary.Write(&respBuf, binary.LittleEndian, resp); err != nil {
|
|
log.Printf("Failed to encode fanotify response: %v", err)
|
|
} else if _, err := unix.Write(fd, respBuf.Bytes()); err != nil {
|
|
log.Printf("Fanotify response write error: %v", err)
|
|
}
|
|
|
|
if shouldClose {
|
|
unix.Close(int(meta.Fd))
|
|
}
|
|
|
|
offset += int(meta.Event_len)
|
|
}
|
|
}
|
|
}
|
|
|