diff --git a/build.sh b/build.sh index 2fb80a1..6013694 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,8 @@ #!/bin/bash go build -o execguard -sudo cp execguard /usr/local/bin/ -sudo ./execguard --update /usr/local/bin/execguard -echo -e "Running execguard...Hit CTRL+C to end." -sudo execguard +if [ $? -eq 0 ]; then + sudo cp execguard /usr/local/bin/ + sudo ./execguard --update /usr/local/bin/execguard + echo -e "Running execguard...Hit CTRL+C to end." + sudo execguard +fi diff --git a/config.json.example b/config.json.example index 1334367..aabb8f9 100644 --- a/config.json.example +++ b/config.json.example @@ -2,6 +2,8 @@ "db_file": "/etc/execguard/system.db", "log_file": "/var/log/execguard.log", "mail_prog": "/usr/bin/mail", + "scanner_prog": "/usr/bin/clamscan", + "downloads": ["/home/bobs/Downloads"], "scan_interval": 0, "protected_dirs": ["/home"], "skip_dirs": [".cache",".git"], diff --git a/core/configure/configure.go b/core/configure/configure.go index e679c1e..8ba1fc0 100644 --- a/core/configure/configure.go +++ b/core/configure/configure.go @@ -9,7 +9,9 @@ type Config struct { DbFile string `json:"db_file"` // optional DB File LogFile string `json:"log_file"` // optional Log File MailProg string `json:"mail_prog"` // optional Mail Program + ScannerProg string `json:"scanner_prog"` // optional Virus Scanner Program ProtectedDirs []string `json:"protected_dirs"` + Downloads []string `josn:"downloads"` AlertEmail string `json:"alert_email"` // optional root@localhost SkipDirs []string `json:"skip_dirs"` ScanInterval int `json:"scan_interval"` // in minutes, 0 disables scan diff --git a/core/monitor/monitor.go b/core/monitor_running_bins/monitor_running_bins.go similarity index 99% rename from core/monitor/monitor.go rename to core/monitor_running_bins/monitor_running_bins.go index 4ef00dc..ecbaac7 100644 --- a/core/monitor/monitor.go +++ b/core/monitor_running_bins/monitor_running_bins.go @@ -1,4 +1,4 @@ -package monitor +package monitor_running_bins import ( "execguard/core/alert" diff --git a/core/new_file_monitor/new_file_monitor.go b/core/new_file_monitor/new_file_monitor.go new file mode 100644 index 0000000..aa1c398 --- /dev/null +++ b/core/new_file_monitor/new_file_monitor.go @@ -0,0 +1,138 @@ +package new_file_monitor + +import ( + "execguard/core/alert" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "time" + "sync" + "database/sql" + "io" + "strings" + + "github.com/fsnotify/fsnotify" +) + +var ( + alertCache sync.Map +) + +func Monitor_new_files(dirs []string, db *sql.DB, log log.Logger, scannerPath string) { + // Create new watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + // Start listening for events + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + // Check if the event is a file creation + if event.Op&fsnotify.Create == fsnotify.Create { + if strings.HasSuffix(event.Name, ".tmp") || strings.HasSuffix(event.Name, ".swp") { + continue + } + // Give the file a moment to finish writing (if downloading) + time.Sleep(2 * time.Second) + + // Check if it's a file (not a directory) + fileInfo, err := os.Stat(event.Name) + if err != nil { + log.Printf("Error checking file: %v", err) + continue + } + + if !fileInfo.IsDir() { + //exists, err := isFileNonEmpty(event.Name) + //if err != nil { + // log.Printf("Error checking file size: %v", err) + // continue + //} + if fileInfo.Size() > 0 { + log.Printf("New file detected: %s\n", event.Name) + go scanFile(event.Name, scannerPath, db, log) + } + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("Error:", err) + } + } + }() + + for _, dir := range dirs { + // Add the folder to watch + err = watcher.Add(dir) + if err != nil { + log.Fatal(err) + } + } + + select {} +} + +func scanFile(filePath string, scannerPath string, db *sql.DB, log log.Logger) { + time.Sleep(time.Duration(300) * time.Millisecond) + // Get just the filename for cleaner output + fileName := filepath.Base(filePath) + + log.Printf("Scanning file: %s\n", fileName) + + cmd := exec.Command(scannerPath, "-v", filePath) + output, err := cmd.CombinedOutput() + + if err != nil { + log.Printf("Error scanning %s: %v\n", fileName, err) + } + + log.Printf("Scan results for %s:\n%s\n", fileName, string(output)) + + // Check the exit status (0 = clean, 1 = virus found) + if cmd.ProcessState.ExitCode() == 1 { + log.Printf("WARNING: Virus detected in %s\n", fileName) + + if _, seen := alertCache.LoadOrStore(filePath, struct{}{}); !seen { + go alert.SendAlert(fmt.Sprintf("Virus detected!: %s", filePath), db, log) + time.AfterFunc(10*time.Minute, func() { + alertCache.Delete(filePath) + }) + } + + } else if cmd.ProcessState.ExitCode() == 0 { + log.Printf("File %s is clean\n", fileName) + } +} + +func isFileNonEmpty(filename string) (bool, error) { + file, err := os.Open(filename) + if err != nil { + return false, err + } + defer file.Close() + + // Seek to the end + _, err = file.Seek(0, io.SeekEnd) + if err != nil { + return false, err + } + + // Get current position (which is file size) + pos, err := file.Seek(0, io.SeekCurrent) + if err != nil { + return false, err + } + + return pos > 0, nil +} diff --git a/core/scanner/scanner.go b/core/scanner/scanner.go index 49aa737..a9644b7 100644 --- a/core/scanner/scanner.go +++ b/core/scanner/scanner.go @@ -37,14 +37,14 @@ func SetGlobalConfig(c configure.Config) { config = c } -func PeriodicScan(dirs []string, db *sql.DB, log log.Logger, mailPath string) { +func PeriodicScan(dirs []string, db *sql.DB, log log.Logger, mailPath string, scanInterval int) { skipSet := make(map[string]struct{}) for _, skip := range config.SkipDirs { if abs, err := filepath.Abs(skip); err == nil { skipSet[abs] = struct{}{} } } - interval := time.Duration(config.ScanInterval) * time.Minute + interval := time.Duration(scanInterval) * time.Minute // log.Printf("Starting periodic scan every %v...", interval) for { for _, dir := range dirs { diff --git a/execguard.go b/execguard.go index 024a79e..7ce4878 100644 --- a/execguard.go +++ b/execguard.go @@ -5,12 +5,14 @@ import ( "execguard/core/configure" "execguard/core/hasher" "execguard/core/make_key" - "execguard/core/monitor" + "execguard/core/monitor_running_bins" + "execguard/core/new_file_monitor" "execguard/core/scanner" "execguard/core/sys_database" "database/sql" "flag" "log" + "strings" "os" "path/filepath" _ "github.com/mattn/go-sqlite3" @@ -21,22 +23,32 @@ const ( dbFileDefault = "/etc/execguard/system.db" logFileDefault = "/var/log/execguard.log" mailPathDefault = "/usr/bin/mail" + clamscanDefault = "/usr/bin/clamscan" + scanIntervalDefault = 0 // Disabled ) var ( + downloadsDefault []string + downloads []string configFile string dbFile string logFile string mailPath string + clamscanPath string + scanIntervalFlag int + downloadsFlag string configFlag string dbFlag string logFlag string mailFlag string + clamscanFlag string initMode bool initFile string updateFile string migrateMode bool newKey bool + dirs []string + scanInterval int config *configure.Config ) @@ -44,10 +56,13 @@ func main() { var err error var log log.Logger + flag.IntVar(&scanIntervalFlag, "scanDelayMinutes", 99, "0 disables scanner") + flag.StringVar(&downloadsFlag, "downloads", "none", "use specified Downloads folders comma-seperated list") flag.StringVar(&configFlag, "config", "", "use specified file for config") flag.StringVar(&dbFlag, "db", "", "use specified file for database") flag.StringVar(&logFlag, "log", "", "use specified file for Logging") flag.StringVar(&mailFlag, "mail", "", "use specified file for Mail sending") + flag.StringVar(&clamscanFlag, "scanner", "", "use specified binary for Virus Scanning") flag.BoolVar(&initMode, "init", false, "initialize and populate allowed executable database") flag.StringVar(&initFile, "initFile", "", "file containing files to add to allowed database with hash") flag.StringVar(&updateFile, "update", "", "add specified file to allowed database with hash") @@ -61,7 +76,7 @@ func main() { } scanner.SetModes(initMode, initFile, updateFile, migrateMode) - monitor.SetModes(initMode, initFile, updateFile, migrateMode) + monitor_running_bins.SetModes(initMode, initFile, updateFile, migrateMode) sys_database.SetModes(initMode, initFile, updateFile, migrateMode) if configFlag != "" { @@ -78,7 +93,7 @@ func main() { hasher.SetGlobalConfig(*config) alert.SetGlobalConfig(*config) - monitor.SetGlobalConfig(*config) + monitor_running_bins.SetGlobalConfig(*config) scanner.SetGlobalConfig(*config) // Set Vars...arguemtns first, then config, then defaults @@ -107,6 +122,34 @@ func main() { } alert.SetGlobalMail(mailPath) + + if clamscanFlag != "" { + clamscanPath = clamscanFlag + } else if config.ScannerProg != "" { + clamscanPath = config.ScannerProg + } else { + clamscanPath = clamscanDefault + } + + if scanIntervalFlag != 99 { + scanInterval = scanIntervalFlag + } else if config.ScanInterval != 99 { + scanInterval = config.ScanInterval + } else { + scanInterval = scanIntervalDefault + } + + if downloadsFlag != "none" { + downloads = strings.Split(downloadsFlag, ",") + } + + if len(downloads) > 0 { + dirs = downloads + } else if len(config.Downloads) > 0 { + dirs = config.Downloads + } else { + dirs = downloadsDefault + } logf, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { @@ -156,18 +199,22 @@ func main() { return } - if config.ScanInterval > 0 { + if scanInterval > 0 { go func() { defer func() { if r := recover(); r != nil { log.Printf("Recovered from scan panic: %v", r) } }() - scanner.PeriodicScan(config.ProtectedDirs, db, log, mailPath) + scanner.PeriodicScan(config.ProtectedDirs, db, log, mailPath, scanInterval) }() } + + if len(dirs) > 0 { + go new_file_monitor.Monitor_new_files(dirs, db, log, clamscanPath) + } - if err := monitor.MonitorExecutions(db, log, mailPath); err != nil { + if err := monitor_running_bins.MonitorExecutions(db, log, mailPath); err != nil { log.Fatalf("Execution monitoring failed: %v", err) os.Exit(4) // Exit with status code 4 } diff --git a/go.mod b/go.mod index e1cccde..e5a4af8 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( github.com/yang3yen/xxtea-go v1.0.3 golang.org/x/sys v0.33.0 ) + +require github.com/fsnotify/fsnotify v1.9.0 // indirect diff --git a/go.sum b/go.sum index 92daa3c..02e39c5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/yang3yen/xxtea-go v1.0.3 h1:C7yBcDRb909v39llhqx+QjAerOeWB+Oyqt/Z7yC7TBk=