Exec Guardian
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.
 
 
execguard/execguard.go

272 lines
6.4 KiB

package main
import (
"crypto/sha512"
"database/sql"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"unsafe"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/sys/unix"
)
const (
configFile = "/etc/execguard/config.json"
dbFile = "/etc/execguard/allowed.db"
logFile = "/var/log/execguard.log"
scanInterval = 9 * time.Minute
)
type Config struct {
ProtectedDirs []string `json:"protected_dirs"`
AlertEmail string `json:"alert_email"`
SkipDirs []string `json:"skip_dirs"`
}
var initMode bool
var updateFile string
var config *Config
func main() {
flag.BoolVar(&initMode, "init", false, "initialize and populate allowed executable database")
flag.StringVar(&updateFile, "update", "", "add specified file to allowed database with hash")
flag.Parse()
if os.Geteuid() != 0 {
log.Fatal("This program must be run as root")
os.Exit(1) // Exit with status code 1
}
logf, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Error opening log file: %v", err)
}
defer logf.Close()
log.SetOutput(logf)
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
log.Fatalf("Error opening database: %v", err)
os.Exit(2) // Exit with status code 2
}
defer db.Close()
createTable(db)
if updateFile != "" {
absPath, err := filepath.Abs(updateFile)
if err != nil {
log.Fatalf("Invalid update file path: %v", err)
os.Exit(1) // Exit with status code 1
}
addToAllowed(db, absPath)
log.Printf("Added to allowed list: %s", absPath)
return
}
config, err = loadConfig()
if err != nil {
log.Fatalf("Error loading config: %v", err)
os.Exit(3) // Exit with status code 3
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from scan panic: %v", r)
}
}()
periodicScan(config.ProtectedDirs, db)
}()
if err := monitorExecutions(db); err != nil {
log.Fatalf("Execution monitoring failed: %v", err)
os.Exit(4) // Exit with status code 4
}
}
func loadConfig() (*Config, error) {
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
func createTable(db *sql.DB) {
query := `CREATE TABLE IF NOT EXISTS allowed (
path TEXT PRIMARY KEY,
hash TEXT
)`
_, err := db.Exec(query)
if err != nil {
log.Fatalf("Failed to create table: %v", err)
os.Exit(5) // Exit with status code 5
}
}
func isAllowed(db *sql.DB, path string) bool {
var storedHash string
hash := computeHash(path)
if hash == "" {
return false
}
err := db.QueryRow("SELECT hash FROM allowed WHERE path = ?", path).Scan(&storedHash)
return err == nil && storedHash == hash
}
func addToAllowed(db *sql.DB, path string) {
hash := ""
if initMode || updateFile != "" {
hash = computeHash(path)
}
_, err := db.Exec("INSERT OR REPLACE INTO allowed(path, hash) VALUES(?, ?)", path, hash)
if err != nil {
log.Printf("Error inserting allowed entry: %v", err)
}
}
func computeHash(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
sha := sha512.Sum512(data)
return hex.EncodeToString(sha[:])
}
func periodicScan(dirs []string, db *sql.DB) {
skipSet := make(map[string]struct{})
for _, skip := range config.SkipDirs {
if abs, err := filepath.Abs(skip); err == nil {
skipSet[abs] = struct{}{}
}
}
for {
for _, dir := range dirs {
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
absPath, err := filepath.Abs(path)
if err != nil {
return nil
}
// Skip if in any of the SkipDirs
for skipDir := range skipSet {
if strings.HasPrefix(absPath, skipDir) {
return filepath.SkipDir
}
}
if d.Type().IsRegular() {
info, err := d.Info()
if err != nil || (info.Mode().Perm()&0111 == 0) {
return nil
}
absPath, _ = filepath.EvalSymlinks(absPath)
if initMode {
addToAllowed(db, absPath)
} else if !isAllowed(db, absPath) {
log.Printf("Found unauthorized executable: %s", absPath)
os.Chmod(absPath, info.Mode()&^0111)
sendAlert(fmt.Sprintf("Unauthorized executable found and blocked: %s", absPath))
}
}
return nil
})
}
time.Sleep(scanInterval)
}
}
func monitorExecutions(db *sql.DB) error {
fd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE)
if err != nil {
return err
}
defer unix.Close(fd)
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)
}
}
buf := make([]byte, 4096)
for {
n, err := unix.Read(fd, buf)
if err != nil {
return err
}
for offset := 0; offset < n; {
meta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buf[offset]))
if meta.Event_len == 0 {
break
}
resp := unix.FanotifyResponse{Fd: meta.Fd, Response: unix.FAN_ALLOW}
defer unix.Close(int(meta.Fd))
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)
info, statErr := os.Stat(absPath)
if statErr == nil && info.Mode().IsRegular() && (info.Mode().Perm()&0111 != 0) {
if initMode {
addToAllowed(db, absPath)
} else if !isAllowed(db, absPath) {
log.Printf("Blocked execution attempt: %s", absPath)
sendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath))
resp.Response = unix.FAN_DENY
}
}
}
}
b := (*[unsafe.Sizeof(resp)]byte)(unsafe.Pointer(&resp))[:]
if _, err := unix.Write(fd, b); err != nil {
log.Printf("Fanotify response write error: %v", err)
}
offset += int(meta.Event_len)
}
}
}
func sendAlert(message string) {
if config.AlertEmail == "" {
return
}
if _, err := exec.LookPath("mail"); err != nil {
log.Printf("Mail command not found: %v", err)
return
}
cmd := exec.Command("mail", "-s", "ExecGuard Alert", config.AlertEmail)
cmd.Stdin = strings.NewReader(message)
if err := cmd.Run(); err != nil {
log.Printf("Failed to send alert: %v", err)
}
}