Files
winauthmon-agent/main.go
2025-05-25 20:42:59 +01:00

412 lines
13 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"monitoring-agent-win/config"
"monitoring-agent-win/service"
)
const (
// ExeName is the name of the executable
ExeName = "winagentUSM.exe"
)
// Command-line flags
var (
serviceFlag = flag.String("service", "", "Service command: install, start, stop, restart, remove, run")
configFlag = flag.String("config", "", "Path to configuration file (used with -service install)")
apiKeyFlag = flag.String("api-key", "", "API key for authentication")
urlFlag = flag.String("url", "", "Server URL")
debugFlag = flag.String("debug", "", "Enable or disable debug logging (true/false)")
timezoneFlag = flag.String("timezone", "", "Timezone (e.g., Europe/London, America/New_York)")
checkIntervalFlag = flag.String("check-interval", "", "Health check interval in minutes (0 to disable)")
sessionLogSizeFlag = flag.String("session-log-size", "", "Session log rotation size in MB (0 to disable, max 20)")
errorLogSizeFlag = flag.String("error-log-size", "", "Error log rotation size in MB (0 to disable, max 20)")
eventLogSizeFlag = flag.String("event-log-size", "", "Event CSV log rotation size in MB (0 to disable, max 20)")
helpFlag = flag.Bool("help", false, "Display help information")
)
func main() {
flag.Parse()
// Display help if requested
if *helpFlag {
displayHelp()
return
}
// Get configuration
cfg := config.GetConfig()
// Determine config file path - first check if it exists in install dir, otherwise use current directory
defaultConfigFile := filepath.Join(cfg.InstallDir, "config.ini")
currentDirConfig := "./config.ini"
var configFile string
if _, err := os.Stat(defaultConfigFile); err == nil {
configFile = defaultConfigFile
} else if _, err := os.Stat(currentDirConfig); err == nil {
configFile = currentDirConfig
} else {
// Use default location if neither exists
configFile = defaultConfigFile
}
// Handle service commands
if *serviceFlag != "" {
handleServiceCommand(*serviceFlag, configFile, *configFlag)
return
}
// Check if any config update flags are provided
hasConfigFlags := *apiKeyFlag != "" || *urlFlag != "" || *debugFlag != "" || *timezoneFlag != "" || *checkIntervalFlag != "" ||
*sessionLogSizeFlag != "" || *errorLogSizeFlag != "" || *eventLogSizeFlag != ""
// If config update flags are provided, load existing config first
if hasConfigFlags {
// Load existing configuration from file if it exists
if err := cfg.Load(configFile); err != nil {
fmt.Printf("Failed to load existing configuration: %v\n", err)
os.Exit(1)
}
}
// Update config if flags provided
configUpdated := false
restartService := false
if *apiKeyFlag != "" {
cfg.UpdateAPIKey(*apiKeyFlag)
configUpdated = true
restartService = true
}
if *urlFlag != "" {
cfg.UpdateServerURL(*urlFlag)
configUpdated = true
restartService = true
}
if *debugFlag != "" {
debugValue, err := strconv.ParseBool(*debugFlag)
if err != nil {
fmt.Printf("Invalid debug value: %s. Must be 'true' or 'false'.\n", *debugFlag)
os.Exit(1)
}
cfg.UpdateDebugLogs(debugValue)
configUpdated = true
restartService = true
}
if *timezoneFlag != "" {
cfg.UpdateTimezone(*timezoneFlag)
configUpdated = true
restartService = true
}
if *checkIntervalFlag != "" {
intervalValue, err := strconv.Atoi(*checkIntervalFlag)
if err != nil {
fmt.Printf("Invalid check-interval value: %s. Must be a number.\n", *checkIntervalFlag)
os.Exit(1)
}
if intervalValue < 0 {
fmt.Printf("Invalid check-interval value: %d. Must be 0 or greater.\n", intervalValue)
os.Exit(1)
}
cfg.UpdateHealthCheckInterval(intervalValue)
configUpdated = true
restartService = true
}
if *sessionLogSizeFlag != "" {
sizeValue, err := strconv.Atoi(*sessionLogSizeFlag)
if err != nil {
fmt.Printf("Invalid session-log-size value: %s. Must be a number.\n", *sessionLogSizeFlag)
os.Exit(1)
}
if sizeValue < 0 || sizeValue > 20 {
fmt.Printf("Invalid session-log-size value: %d. Must be between 0 and 20 MB.\n", sizeValue)
os.Exit(1)
}
cfg.UpdateSessionLogRotationSize(sizeValue)
configUpdated = true
restartService = true
}
if *errorLogSizeFlag != "" {
sizeValue, err := strconv.Atoi(*errorLogSizeFlag)
if err != nil {
fmt.Printf("Invalid error-log-size value: %s. Must be a number.\n", *errorLogSizeFlag)
os.Exit(1)
}
if sizeValue < 0 || sizeValue > 20 {
fmt.Printf("Invalid error-log-size value: %d. Must be between 0 and 20 MB.\n", sizeValue)
os.Exit(1)
}
cfg.UpdateErrorLogRotationSize(sizeValue)
configUpdated = true
restartService = true
}
if *eventLogSizeFlag != "" {
sizeValue, err := strconv.Atoi(*eventLogSizeFlag)
if err != nil {
fmt.Printf("Invalid event-log-size value: %s. Must be a number.\n", *eventLogSizeFlag)
os.Exit(1)
}
if sizeValue < 0 || sizeValue > 20 {
fmt.Printf("Invalid event-log-size value: %d. Must be between 0 and 20 MB.\n", sizeValue)
os.Exit(1)
}
cfg.UpdateEventLogRotationSize(sizeValue)
configUpdated = true
restartService = true
}
// Save config if updated
if configUpdated {
if err := cfg.Save(configFile); err != nil {
fmt.Printf("Failed to save configuration: %v\n", err)
os.Exit(1)
}
fmt.Println("Configuration updated successfully.")
// Restart service if it was a configuration change that affects the running service
if restartService {
fmt.Println("Restarting service to apply changes...")
if err := service.StopService(); err != nil {
fmt.Printf("Warning: Failed to stop service: %v\n", err)
} else {
fmt.Println("Service stopped.")
}
if err := service.StartService(); err != nil {
fmt.Printf("Failed to start service: %v\n", err)
os.Exit(1)
} else {
fmt.Println("Service restarted successfully.")
fmt.Println("Health check will be performed automatically after restart.")
}
}
return
}
// If no flags provided, display help
if flag.NFlag() == 0 {
displayHelp()
return
}
}
// handleServiceCommand handles service commands
func handleServiceCommand(cmd, configFile, externalConfigFile string) {
var err error
// Get configuration
cfg := config.GetConfig()
// Make sure the config directory exists
if err := os.MkdirAll(filepath.Dir(configFile), 0755); err != nil {
fmt.Printf("Failed to create config directory: %v\n", err)
os.Exit(1)
}
// For install command, handle config file loading
if strings.ToLower(cmd) == "install" {
var sourceConfigFile string
// Determine which config file to use as source
if externalConfigFile != "" {
// Use external config file if provided
sourceConfigFile = externalConfigFile
fmt.Printf("Using external config file: %s\n", sourceConfigFile)
// Check if the external config file exists
if _, err := os.Stat(sourceConfigFile); os.IsNotExist(err) {
fmt.Printf("External config file does not exist: %s\n", sourceConfigFile)
os.Exit(1)
}
} else {
// Check if config.ini exists in current directory
currentDirConfig := "./config.ini"
if _, err := os.Stat(currentDirConfig); err == nil {
sourceConfigFile = currentDirConfig
fmt.Printf("Using config file from current directory: %s\n", sourceConfigFile)
} else {
// Use default config (will be created)
sourceConfigFile = ""
fmt.Println("Using default configuration")
}
}
// Load configuration from source
if sourceConfigFile != "" {
if err := cfg.LoadFromSource(sourceConfigFile); err != nil {
fmt.Printf("Failed to load source configuration: %v\n", err)
os.Exit(1)
}
} else {
// Load default config to ensure defaults are set
if err := cfg.Load(configFile); err != nil {
fmt.Printf("Failed to load default configuration: %v\n", err)
os.Exit(1)
}
}
// Save the config to the installation directory
if err := cfg.Save(configFile); err != nil {
fmt.Printf("Failed to save configuration to installation directory: %v\n", err)
os.Exit(1)
}
if sourceConfigFile != "" && sourceConfigFile != configFile {
fmt.Printf("Configuration copied from %s to %s\n", sourceConfigFile, configFile)
}
}
switch strings.ToLower(cmd) {
case "install":
fmt.Println("Installing service...")
err = service.InstallService(configFile)
case "start":
fmt.Println("Starting service...")
err = service.StartService()
case "stop":
fmt.Println("Stopping service...")
err = service.StopService()
case "restart":
fmt.Println("Restarting service...")
if err = service.StopService(); err == nil {
err = service.StartService()
}
case "remove":
fmt.Println("Removing service...")
err = service.UninstallService()
case "run":
fmt.Println("Running service...")
err = service.RunService(configFile)
default:
fmt.Printf("Invalid service command: %s\n", cmd)
displayHelp()
os.Exit(1)
}
if err != nil {
fmt.Printf("Service command failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Service command completed successfully.")
}
// displayHelp displays help information
func displayHelp() {
fmt.Println(`
User Session Monitor Agent
This application monitors Windows session events (logon, logoff, lock, unlock) and sends them to an API.
It needs to be run with administrative privileges and is designed to be used as a Windows service.
All configuration values (API key, URL, debug mode, timezone) are read from config.ini.
The application includes automatic health checks:
- On startup: Validates API key and server connectivity
- Every 30 minutes: Checks server health and retries any failed events
- After config changes: Verifies new settings work correctly
Available command-line options (flags):
--service <install, start, stop, restart, remove, run>
Manage the Windows service.
- install: Installs the service by copying the executable to the installation directory and configuring it.
- start: Starts the service.
- stop: Stops the service.
- restart: Restarts the service (stops and then starts).
- remove: Removes the service and terminates related processes.
- run: Runs the service directly (for debugging).
--config <path>
Path to configuration file (used with --service install).
If specified, this config file will be copied to the installation directory.
If not specified, looks for config.ini in current directory, otherwise uses defaults.
Example: --service install --config "c:\users\bob\Downloads\config.ini"
--api-key <key>
Updates the API key in config.ini.
Example: --api-key 47959c6d5d8db64eb0ec3ad824ccbe82618632e2a58823d84aba92078da693fa
--url <url>
Updates the server URL in config.ini.
Example: --url https://yourserver:8000
--debug <true/false>
Enables or disables debug logging in config.ini.
Example: --debug true
--timezone <timezone>
Updates the timezone in config.ini.
Example: --timezone Europe/London
--check-interval <minutes>
Updates the health check interval in minutes in config.ini.
Use 0 to disable periodic health checks.
Example: --check-interval 15
--session-log-size <MB>
Updates the session monitor log rotation size in MB.
Use 0 to disable rotation, maximum is 20 MB.
Example: --session-log-size 10
--error-log-size <MB>
Updates the error log rotation size in MB.
Use 0 to disable rotation, maximum is 20 MB.
Example: --error-log-size 5
--event-log-size <MB>
Updates the event CSV log rotation size in MB.
Use 0 to disable rotation, maximum is 20 MB.
Example: --event-log-size 8
After installing this as service, it will copy the exe file to the installation directory and create a default config.
This will be in:
C:\ProgramData\UserSessionMon\config.ini
The default config looks like below; ensure the correct API key and URL are set.
[API]
api_key = 47959c6d5d8db64eb0ec3ad824ccbe82618632e2a58823d84aba92078da693fa
server_url = https://monitoring.vm.com:8000
debug_logs = false
timezone = Europe/London
health_check_interval = 30
health_check_path = /api/health
install_dir = C:\ProgramData\UserSessionMon
[Logging]
session_log_rotation_size_mb = 5
error_log_rotation_size_mb = 5
event_log_rotation_size_mb = 5
Usage Examples:
Install the service:
winagentUSM.exe --service install
Start the service:
winagentUSM.exe --service start
Update API key and URL in config.ini:
winagentUSM.exe --api-key your_api_key --url https://yourserver:8000
Enable debug logging:
winagentUSM.exe --debug true
Update timezone:
winagentUSM.exe --timezone America/New_York
Update health check interval to 15 minutes:
winagentUSM.exe --check-interval 15
Disable periodic health checks:
winagentUSM.exe --check-interval 0
Note: Always run this application from an Administrator Command Prompt.
`)
}