first commit

This commit is contained in:
ghostersk
2025-05-25 20:42:59 +01:00
commit c96f604cff
19 changed files with 3464 additions and 0 deletions

160
README.md Normal file
View File

@@ -0,0 +1,160 @@
# User Session Monitor Agent
## Overview
This application monitors Windows session events (logon, logoff, lock, unlock) and sends them to a specified API endpoint. It's designed to run as a Windows service on any version of Windows, collecting user session activity and securely transmitting it with an API key.
It is meant to be used as a service installed with the command `winagentUSM.exe --service install`.
Then it will copy the .exe to a folder in `C:\ProgramData\UserSessionMon\` where it will create a default `config.ini`, local record keeping with `event_log.csv` and `error.log` for errors.
All these files will have permissions restricted to SYSTEM and Administrators to view and edit.
## Core Features
- **Session Event Monitoring**: Detects and logs Windows user session events (logon, logoff, lock, unlock)
- **Secure API Communication**: Sends event data to a REST API with API key authentication
- **Configurable Settings**: Uses config.ini for API key, server URL, debug logging, and timezone
- **CSV Logging**: Maintains a local CSV log of all events with send status
- **Error Recovery**: Automatically retries sending failed events
- **Windows Service**: Can be installed and managed as a Windows service
- **Secure Permissions**: Ensures secure file permissions for config and logs
## Data Collected
For each session event, the following data is collected and sent:
- **Event Type**: Logon, Logoff, Lock, or Unlock
- **Username**: The Windows username associated with the session
- **Computer Name**: The hostname of the computer
- **IP Address**: The primary IPv4 address of the computer
- **Timestamp**: The time of the event (in the configured timezone)
## Configuration
After installing the application as a service with `winagentUSM.exe --service install`, the application will use a config.ini file located at `C:\ProgramData\UserSessionMon\config.ini` with the following structure:
```ini
[API]
api_key = 47959c6d5d8db64eb0ec3ad824ccbe82618632e2a58823d84aba92078da693fa
server_url = https://192.168.27.1:8000
debug_logs = false
timezone = Europe/London
health_check_interval = 30
health_check_path = /api/health
install_dir = C:\ProgramData\UserSessionMon
```
You can modify these settings directly in the config file or use command-line options.
## Command-Line Usage
The application supports the following command-line options:
### Service Management
```
winagentUSM.exe --service install # Install the Windows service
winagentUSM.exe --service start # Start the service
winagentUSM.exe --service stop # Stop the service
winagentUSM.exe --service restart # Restart the service
winagentUSM.exe --service remove # Remove the service
winagentUSM.exe --service run # Run the service directly (for debugging)
```
### Configuration
```
winagentUSM.exe --api-key <key> # Update the API key
winagentUSM.exe --url <url> # Update the server URL
winagentUSM.exe --debug true|false # Enable or disable debug logging
winagentUSM.exe --timezone <timezone> # Update the timezone (e.g., Europe/London)
winagentUSM.exe --check-interval <mins> # Update health check interval (0 to disable)
```
### Help
```
winagentUSM.exe --help # Display help information
```
## Health Checks
The application includes automatic health checks to ensure connectivity and retry failed events:
- **Startup Check**: Validates API key and server connectivity when the service starts
- **Periodic Checks**: Runs every 30 minutes by default (configurable)
- **Config Change Checks**: Triggered when configuration is updated via command-line flags
- **Failed Event Retry**: Automatically retries sending failed events after successful health checks
### Health Check Configuration
- `health_check_interval`: Minutes between health checks (default: 30, set to 0 to disable)
- `health_check_path`: API endpoint path for health checks (default: `/api/health`)
## Installation
To install as a Windows service:
1. Run Command Prompt or PowerShell as Administrator
2. Navigate to the directory containing the executable
3. Execute: `winagentUSM.exe --service install`
4. (Optional) Configure: `winagentUSM.exe --url "https://your-server:8000" --api-key "your-api-key"`
## File Locations
- **Executable**: `C:\ProgramData\UserSessionMon\winagentUSM.exe` (after installation)
- **Config**: `C:\ProgramData\UserSessionMon\config.ini`
- **Event Log**: `C:\ProgramData\UserSessionMon\event_log.csv`
- **Error Log**: `C:\ProgramData\UserSessionMon\error.log`
- **Service Log**: `C:\ProgramData\UserSessionMon\session_monitor.log`
## Building from Source
### Prerequisites
- Go 1.20 or newer
- Windows operating system
- Administrator rights (for service installation)
### Build Steps
1. Clone the repository
2. Navigate to the project directory
3. Run the build command:
```
go build -o winagentUSM.exe ./cmd/winagent
```
## CSV Log Format
The CSV log file contains the following columns:
```
eventtype,username,hostname,ipaddress,timestamp,send_status
```
Where `send_status` is:
- 0: Failed to send to API
- 1: Successfully sent to API
- 2: Successfully sent on retry
### Example CSV:
```csv
eventtype,username,hostname,ipaddress,timestamp,send_status
Lock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:06:14Z,1
Unlock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:06:24Z,1
Lock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:08:30Z,1
Unlock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:09:10Z,1
Lock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:10:23Z,0
Logon,Irena,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:10:38Z,0
Logoff,Irena,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:10:51Z,0
Unlock,Josh,DESKTOP-5D51HM9,192.168.66.34,2025-04-27T07:11:03Z,0
```
## Troubleshooting
Common issues and solutions:
- **Service Won't Start**: Check `error.log` for details
- **Events Not Being Sent**: Verify network connectivity and API endpoint availability
- **Permission Errors**: Ensure the application is run as administrator

BIN
agent_icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

105
api/client.go Normal file
View File

@@ -0,0 +1,105 @@
package api
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
)
// EventData represents the data sent to the API
type EventData struct {
EventType string `json:"EventType"`
UserName string `json:"UserName"`
ComputerName string `json:"ComputerName"`
IPAddress string `json:"IPAddress"`
Timestamp string `json:"Timestamp"`
Retry int `json:"retry,omitempty"`
}
// Client represents an API client
type Client struct {
apiKey string
serverURL string
client *http.Client
}
// NewClient creates a new API client
func NewClient(apiKey, serverURL string) *Client {
// Create HTTP client with TLS configuration that skips certificate validation
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
return &Client{
apiKey: apiKey,
serverURL: serverURL,
client: client,
}
}
// SendEvent sends an event to the API
func (c *Client) SendEvent(eventType, username, computerName, ipAddress, timestamp string, retry int) (bool, error) {
// Create event data
eventData := EventData{
EventType: eventType,
UserName: username,
ComputerName: computerName,
IPAddress: ipAddress,
Timestamp: timestamp,
}
// Add retry flag if this is a retry
if retry > 0 {
eventData.Retry = retry
}
// Convert to JSON
jsonData, err := json.Marshal(eventData)
if err != nil {
return false, fmt.Errorf("failed to marshal event data: %v", err)
}
// Create request
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/log_event", c.serverURL), bytes.NewBuffer(jsonData))
if err != nil {
return false, fmt.Errorf("failed to create request: %v", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", c.apiKey)
// Send request
resp, err := c.client.Do(req)
if err != nil {
return false, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return false, fmt.Errorf("API returned non-success status: %d", resp.StatusCode)
}
return true, nil
}
// UpdateAPIKey updates the API key
func (c *Client) UpdateAPIKey(apiKey string) {
c.apiKey = apiKey
}
// UpdateServerURL updates the server URL
func (c *Client) UpdateServerURL(serverURL string) {
c.serverURL = serverURL
}

284
api/health.go Normal file
View File

@@ -0,0 +1,284 @@
package api
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
"monitoring-agent-win/config"
"monitoring-agent-win/logging"
)
// HealthResponse represents the response from the health check endpoint
type HealthResponse struct {
APIKeyVerified bool `json:"api_key_verified"`
Database string `json:"database"`
Message string `json:"message"`
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
}
// HealthChecker manages health check operations
type HealthChecker struct {
logger *logging.Logger
csvLogger *logging.CSVLogger
ticker *time.Ticker
stopCh chan struct{}
}
// NewHealthChecker creates a new health checker
func NewHealthChecker(logger *logging.Logger, csvLogger *logging.CSVLogger) *HealthChecker {
return &HealthChecker{
logger: logger,
csvLogger: csvLogger,
stopCh: make(chan struct{}),
}
}
// PerformHealthCheck performs a single health check
func (hc *HealthChecker) PerformHealthCheck() (*HealthResponse, error) {
cfg := config.GetConfig()
// Construct the health check URL using configurable path
healthURL := cfg.ServerURL + cfg.HealthCheckPath
// Create HTTP client with TLS skip verification (like curl -k)
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// Create POST request
req, err := http.NewRequest("POST", healthURL, bytes.NewBuffer([]byte("{}")))
if err != nil {
return nil, fmt.Errorf("failed to create health check request: %v", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", cfg.APIKey)
// Perform the request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("health check request failed: %v", err)
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("health check failed with status: %d", resp.StatusCode)
}
// Parse response
var healthResp HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil {
return nil, fmt.Errorf("failed to decode health check response: %v", err)
}
return &healthResp, nil
}
// CheckHealthAndRetryEvents performs health check and retries failed events if successful
func (hc *HealthChecker) CheckHealthAndRetryEvents() {
healthResp, err := hc.PerformHealthCheck()
if err != nil {
if hc.logger != nil {
hc.logger.Error("Health check failed: %v", err)
}
return
}
// Log successful health check
if hc.logger != nil {
if healthResp.APIKeyVerified {
hc.logger.Info("Health check passed - API key verified, Status: %s",
healthResp.Status)
} else {
hc.logger.Error("Health check passed but API key not verified")
return
}
}
// If health check passed and API key is verified, trigger retry of failed events
if healthResp.APIKeyVerified && healthResp.Status == "ok" {
hc.retryFailedEvents()
}
}
// retryFailedEvents retries sending failed events (similar to session monitor)
func (hc *HealthChecker) retryFailedEvents() {
if hc.csvLogger == nil {
return
}
// Get failed events
failedEvents, err := hc.csvLogger.GetFailedEvents()
if err != nil {
if hc.logger != nil {
hc.logger.Error("Failed to get failed events during health check: %v", err)
}
return
}
if len(failedEvents) == 0 {
return
}
if hc.logger != nil {
hc.logger.Info("Health check triggered retry of %d failed events", len(failedEvents))
}
// Create API client for retry
cfg := config.GetConfig()
apiClient := NewClient(cfg.APIKey, cfg.ServerURL)
// Try to send each failed event
successCount := 0
var successfulEvents []map[string]string
for _, event := range failedEvents {
// Send to API with retry flag
success, err := apiClient.SendEvent(
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"],
1, // Retry flag
)
if err != nil {
if hc.logger != nil {
hc.logger.Error("Failed to retry event during health check: %v", err)
}
continue
}
if success {
// Log to main CSV with retry success status
if err := hc.csvLogger.LogEvent(
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"],
logging.SendStatusRetrySuccess,
); err != nil {
if hc.logger != nil {
hc.logger.Error("Failed to log retry success to CSV during health check: %v", err)
}
}
successCount++
successfulEvents = append(successfulEvents, event)
}
}
// Remove successful events from retry file
if successCount > 0 {
if successCount == len(failedEvents) {
// If all retries were successful, clear the entire retry file (more efficient)
if err := hc.csvLogger.ClearRetryFile(); err != nil {
if hc.logger != nil {
hc.logger.Error("Failed to clear retry file during health check: %v", err)
}
}
} else {
// If only some retries were successful, remove just the successful ones
if err := hc.csvLogger.RemoveSuccessfulRetries(successfulEvents); err != nil {
if hc.logger != nil {
hc.logger.Error("Failed to remove successful retries during health check: %v", err)
}
}
}
if hc.logger != nil {
hc.logger.Info("Health check retry completed: %d/%d events successfully resent", successCount, len(failedEvents))
}
}
}
// StartPeriodicHealthCheck starts periodic health checks based on configured interval
func (hc *HealthChecker) StartPeriodicHealthCheck() {
cfg := config.GetConfig()
// If interval is 0, don't start periodic checks
if cfg.HealthCheckInterval == 0 {
if hc.logger != nil {
hc.logger.Info("Periodic health checks disabled (interval set to 0)")
}
return
}
// Perform initial health check
go hc.CheckHealthAndRetryEvents()
// Start ticker for periodic checks using configured interval
interval := time.Duration(cfg.HealthCheckInterval) * time.Minute
hc.ticker = time.NewTicker(interval)
if hc.logger != nil {
hc.logger.Info("Starting periodic health checks every %d minutes", cfg.HealthCheckInterval)
}
go func() {
for {
select {
case <-hc.ticker.C:
hc.CheckHealthAndRetryEvents()
case <-hc.stopCh:
return
}
}
}()
}
// Stop stops the periodic health check
func (hc *HealthChecker) Stop() {
if hc.ticker != nil {
hc.ticker.Stop()
}
close(hc.stopCh)
}
// RestartPeriodicHealthCheck restarts the periodic health checks with new configuration
func (hc *HealthChecker) RestartPeriodicHealthCheck() {
// Stop current ticker if running
if hc.ticker != nil {
hc.ticker.Stop()
hc.ticker = nil
}
// Recreate the stop channel since it might be closed
hc.stopCh = make(chan struct{})
// Start with new configuration
hc.StartPeriodicHealthCheck()
}
// ValidateConfigOnStartup performs health check on startup and reports config issues
func (hc *HealthChecker) ValidateConfigOnStartup() error {
healthResp, err := hc.PerformHealthCheck()
if err != nil {
return fmt.Errorf("startup health check failed - please verify your URL and API key configuration: %v", err)
}
if !healthResp.APIKeyVerified {
return fmt.Errorf("API key verification failed - please check your API key configuration")
}
if healthResp.Status != "ok" {
return fmt.Errorf("server health check failed - status: %s, message: %s",
healthResp.Status, healthResp.Message)
}
if hc.logger != nil {
hc.logger.Info("Startup health check passed - API key verified")
}
return nil
}

30
build.bat Normal file
View File

@@ -0,0 +1,30 @@
@echo off
echo Building User Session Monitor Agent...
REM Check if Go is installed
where go >nul 2>nul
if %errorlevel% neq 0 (
echo Error: Go is not installed or not in PATH.
echo Please install Go from https://golang.org/dl/
exit /b 1
)
REM Install dependencies
echo Installing dependencies...
go get github.com/kardianos/service
go get golang.org/x/sys/windows
go get gopkg.in/ini.v1
REM Build the application
echo Building application...
go build -o winagentUSM.exe
if %errorlevel% neq 0 (
echo Build failed.
exit /b 1
)
echo Build successful. Output: winagentUSM.exe
echo.
echo To install as a service, run: winagentUSM.exe --service install
echo To see all available commands, run: winagentUSM.exe --help

BIN
build/winagentUSM.exe Normal file

Binary file not shown.

404
config/config.go Normal file
View File

@@ -0,0 +1,404 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"monitoring-agent-win/utils"
"gopkg.in/ini.v1"
)
const (
// DefaultAPIKey is the default API key used if none is provided
DefaultAPIKey = "47959c6d5d8db64eb0ec3ad824ccbe82618632e2a58823d84aba92078da693fa"
// DefaultServerURL is the default server URL used if none is provided
DefaultServerURL = "https://monitoring.vm.com:8000"
// DefaultTimezone is the default timezone used if none is provided
DefaultTimezone = "Europe/London"
// DefaultInstallDir is the default installation directory
DefaultInstallDir = "C:\\ProgramData\\UserSessionMon"
// DefaultDebugLogs is the default debug logging setting
DefaultDebugLogs = false
// DefaultHealthCheckInterval is the default health check interval in minutes (30 minutes)
DefaultHealthCheckInterval = 30
// DefaultHealthCheckPath is the default health check endpoint path
DefaultHealthCheckPath = "/api/health"
// DefaultLogRotationSizeMB is the default log rotation size in MB (5MB)
DefaultLogRotationSizeMB = 5
)
// Config represents the application configuration
type Config struct {
APIKey string
ServerURL string
DebugLogs bool
Timezone string
InstallDir string
HealthCheckInterval int // Health check interval in minutes (0 = disabled)
HealthCheckPath string // Health check endpoint path
SessionLogRotationSizeMB int // Session monitor log rotation size in MB (0 = no rotation, max 20MB)
ErrorLogRotationSizeMB int // Error log rotation size in MB (0 = no rotation, max 20MB)
EventLogRotationSizeMB int // Event CSV log rotation size in MB (0 = no rotation, max 20MB)
mu sync.RWMutex
}
var (
// instance is the singleton instance of Config
instance *Config
once sync.Once
)
// GetConfig returns the singleton instance of Config
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
APIKey: DefaultAPIKey,
ServerURL: DefaultServerURL,
DebugLogs: DefaultDebugLogs,
Timezone: DefaultTimezone,
InstallDir: DefaultInstallDir,
HealthCheckInterval: DefaultHealthCheckInterval,
HealthCheckPath: DefaultHealthCheckPath,
SessionLogRotationSizeMB: DefaultLogRotationSizeMB,
ErrorLogRotationSizeMB: DefaultLogRotationSizeMB,
EventLogRotationSizeMB: DefaultLogRotationSizeMB,
}
})
return instance
}
// isInInstallDirectory checks if the given file path is within the installation directory
func (c *Config) isInInstallDirectory(filePath string) bool {
// Get absolute paths for comparison
absFilePath, err := filepath.Abs(filePath)
if err != nil {
return false
}
absInstallDir, err := filepath.Abs(c.InstallDir)
if err != nil {
return false
}
// Check if the file is within the install directory
rel, err := filepath.Rel(absInstallDir, absFilePath)
if err != nil {
return false
}
// If the relative path doesn't start with ".." then it's within the install directory
return !strings.HasPrefix(rel, "..")
}
// Load loads the configuration from the specified file
func (c *Config) Load(configFile string) error {
c.mu.Lock()
defer c.mu.Unlock()
// Create parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(configFile), 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
// Check if the config file exists, create it if not
if _, err := os.Stat(configFile); os.IsNotExist(err) {
if err := c.createDefaultConfig(configFile); err != nil {
return fmt.Errorf("failed to create default config: %v", err)
}
// Don't secure the file yet, we need to load it first
}
// Load the config file
cfg, err := ini.Load(configFile)
if err != nil {
return fmt.Errorf("failed to load config file: %v", err)
}
// Read API section
apiSection := cfg.Section("API")
c.APIKey = apiSection.Key("api_key").MustString(DefaultAPIKey)
c.ServerURL = apiSection.Key("server_url").MustString(DefaultServerURL)
c.DebugLogs = apiSection.Key("debug_logs").MustBool(DefaultDebugLogs)
c.Timezone = apiSection.Key("timezone").MustString(DefaultTimezone)
c.InstallDir = apiSection.Key("install_dir").MustString(DefaultInstallDir)
c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval)
c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath)
// Read Logging section
loggingSection := cfg.Section("Logging")
c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
c.ErrorLogRotationSizeMB = loggingSection.Key("error_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
c.EventLogRotationSizeMB = loggingSection.Key("event_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
// Validate log rotation sizes (0 = disabled, max 20MB)
if c.SessionLogRotationSizeMB < 0 || c.SessionLogRotationSizeMB > 20 {
c.SessionLogRotationSizeMB = DefaultLogRotationSizeMB
}
if c.ErrorLogRotationSizeMB < 0 || c.ErrorLogRotationSizeMB > 20 {
c.ErrorLogRotationSizeMB = DefaultLogRotationSizeMB
}
if c.EventLogRotationSizeMB < 0 || c.EventLogRotationSizeMB > 20 {
c.EventLogRotationSizeMB = DefaultLogRotationSizeMB
}
// Now secure the file only if it's in the installation directory
if c.isInInstallDirectory(configFile) {
// Only try to secure the file if we're running as admin
if utils.IsRunningAsAdmin() {
if err := utils.SecureConfigFile(configFile); err != nil {
// Just log the error but don't fail
fmt.Printf("Warning: failed to secure config file: %v\n", err)
}
}
}
return nil
}
// LoadFromSource loads the configuration from the specified file without applying security
// This is useful for reading external config files during installation
func (c *Config) LoadFromSource(configFile string) error {
c.mu.Lock()
defer c.mu.Unlock()
// Check if the config file exists
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return fmt.Errorf("config file does not exist: %s", configFile)
}
// Load the config file
cfg, err := ini.Load(configFile)
if err != nil {
return fmt.Errorf("failed to load config file: %v", err)
}
// Read API section
apiSection := cfg.Section("API")
c.APIKey = apiSection.Key("api_key").MustString(DefaultAPIKey)
c.ServerURL = apiSection.Key("server_url").MustString(DefaultServerURL)
c.DebugLogs = apiSection.Key("debug_logs").MustBool(DefaultDebugLogs)
c.Timezone = apiSection.Key("timezone").MustString(DefaultTimezone)
c.InstallDir = apiSection.Key("install_dir").MustString(DefaultInstallDir)
c.HealthCheckInterval = apiSection.Key("health_check_interval").MustInt(DefaultHealthCheckInterval)
c.HealthCheckPath = apiSection.Key("health_check_path").MustString(DefaultHealthCheckPath)
// Read Logging section
loggingSection := cfg.Section("Logging")
c.SessionLogRotationSizeMB = loggingSection.Key("session_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
c.ErrorLogRotationSizeMB = loggingSection.Key("error_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
c.EventLogRotationSizeMB = loggingSection.Key("event_log_rotation_size_mb").MustInt(DefaultLogRotationSizeMB)
// Validate log rotation sizes (0 = disabled, max 20MB)
if c.SessionLogRotationSizeMB < 0 || c.SessionLogRotationSizeMB > 20 {
c.SessionLogRotationSizeMB = DefaultLogRotationSizeMB
}
if c.ErrorLogRotationSizeMB < 0 || c.ErrorLogRotationSizeMB > 20 {
c.ErrorLogRotationSizeMB = DefaultLogRotationSizeMB
}
if c.EventLogRotationSizeMB < 0 || c.EventLogRotationSizeMB > 20 {
c.EventLogRotationSizeMB = DefaultLogRotationSizeMB
}
// Note: No security is applied in this method - it's just for reading
return nil
}
// Save saves the configuration to the specified file
func (c *Config) Save(configFile string) error {
c.mu.RLock()
defer c.mu.RUnlock()
// Create parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(configFile), 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
// Create a new INI file
cfg := ini.Empty()
apiSection, err := cfg.NewSection("API")
if err != nil {
return fmt.Errorf("failed to create API section: %v", err)
}
// Set the values
apiSection.Key("api_key").SetValue(c.APIKey)
apiSection.Key("server_url").SetValue(c.ServerURL)
apiSection.Key("debug_logs").SetValue(fmt.Sprintf("%v", c.DebugLogs))
apiSection.Key("timezone").SetValue(c.Timezone)
apiSection.Key("install_dir").SetValue(c.InstallDir)
apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval))
apiSection.Key("health_check_path").SetValue(c.HealthCheckPath)
// Create Logging section
loggingSection, err := cfg.NewSection("Logging")
if err != nil {
return fmt.Errorf("failed to create Logging section: %v", err)
}
// Set logging values
loggingSection.Key("session_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.SessionLogRotationSizeMB))
loggingSection.Key("error_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.ErrorLogRotationSizeMB))
loggingSection.Key("event_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.EventLogRotationSizeMB))
// Save the file
if err := cfg.SaveTo(configFile); err != nil {
return fmt.Errorf("failed to save config file: %v", err)
}
// Secure the config file only if it's in the installation directory
if c.isInInstallDirectory(configFile) {
if err := utils.SecureConfigFile(configFile); err != nil {
return fmt.Errorf("failed to secure config file: %v", err)
}
}
return nil
}
// UpdateAPIKey updates the API key
func (c *Config) UpdateAPIKey(apiKey string) {
c.mu.Lock()
defer c.mu.Unlock()
c.APIKey = apiKey
}
// UpdateServerURL updates the server URL
func (c *Config) UpdateServerURL(serverURL string) {
c.mu.Lock()
defer c.mu.Unlock()
c.ServerURL = serverURL
}
// UpdateDebugLogs updates the debug logging setting
func (c *Config) UpdateDebugLogs(debugLogs bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.DebugLogs = debugLogs
}
// UpdateTimezone updates the timezone setting
func (c *Config) UpdateTimezone(timezone string) {
c.mu.Lock()
defer c.mu.Unlock()
c.Timezone = timezone
}
// UpdateHealthCheckInterval updates the health check interval setting
func (c *Config) UpdateHealthCheckInterval(interval int) {
c.mu.Lock()
defer c.mu.Unlock()
c.HealthCheckInterval = interval
}
// UpdateSessionLogRotationSize updates the session log rotation size setting
func (c *Config) UpdateSessionLogRotationSize(sizeMB int) {
c.mu.Lock()
defer c.mu.Unlock()
c.SessionLogRotationSizeMB = sizeMB
}
// UpdateErrorLogRotationSize updates the error log rotation size setting
func (c *Config) UpdateErrorLogRotationSize(sizeMB int) {
c.mu.Lock()
defer c.mu.Unlock()
c.ErrorLogRotationSizeMB = sizeMB
}
// UpdateEventLogRotationSize updates the event log rotation size setting
func (c *Config) UpdateEventLogRotationSize(sizeMB int) {
c.mu.Lock()
defer c.mu.Unlock()
c.EventLogRotationSizeMB = sizeMB
}
// UpdateInstallDir updates the installation directory
func (c *Config) UpdateInstallDir(installDir string) {
c.mu.Lock()
defer c.mu.Unlock()
c.InstallDir = installDir
}
// createDefaultConfig creates a default configuration file
func (c *Config) createDefaultConfig(configFile string) error {
// Create a new INI file
cfg := ini.Empty()
apiSection, err := cfg.NewSection("API")
if err != nil {
return fmt.Errorf("failed to create API section: %v", err)
}
// Set the default values
apiSection.Key("api_key").SetValue(c.APIKey)
apiSection.Key("server_url").SetValue(c.ServerURL)
apiSection.Key("debug_logs").SetValue(fmt.Sprintf("%v", c.DebugLogs))
apiSection.Key("timezone").SetValue(c.Timezone)
apiSection.Key("install_dir").SetValue(c.InstallDir)
apiSection.Key("health_check_interval").SetValue(fmt.Sprintf("%d", c.HealthCheckInterval))
apiSection.Key("health_check_path").SetValue(c.HealthCheckPath)
// Create Logging section
loggingSection, err := cfg.NewSection("Logging")
if err != nil {
return fmt.Errorf("failed to create Logging section: %v", err)
}
// Set default logging values
loggingSection.Key("session_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.SessionLogRotationSizeMB))
loggingSection.Key("error_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.ErrorLogRotationSizeMB))
loggingSection.Key("event_log_rotation_size_mb").SetValue(fmt.Sprintf("%d", c.EventLogRotationSizeMB))
// Save the file
if err := cfg.SaveTo(configFile); err != nil {
return fmt.Errorf("failed to save default config file: %v", err)
}
// Try to secure the config file with strict permissions, but don't fail if it doesn't work
// This is similar to how the Load method handles security
if c.isInInstallDirectory(configFile) {
// Only try to secure the file if we're running as admin
if utils.IsRunningAsAdmin() {
if err := utils.SecureConfigFile(configFile); err != nil {
// Just log the error but don't fail - this allows the config file to be created
fmt.Printf("Warning: failed to secure default config file: %v\n", err)
}
}
}
return nil
}
// WatchConfig watches for changes to the config file and reloads it
func WatchConfig(configFile string, reloadInterval time.Duration, onChange func()) {
lastModTime := time.Now()
for {
time.Sleep(reloadInterval)
info, err := os.Stat(configFile)
if err != nil {
continue
}
if info.ModTime().After(lastModTime) {
lastModTime = info.ModTime()
// Reload the config
if err := GetConfig().Load(configFile); err == nil && onChange != nil {
onChange()
}
}
}
}

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module monitoring-agent-win
go 1.24.2
require (
github.com/kardianos/service v1.2.2
golang.org/x/sys v0.33.0
gopkg.in/ini.v1 v1.67.0
)
require github.com/stretchr/testify v1.10.0 // indirect

15
go.sum Normal file
View File

@@ -0,0 +1,15 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

384
logging/csv_logger.go Normal file
View File

@@ -0,0 +1,384 @@
package logging
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"sync"
"monitoring-agent-win/config"
"monitoring-agent-win/utils"
)
// CSVLogger represents a CSV logger
type CSVLogger struct {
csvFile string
retryFile string
archiveFile string
sizeThreshold int64
rotator *LogRotator
mu sync.Mutex
}
const (
// CSV file columns
EventTypeCol = 0
UsernameCol = 1
HostnameCol = 2
IPAddressCol = 3
TimestampCol = 4
SendStatusCol = 5
// Send status values
SendStatusFailed = 0
SendStatusSuccess = 1
SendStatusRetrySuccess = 2
// Default size threshold for log rotation (5MB)
DefaultSizeThreshold = 5 * 1024 * 1024
)
// Headers for CSV files
var (
MainCSVHeaders = []string{"eventtype", "username", "hostname", "ipaddress", "timestamp", "send_status"}
RetryCSVHeaders = []string{"eventtype", "username", "hostname", "ipaddress", "timestamp"}
)
// NewCSVLogger creates a new CSV logger
func NewCSVLogger(logDir string) (*CSVLogger, error) {
csvFile := filepath.Join(logDir, "event_log.csv")
retryFile := filepath.Join(logDir, "event_log_retry.csv")
archiveFile := filepath.Join(logDir, "event_log_archive.csv")
logger := &CSVLogger{
csvFile: csvFile,
retryFile: retryFile,
archiveFile: archiveFile,
sizeThreshold: DefaultSizeThreshold,
rotator: NewLogRotator(),
}
// Initialize CSV files
if err := logger.initCSVFile(csvFile, MainCSVHeaders); err != nil {
return nil, fmt.Errorf("failed to initialize main CSV file: %v", err)
}
if err := logger.initCSVFile(retryFile, RetryCSVHeaders); err != nil {
return nil, fmt.Errorf("failed to initialize retry CSV file: %v", err)
}
return logger, nil
}
// initCSVFile initializes a CSV file with headers if it doesn't exist
func (l *CSVLogger) initCSVFile(filePath string, headers []string) error {
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// Create parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
// Create file and write headers
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create CSV file: %v", err)
}
defer file.Close()
writer := csv.NewWriter(file)
if err := writer.Write(headers); err != nil {
return fmt.Errorf("failed to write CSV headers: %v", err)
}
writer.Flush()
if err := writer.Error(); err != nil {
return fmt.Errorf("error flushing CSV writer: %v", err)
}
// Close file before securing it
file.Close()
// Secure the file with SYSTEM and Administrators permissions
if err := utils.SecureFile(filePath); err != nil {
return fmt.Errorf("failed to secure CSV file: %v", err)
}
}
return nil
}
// LogEvent logs an event to the CSV file
func (l *CSVLogger) LogEvent(eventType, username, hostname, ipAddress, timestamp string, sendStatus int) error {
l.mu.Lock()
defer l.mu.Unlock()
// Check if we need to rotate logs
if err := l.checkAndRotateLogs(); err != nil {
return fmt.Errorf("failed to rotate logs: %v", err)
}
// Determine which file to write to
var filePath string
var record []string
if sendStatus == SendStatusFailed {
// For failed events, use the retry file
filePath = l.retryFile
record = []string{eventType, username, hostname, ipAddress, timestamp}
} else {
// For successful events, use the main file
filePath = l.csvFile
record = []string{eventType, username, hostname, ipAddress, timestamp, fmt.Sprintf("%d", sendStatus)}
}
// Open the file in append mode
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open CSV file: %v", err)
}
defer file.Close()
// Write the record
writer := csv.NewWriter(file)
if err := writer.Write(record); err != nil {
return fmt.Errorf("failed to write CSV record: %v", err)
}
writer.Flush()
if err := writer.Error(); err != nil {
return fmt.Errorf("error flushing CSV writer: %v", err)
}
return nil
}
// GetFailedEvents retrieves failed events from the retry file
func (l *CSVLogger) GetFailedEvents() ([]map[string]string, error) {
l.mu.Lock()
defer l.mu.Unlock()
// Check if retry file exists
if _, err := os.Stat(l.retryFile); os.IsNotExist(err) {
return []map[string]string{}, nil
}
// Open the retry file
file, err := os.Open(l.retryFile)
if err != nil {
return nil, fmt.Errorf("failed to open retry file: %v", err)
}
defer file.Close()
// Read all records
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV records: %v", err)
}
// Skip header row
if len(records) <= 1 {
return []map[string]string{}, nil
}
// Convert records to maps
var events []map[string]string
for i, record := range records {
// Skip header row
if i == 0 {
continue
}
// Ensure record has enough fields
if len(record) < len(RetryCSVHeaders) {
continue
}
event := make(map[string]string)
for j, header := range RetryCSVHeaders {
event[header] = record[j]
}
events = append(events, event)
}
return events, nil
}
// ClearRetryFile clears the retry file after successful retries
func (l *CSVLogger) ClearRetryFile() error {
l.mu.Lock()
defer l.mu.Unlock()
// Remove the existing retry file if it exists
if _, err := os.Stat(l.retryFile); err == nil {
if err := os.Remove(l.retryFile); err != nil {
return fmt.Errorf("failed to remove retry file: %v", err)
}
}
// Recreate the retry file with just headers
return l.initCSVFile(l.retryFile, RetryCSVHeaders)
}
// RemoveSuccessfulRetries removes successfully retried events from the retry file
func (l *CSVLogger) RemoveSuccessfulRetries(successfulEvents []map[string]string) error {
l.mu.Lock()
defer l.mu.Unlock()
// If no successful events to remove, return early
if len(successfulEvents) == 0 {
return nil
}
// Read all current failed events
allFailedEvents, err := l.getFailedEventsUnsafe() // Unsafe version that doesn't lock
if err != nil {
return fmt.Errorf("failed to get failed events: %v", err)
}
// Create a map of successful events for quick lookup
successfulMap := make(map[string]bool)
for _, event := range successfulEvents {
key := fmt.Sprintf("%s|%s|%s|%s|%s",
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"])
successfulMap[key] = true
}
// Filter out successful events from the failed events list
var remainingEvents []map[string]string
for _, event := range allFailedEvents {
key := fmt.Sprintf("%s|%s|%s|%s|%s",
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"])
if !successfulMap[key] {
remainingEvents = append(remainingEvents, event)
}
}
// Remove the existing retry file
if err := os.Remove(l.retryFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove retry file: %v", err)
}
// Recreate the retry file with just headers
if err := l.initCSVFile(l.retryFile, RetryCSVHeaders); err != nil {
return fmt.Errorf("failed to recreate retry file: %v", err)
}
// Write remaining failed events back to the retry file
if len(remainingEvents) > 0 {
file, err := os.OpenFile(l.retryFile, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open retry file for writing: %v", err)
}
defer file.Close()
writer := csv.NewWriter(file)
for _, event := range remainingEvents {
record := []string{
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"],
}
if err := writer.Write(record); err != nil {
return fmt.Errorf("failed to write retry record: %v", err)
}
}
writer.Flush()
if err := writer.Error(); err != nil {
return fmt.Errorf("error flushing retry CSV writer: %v", err)
}
}
return nil
}
// getFailedEventsUnsafe retrieves failed events from the retry file without locking (internal use only)
func (l *CSVLogger) getFailedEventsUnsafe() ([]map[string]string, error) {
// Check if retry file exists
if _, err := os.Stat(l.retryFile); os.IsNotExist(err) {
return []map[string]string{}, nil
}
// Open the retry file
file, err := os.Open(l.retryFile)
if err != nil {
return nil, fmt.Errorf("failed to open retry file: %v", err)
}
defer file.Close()
// Read all records
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV records: %v", err)
}
// Skip header row
if len(records) <= 1 {
return []map[string]string{}, nil
}
// Convert records to maps
var events []map[string]string
for i, record := range records {
// Skip header row
if i == 0 {
continue
}
// Ensure record has enough fields
if len(record) < len(RetryCSVHeaders) {
continue
}
event := make(map[string]string)
for j, header := range RetryCSVHeaders {
event[header] = record[j]
}
events = append(events, event)
}
return events, nil
}
// checkAndRotateLogs checks if the main CSV file has reached the size threshold and rotates if needed
func (l *CSVLogger) checkAndRotateLogs() error {
// Get rotation size from config
cfg := config.GetConfig()
if cfg.EventLogRotationSizeMB == 0 {
// Rotation disabled
return nil
}
// Use the rotator to check and rotate the CSV file
if err := l.rotator.CheckAndRotateFile(l.csvFile, cfg.EventLogRotationSizeMB); err != nil {
return fmt.Errorf("failed to rotate CSV file: %v", err)
}
// If the file was rotated, we need to recreate it with headers
if _, err := os.Stat(l.csvFile); os.IsNotExist(err) {
if err := l.initCSVFile(l.csvFile, MainCSVHeaders); err != nil {
return fmt.Errorf("failed to recreate CSV file after rotation: %v", err)
}
}
return nil
}
// SetSizeThreshold sets the size threshold for log rotation
func (l *CSVLogger) SetSizeThreshold(sizeThreshold int64) {
l.mu.Lock()
defer l.mu.Unlock()
l.sizeThreshold = sizeThreshold
}

188
logging/logger.go Normal file
View File

@@ -0,0 +1,188 @@
package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"monitoring-agent-win/config"
"monitoring-agent-win/utils"
)
// Logger levels
const (
LevelDebug = iota
LevelInfo
LevelWarning
LevelError
)
// Logger represents a logger instance
type Logger struct {
debugLogger *log.Logger
infoLogger *log.Logger
warningLogger *log.Logger
errorLogger *log.Logger
level int
logDir string
rotator *LogRotator
mu sync.Mutex
}
var (
// instance is the singleton instance of Logger
instance *Logger
once sync.Once
)
// GetLogger returns the singleton instance of Logger
func GetLogger() *Logger {
once.Do(func() {
instance = &Logger{
level: LevelInfo,
rotator: NewLogRotator(),
}
})
return instance
}
// Setup initializes the logger with the specified log file and level
func (l *Logger) Setup(logDir string, debugEnabled bool) error {
l.mu.Lock()
defer l.mu.Unlock()
// Store log directory for rotation checks
l.logDir = logDir
// Create log directory if it doesn't exist
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %v", err)
}
// Check and rotate logs before opening them
cfg := config.GetConfig()
if err := l.rotator.CheckAndRotateIfNeeded(logDir, cfg.SessionLogRotationSizeMB, cfg.ErrorLogRotationSizeMB, cfg.EventLogRotationSizeMB); err != nil {
// Log rotation failure shouldn't prevent the service from starting
fmt.Printf("Warning: Log rotation check failed: %v\n", err)
}
// Set log level
if debugEnabled {
l.level = LevelDebug
} else {
l.level = LevelInfo
}
// Open session log file
sessionLogFile := filepath.Join(logDir, "session_monitor.log")
sessionLog, err := os.OpenFile(sessionLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to open session log file: %v", err)
}
// Open error log file
errorLogFile := filepath.Join(logDir, "error.log")
errorLog, err := os.OpenFile(errorLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
sessionLog.Close()
return fmt.Errorf("failed to open error log file: %v", err)
}
// Create multi-writer for error logs (both error log file and session log file)
errorWriter := io.MultiWriter(errorLog, sessionLog)
// Set up loggers
l.debugLogger = log.New(sessionLog, "DEBUG: ", log.Ldate|log.Ltime)
l.infoLogger = log.New(sessionLog, "INFO: ", log.Ldate|log.Ltime)
l.warningLogger = log.New(sessionLog, "WARNING: ", log.Ldate|log.Ltime)
l.errorLogger = log.New(errorWriter, "ERROR: ", log.Ldate|log.Ltime)
// Close file handles so we can secure the files
sessionLog.Close()
errorLog.Close()
// Secure log files
if err := utils.SecureFile(sessionLogFile); err != nil {
return fmt.Errorf("failed to secure session log file: %v", err)
}
if err := utils.SecureFile(errorLogFile); err != nil {
return fmt.Errorf("failed to secure error log file: %v", err)
}
// Reopen files with appropriate permissions
sessionLog, err = os.OpenFile(sessionLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to reopen session log file: %v", err)
}
errorLog, err = os.OpenFile(errorLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
sessionLog.Close()
return fmt.Errorf("failed to reopen error log file: %v", err)
}
// Update loggers with new file handles
errorWriter = io.MultiWriter(errorLog, sessionLog)
l.debugLogger = log.New(sessionLog, "DEBUG: ", log.Ldate|log.Ltime)
l.infoLogger = log.New(sessionLog, "INFO: ", log.Ldate|log.Ltime)
l.warningLogger = log.New(sessionLog, "WARNING: ", log.Ldate|log.Ltime)
l.errorLogger = log.New(errorWriter, "ERROR: ", log.Ldate|log.Ltime)
return nil
}
// checkLogRotation checks if log files need rotation
func (l *Logger) checkLogRotation() {
if l.logDir == "" {
return
}
cfg := config.GetConfig()
if err := l.rotator.CheckAndRotateIfNeeded(l.logDir, cfg.SessionLogRotationSizeMB, cfg.ErrorLogRotationSizeMB, cfg.EventLogRotationSizeMB); err != nil {
// Don't log rotation errors to avoid infinite loops, just print to stdout
fmt.Printf("Warning: Log rotation check failed: %v\n", err)
}
}
// Debug logs a debug message
func (l *Logger) Debug(format string, v ...interface{}) {
l.checkLogRotation()
if l.level <= LevelDebug && l.debugLogger != nil {
l.debugLogger.Printf(format, v...)
}
}
// Info logs an info message
func (l *Logger) Info(format string, v ...interface{}) {
l.checkLogRotation()
if l.level <= LevelInfo && l.infoLogger != nil {
l.infoLogger.Printf(format, v...)
}
}
// Warning logs a warning message
func (l *Logger) Warning(format string, v ...interface{}) {
l.checkLogRotation()
if l.level <= LevelWarning && l.warningLogger != nil {
l.warningLogger.Printf(format, v...)
}
}
// Error logs an error message
func (l *Logger) Error(format string, v ...interface{}) {
l.checkLogRotation()
if l.level <= LevelError && l.errorLogger != nil {
l.errorLogger.Printf(format, v...)
}
}
// SetLevel sets the logging level
func (l *Logger) SetLevel(level int) {
l.mu.Lock()
defer l.mu.Unlock()
l.level = level
}

99
logging/rotation.go Normal file
View File

@@ -0,0 +1,99 @@
package logging
import (
"fmt"
"os"
"path/filepath"
"sync"
)
// LogRotator handles log file rotation
type LogRotator struct {
mu sync.Mutex
}
// NewLogRotator creates a new log rotator
func NewLogRotator() *LogRotator {
return &LogRotator{}
}
// CheckAndRotateFile checks if a file needs rotation and rotates it if necessary
func (lr *LogRotator) CheckAndRotateFile(filePath string, maxSizeMB int) error {
lr.mu.Lock()
defer lr.mu.Unlock()
// If maxSizeMB is 0, rotation is disabled
if maxSizeMB == 0 {
return nil
}
// Check if file exists
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
// File doesn't exist, nothing to rotate
return nil
}
return fmt.Errorf("failed to stat file %s: %v", filePath, err)
}
// Check file size
maxSizeBytes := int64(maxSizeMB) * 1024 * 1024
if fileInfo.Size() < maxSizeBytes {
// File is under the size limit
return nil
}
// Determine archive file name based on file type
var archiveFile string
switch filepath.Base(filePath) {
case "session_monitor.log":
archiveFile = filepath.Join(filepath.Dir(filePath), "session_monitor.log.archived")
case "error.log":
archiveFile = filepath.Join(filepath.Dir(filePath), "error.log.archived")
case "event_log.csv":
archiveFile = filepath.Join(filepath.Dir(filePath), "event_log_archived.csv")
default:
// Generic archive naming
ext := filepath.Ext(filePath)
name := filePath[:len(filePath)-len(ext)]
archiveFile = name + "_archived" + ext
}
// Remove existing archive file if it exists (overwrite)
if _, err := os.Stat(archiveFile); err == nil {
if err := os.Remove(archiveFile); err != nil {
return fmt.Errorf("failed to remove existing archive file %s: %v", archiveFile, err)
}
}
// Rename current file to archive
if err := os.Rename(filePath, archiveFile); err != nil {
return fmt.Errorf("failed to rename %s to %s: %v", filePath, archiveFile, err)
}
return nil
}
// CheckAndRotateIfNeeded checks multiple files for rotation
func (lr *LogRotator) CheckAndRotateIfNeeded(logDir string, sessionSizeMB, errorSizeMB, eventSizeMB int) error {
// Rotate session monitor log
sessionLogPath := filepath.Join(logDir, "session_monitor.log")
if err := lr.CheckAndRotateFile(sessionLogPath, sessionSizeMB); err != nil {
return fmt.Errorf("failed to rotate session log: %v", err)
}
// Rotate error log
errorLogPath := filepath.Join(logDir, "error.log")
if err := lr.CheckAndRotateFile(errorLogPath, errorSizeMB); err != nil {
return fmt.Errorf("failed to rotate error log: %v", err)
}
// Rotate event CSV log
eventLogPath := filepath.Join(logDir, "event_log.csv")
if err := lr.CheckAndRotateFile(eventLogPath, eventSizeMB); err != nil {
return fmt.Errorf("failed to rotate event log: %v", err)
}
return nil
}

411
main.go Normal file
View File

@@ -0,0 +1,411 @@
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.
`)
}

1
resource.rc Normal file
View File

@@ -0,0 +1 @@
IDI_ICON1 ICON "agent_icon.ico"

BIN
rsrc.syso Normal file

Binary file not shown.

413
service/service.go Normal file
View File

@@ -0,0 +1,413 @@
package service
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"monitoring-agent-win/api"
"monitoring-agent-win/config"
"monitoring-agent-win/logging"
"monitoring-agent-win/session"
"monitoring-agent-win/utils"
"github.com/kardianos/service"
)
const (
// ServiceName is the name of the Windows service
ServiceName = "UserSessionMonService"
// ServiceDisplayName is the display name of the Windows service
ServiceDisplayName = "User Session Monitor Service"
// ServiceDescription is the description of the Windows service
ServiceDescription = "Monitors Windows session events (logon, logoff, lock, unlock) and sends to API"
)
// Program represents the service program
type Program struct {
cfg *config.Config
logger *logging.Logger
csvLogger *logging.CSVLogger
apiClient *api.Client
monitor *session.Monitor
healthChecker *api.HealthChecker
configFile string
exit chan struct{}
}
// NewProgram creates a new service program
func NewProgram(configFile string) *Program {
return &Program{
cfg: config.GetConfig(),
logger: logging.GetLogger(),
configFile: configFile,
exit: make(chan struct{}),
}
}
// Start starts the service
func (p *Program) Start(s service.Service) error {
// Start the service asynchronously
go p.run()
return nil
}
// Stop stops the service
func (p *Program) Stop(s service.Service) error {
// Signal the service to stop
close(p.exit)
// Stop the health checker
if p.healthChecker != nil {
p.healthChecker.Stop()
}
// Stop the session monitor
if p.monitor != nil {
p.monitor.Stop()
}
return nil
}
// run runs the service
func (p *Program) run() {
// Load configuration
if err := p.cfg.Load(p.configFile); err != nil {
fmt.Printf("Failed to load configuration: %v\n", err)
os.Exit(1)
}
// Create log directory
logDir := filepath.Join(p.cfg.InstallDir)
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Printf("Failed to create log directory: %v\n", err)
os.Exit(1)
}
// Setup logger
if err := p.logger.Setup(logDir, p.cfg.DebugLogs); err != nil {
fmt.Printf("Failed to setup logger: %v\n", err)
os.Exit(1)
}
// Initialize CSV logger
csvLogger, err := logging.NewCSVLogger(logDir)
if err != nil {
p.logger.Error("Failed to initialize CSV logger: %v", err)
os.Exit(1)
}
p.csvLogger = csvLogger
// Secure all sensitive files
p.secureFiles(logDir)
// Create API client
p.apiClient = api.NewClient(p.cfg.APIKey, p.cfg.ServerURL)
// Initialize health checker
p.healthChecker = api.NewHealthChecker(p.logger, p.csvLogger)
// Perform startup health check to validate configuration
p.logger.Info("Performing startup health check...")
if err := p.healthChecker.ValidateConfigOnStartup(); err != nil {
p.logger.Error("Startup health check failed: %v", err)
p.logger.Error("Service will continue running, but please check your URL and API key configuration")
} else {
p.logger.Info("Startup health check passed - configuration is valid")
}
// Start periodic health checks
p.healthChecker.StartPeriodicHealthCheck()
// Create session monitor
p.monitor = session.NewMonitor(p.apiClient, p.csvLogger, p.logger, p.cfg.Timezone)
// Start session monitor
if err := p.monitor.Start(); err != nil {
p.logger.Error("Failed to start session monitor: %v", err)
os.Exit(1)
}
// Start config file watcher
go p.watchConfig()
// Wait for exit signal
<-p.exit
}
// secureFiles secures all sensitive files with proper permissions
func (p *Program) secureFiles(logDir string) {
// Secure config file with stricter permissions (no user read access)
if err := utils.SecureConfigFile(p.configFile); err != nil {
p.logger.Error("Failed to secure config file: %v", err)
} else {
p.logger.Info("Secured config file with strict SYSTEM and Administrators permissions only")
}
// Secure log files with regular permissions (allow user read access)
logFiles := []string{
filepath.Join(logDir, "session_monitor.log"),
filepath.Join(logDir, "error.log"),
filepath.Join(logDir, "event_log.csv"),
filepath.Join(logDir, "event_log_retry.csv"),
filepath.Join(logDir, "event_log_archive.csv"),
}
for _, file := range logFiles {
if _, err := os.Stat(file); err == nil {
if err := utils.SecureFile(file); err != nil {
p.logger.Error("Failed to secure log file %s: %v", file, err)
} else {
p.logger.Debug("Secured log file %s with SYSTEM and Administrators permissions", file)
}
}
}
}
// watchConfig watches for changes to the config file
func (p *Program) watchConfig() {
config.WatchConfig(p.configFile, 5*time.Second, func() {
p.logger.Info("Configuration changed, updating components")
// Update API client
p.apiClient.UpdateAPIKey(p.cfg.APIKey)
p.apiClient.UpdateServerURL(p.cfg.ServerURL)
// Update logger level
if p.cfg.DebugLogs {
p.logger.SetLevel(logging.LevelDebug)
} else {
p.logger.SetLevel(logging.LevelInfo)
}
// Trigger health check after config change
if p.healthChecker != nil {
p.logger.Info("Triggering health check after configuration change")
go p.healthChecker.CheckHealthAndRetryEvents()
// Restart health checker with new interval if it changed
p.logger.Info("Restarting health checker with updated configuration")
go p.healthChecker.RestartPeriodicHealthCheck()
}
})
}
// InstallService installs the Windows service
func InstallService(configFile string) error {
// Create service configuration
svcConfig := &service.Config{
Name: ServiceName,
DisplayName: ServiceDisplayName,
Description: ServiceDescription,
Executable: os.Args[0],
Arguments: []string{"--service", "run"},
}
// Create service
prg := NewProgram(configFile)
s, err := service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service: %v", err)
}
// Load configuration to get install directory
if err := prg.cfg.Load(configFile); err != nil {
return fmt.Errorf("failed to load configuration: %v", err)
}
// Ensure config file is created
if err := prg.cfg.Save(configFile); err != nil {
return fmt.Errorf("failed to save configuration: %v", err)
}
// Ensure install directory exists
installDir := prg.cfg.InstallDir
if err := os.MkdirAll(installDir, 0755); err != nil {
return fmt.Errorf("failed to create installation directory: %v", err)
}
// Get the current executable path
currentExePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get current executable path: %v", err)
}
// Get the executable filename
exeName := filepath.Base(currentExePath)
// Construct the destination path in the install directory
destExePath := filepath.Join(installDir, exeName)
// Check if the executable is already in the install directory
if !strings.EqualFold(filepath.Dir(currentExePath), installDir) {
// Copy the executable to the install directory
fmt.Printf("Copying executable to %s...\n", installDir)
if err := utils.CopyFile(currentExePath, destExePath); err != nil {
return fmt.Errorf("failed to copy executable to install directory: %v", err)
}
// Update the service configuration to use the copied executable
svcConfig.Executable = destExePath
// Re-create the service with the updated configuration
s, err = service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service with updated executable path: %v", err)
}
}
// Create uninstall script
uninstallPath := filepath.Join(installDir, "uninstall.bat")
// Use the executable name from the destination path
if err := utils.CreateUninstallScript(uninstallPath, installDir, ServiceName, exeName); err != nil {
return fmt.Errorf("failed to create uninstall script: %v", err)
}
// Secure the uninstall script with stricter permissions
if err := utils.SecureConfigFile(uninstallPath); err != nil {
return fmt.Errorf("failed to secure uninstall script: %v", err)
}
// Install service
if err := s.Install(); err != nil {
return fmt.Errorf("failed to install service: %v", err)
}
// Start the service
if err := s.Start(); err != nil {
return fmt.Errorf("service installed but failed to start: %v", err)
}
return nil
}
// UninstallService uninstalls the Windows service without removing files
func UninstallService() error {
// Load configuration to get the proper config file path
cfg := config.GetConfig()
configFile := filepath.Join(cfg.InstallDir, "config.ini")
// Create service configuration
svcConfig := &service.Config{
Name: ServiceName,
}
// Create service
prg := NewProgram(configFile)
s, err := service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service: %v", err)
}
// Stop service if running
status, err := s.Status()
if err == nil && status == service.StatusRunning {
if stopErr := s.Stop(); stopErr != nil {
fmt.Printf("Warning: failed to stop service: %v\n", stopErr)
}
// Give it time to stop
time.Sleep(2 * time.Second)
}
// Uninstall service
if err := s.Uninstall(); err != nil {
return fmt.Errorf("failed to uninstall service: %v", err)
}
fmt.Println("Service successfully removed. Files remain in place.")
fmt.Println("To completely remove all files, run uninstall.bat from the installation directory as administrator.")
return nil
}
// StartService starts the Windows service
func StartService() error {
// Load configuration to ensure it exists
cfg := config.GetConfig()
configFile := filepath.Join(cfg.InstallDir, "config.ini")
// Create config file if it doesn't exist
if err := cfg.Load(configFile); err != nil {
return fmt.Errorf("failed to load configuration: %v", err)
}
// Create service configuration
svcConfig := &service.Config{
Name: ServiceName,
}
// Create service
prg := NewProgram(configFile)
s, err := service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service: %v", err)
}
// Start service
if err := s.Start(); err != nil {
return fmt.Errorf("failed to start service: %v", err)
}
return nil
}
// StopService stops the Windows service
func StopService() error {
// Load configuration to get the proper config file path
cfg := config.GetConfig()
configFile := filepath.Join(cfg.InstallDir, "config.ini")
// Create service configuration
svcConfig := &service.Config{
Name: ServiceName,
}
// Create service
prg := NewProgram(configFile)
s, err := service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service: %v", err)
}
// Stop service
if err := s.Stop(); err != nil {
return fmt.Errorf("failed to stop service: %v", err)
}
return nil
}
// RunService runs the Windows service
func RunService(configFile string) error {
// Load configuration to get install directory
cfg := config.GetConfig()
if err := cfg.Load(configFile); err != nil {
return fmt.Errorf("failed to load configuration: %v", err)
}
// Create service configuration
svcConfig := &service.Config{
Name: ServiceName,
DisplayName: ServiceDisplayName,
Description: ServiceDescription,
}
// Create service
prg := NewProgram(configFile)
s, err := service.New(prg, svcConfig)
if err != nil {
return fmt.Errorf("failed to create service: %v", err)
}
// Run service
if err := s.Run(); err != nil {
return fmt.Errorf("failed to run service: %v", err)
}
return nil
}

619
session/monitor.go Normal file
View File

@@ -0,0 +1,619 @@
package session
import (
"fmt"
"net"
"os"
"sync"
"syscall"
"time"
"unicode/utf16"
"unsafe"
"monitoring-agent-win/api"
"monitoring-agent-win/logging"
"golang.org/x/sys/windows"
)
// Event types
const (
EventLogon = "Logon"
EventLogoff = "Logoff"
EventLock = "Lock"
EventUnlock = "Unlock"
)
// WTS constants
const (
WTS_CURRENT_SERVER_HANDLE = 0
WTS_SESSION_LOGON = 0x5
WTS_SESSION_LOGOFF = 0x6
WTS_SESSION_LOCK = 0x7
WTS_SESSION_UNLOCK = 0x8
WTS_USERNAME = 5
NOTIFY_FOR_ALL_SESSIONS = 1
WM_WTSSESSION_CHANGE = 0x02B1
)
// Windows constants
const (
PM_REMOVE = 0x0001
)
// Windows types
type (
HWND uintptr
HINSTANCE uintptr
LPARAM uintptr
WPARAM uintptr
LRESULT uintptr
HANDLE uintptr
HMODULE uintptr
WNDPROC uintptr
)
// Point represents a point structure
type Point struct {
X int32
Y int32
}
// WNDCLASSEX represents a window class
type WNDCLASSEX struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance HINSTANCE
Icon uintptr
Cursor uintptr
Background uintptr
MenuName *uint16
ClassName *uint16
IconSm uintptr
}
// MSG represents a message
type MSG struct {
Hwnd HWND
Message uint32
WParam WPARAM
LParam LPARAM
Time uint32
Pt Point
}
// Monitor represents a session monitor
type Monitor struct {
apiClient *api.Client
csvLogger *logging.CSVLogger
logger *logging.Logger
timezone string
lastUserMap map[uint32]string
mu sync.RWMutex
stopChan chan struct{}
hwnd HWND
}
// NewMonitor creates a new session monitor
func NewMonitor(apiClient *api.Client, csvLogger *logging.CSVLogger, logger *logging.Logger, timezone string) *Monitor {
return &Monitor{
apiClient: apiClient,
csvLogger: csvLogger,
logger: logger,
timezone: timezone,
lastUserMap: make(map[uint32]string),
stopChan: make(chan struct{}),
}
}
// Start starts the session monitor
func (m *Monitor) Start() error {
// Load required DLLs
user32, err := windows.LoadDLL("user32.dll")
if err != nil {
return fmt.Errorf("failed to load user32.dll: %v", err)
}
defer user32.Release()
kernel32, err := windows.LoadDLL("kernel32.dll")
if err != nil {
return fmt.Errorf("failed to load kernel32.dll: %v", err)
}
defer kernel32.Release()
// Get procedures
registerClassEx, err := user32.FindProc("RegisterClassExW")
if err != nil {
return fmt.Errorf("failed to find RegisterClassExW: %v", err)
}
createWindowEx, err := user32.FindProc("CreateWindowExW")
if err != nil {
return fmt.Errorf("failed to find CreateWindowExW: %v", err)
}
getModuleHandle, err := kernel32.FindProc("GetModuleHandleW")
if err != nil {
return fmt.Errorf("failed to find GetModuleHandleW: %v", err)
}
// Register window class
className, err := syscall.UTF16PtrFromString("SessionMonitorWindow")
if err != nil {
return fmt.Errorf("failed to convert class name: %v", err)
}
// Get module handle
hInstance, _, _ := getModuleHandle.Call(0)
if hInstance == 0 {
return fmt.Errorf("failed to get module handle")
}
// Define window procedure callback
wndProcCallback := syscall.NewCallback(m.wndProc)
// Register window class
wndClass := WNDCLASSEX{
Size: uint32(unsafe.Sizeof(WNDCLASSEX{})),
WndProc: wndProcCallback,
Instance: HINSTANCE(hInstance),
ClassName: className,
}
_, _, err = registerClassEx.Call(uintptr(unsafe.Pointer(&wndClass)))
if err != syscall.Errno(0) {
return fmt.Errorf("failed to register window class: %v", err)
}
// Create window
windowName, err := syscall.UTF16PtrFromString("Session Monitor")
if err != nil {
return fmt.Errorf("failed to convert window name: %v", err)
}
hwnd, _, err := createWindowEx.Call(
0,
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowName)),
0,
0, 0, 0, 0,
0, 0, hInstance, 0,
)
if hwnd == 0 {
return fmt.Errorf("failed to create window: %v", err)
}
m.hwnd = HWND(hwnd)
// Register for session notifications
if err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS); err != nil {
destroyWindow(m.hwnd)
return fmt.Errorf("failed to register for session notifications: %v", err)
}
// Start message loop in a goroutine
go m.messageLoop()
return nil
}
// Stop stops the session monitor
func (m *Monitor) Stop() {
close(m.stopChan)
// Unregister session notifications and destroy window
if m.hwnd != 0 {
wtsUnRegisterSessionNotification(m.hwnd)
destroyWindow(m.hwnd)
m.hwnd = 0
}
}
// messageLoop processes window messages
func (m *Monitor) messageLoop() {
user32, err := windows.LoadDLL("user32.dll")
if err != nil {
m.logger.Error("Failed to load user32.dll: %v", err)
return
}
defer user32.Release()
peekMessage, err := user32.FindProc("PeekMessageW")
if err != nil {
m.logger.Error("Failed to find PeekMessageW: %v", err)
return
}
translateMessage, err := user32.FindProc("TranslateMessage")
if err != nil {
m.logger.Error("Failed to find TranslateMessage: %v", err)
return
}
dispatchMessage, err := user32.FindProc("DispatchMessageW")
if err != nil {
m.logger.Error("Failed to find DispatchMessageW: %v", err)
return
}
var msg MSG
for {
select {
case <-m.stopChan:
return
default:
// Process messages
ret, _, _ := peekMessage.Call(
uintptr(unsafe.Pointer(&msg)),
0,
0,
0,
PM_REMOVE,
)
if ret != 0 {
translateMessage.Call(uintptr(unsafe.Pointer(&msg)))
dispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
}
// Sleep to avoid high CPU usage
time.Sleep(10 * time.Millisecond)
}
}
}
// wndProc is the window procedure callback
func (m *Monitor) wndProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case WM_WTSSESSION_CHANGE:
sessionID := uint32(lParam)
eventType := uint32(wParam)
// Get session info
username := m.getSessionUsername(sessionID)
computerName := getComputerName()
ipAddress := getDefaultIPv4()
// Log the event
switch eventType {
case WTS_SESSION_LOGON:
m.logEvent(EventLogon, username, computerName, ipAddress)
case WTS_SESSION_LOGOFF:
m.logEvent(EventLogoff, username, computerName, ipAddress)
case WTS_SESSION_LOCK:
m.logEvent(EventLock, username, computerName, ipAddress)
case WTS_SESSION_UNLOCK:
m.logEvent(EventUnlock, username, computerName, ipAddress)
}
}
return defWindowProc(hwnd, msg, wParam, lParam)
}
// defWindowProc calls the default window procedure
func defWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
user32, err := windows.LoadDLL("user32.dll")
if err != nil {
return 0
}
defer user32.Release()
proc, err := user32.FindProc("DefWindowProcW")
if err != nil {
return 0
}
ret, _, _ := proc.Call(
uintptr(hwnd),
uintptr(msg),
wParam,
lParam,
)
return ret
}
// destroyWindow destroys a window
func destroyWindow(hwnd HWND) {
user32, err := windows.LoadDLL("user32.dll")
if err != nil {
return
}
defer user32.Release()
proc, err := user32.FindProc("DestroyWindow")
if err != nil {
return
}
proc.Call(uintptr(hwnd))
}
// logEvent logs an event to the CSV file and sends it to the API
func (m *Monitor) logEvent(eventType, username, computerName, ipAddress string) {
// Get current timestamp in the configured timezone
var timestamp string
if m.timezone != "" {
// Load the configured timezone
loc, err := time.LoadLocation(m.timezone)
if err != nil {
// If timezone loading fails, fall back to local time but log the error
m.logger.Warning("Failed to load timezone '%s': %v - using local time", m.timezone, err)
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00")
} else {
// Use the configured timezone
timestamp = time.Now().In(loc).Format("2006-01-02T15:04:05-07:00")
}
} else {
// If no timezone configured, use local time
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00")
}
// Log the event
m.logger.Info("User: %s - Event: %s - Computer: %s - IP: %s", username, eventType, computerName, ipAddress)
// Send to API
success, err := m.apiClient.SendEvent(eventType, username, computerName, ipAddress, timestamp, 0)
if err != nil {
m.logger.Error("Failed to send event: %v", err)
}
// Log to CSV
sendStatus := logging.SendStatusSuccess
if !success {
sendStatus = logging.SendStatusFailed
}
if err := m.csvLogger.LogEvent(eventType, username, computerName, ipAddress, timestamp, sendStatus); err != nil {
m.logger.Error("Failed to log event to CSV: %v", err)
}
// If the current event was sent successfully, retry failed events
if success {
m.retryFailedEvents()
}
}
// retryFailedEvents retries sending failed events
func (m *Monitor) retryFailedEvents() {
// Get failed events
failedEvents, err := m.csvLogger.GetFailedEvents()
if err != nil {
m.logger.Error("Failed to get failed events: %v", err)
return
}
if len(failedEvents) == 0 {
return
}
// Try to send each failed event
successCount := 0
var successfulEvents []map[string]string
for _, event := range failedEvents {
// Send to API with retry flag
success, err := m.apiClient.SendEvent(
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"],
1, // Retry flag
)
if err != nil {
m.logger.Error("Failed to retry event: %v", err)
continue
}
if success {
// Log to main CSV with retry success status
if err := m.csvLogger.LogEvent(
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["timestamp"],
logging.SendStatusRetrySuccess,
); err != nil {
m.logger.Error("Failed to log retry success to CSV: %v", err)
}
successCount++
successfulEvents = append(successfulEvents, event)
}
}
// Remove successful events from retry file
if successCount > 0 {
if successCount == len(failedEvents) {
// If all retries were successful, clear the entire retry file (more efficient)
if err := m.csvLogger.ClearRetryFile(); err != nil {
m.logger.Error("Failed to clear retry file: %v", err)
}
} else {
// If only some retries were successful, remove just the successful ones
if err := m.csvLogger.RemoveSuccessfulRetries(successfulEvents); err != nil {
m.logger.Error("Failed to remove successful retries: %v", err)
}
}
}
}
// getSessionUsername gets the username for a session
func (m *Monitor) getSessionUsername(sessionID uint32) string {
var username string
var size uint32
// Try to get the username from WTS
if wtsQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, sessionID, WTS_USERNAME, &username, &size) {
if username != "" {
// Cache the username
m.mu.Lock()
m.lastUserMap[sessionID] = username
m.mu.Unlock()
return username
}
}
// If we couldn't get the username, try to use the cached one
m.mu.RLock()
cachedUsername, ok := m.lastUserMap[sessionID]
m.mu.RUnlock()
if ok {
return cachedUsername
}
// If all else fails, return "Unknown"
return "Unknown"
}
// getComputerName gets the computer name
func getComputerName() string {
hostname, err := os.Hostname()
if err != nil {
return "Unknown"
}
return hostname
}
// getDefaultIPv4 gets the default IPv4 address
func getDefaultIPv4() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "Unknown"
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
// wtsRegisterSessionNotification registers for session notifications
func wtsRegisterSessionNotification(hwnd HWND, dwFlags uint32) error {
wtsapi32, err := windows.LoadDLL("wtsapi32.dll")
if err != nil {
return fmt.Errorf("failed to load wtsapi32.dll: %v", err)
}
defer wtsapi32.Release()
proc, err := wtsapi32.FindProc("WTSRegisterSessionNotification")
if err != nil {
return fmt.Errorf("failed to find WTSRegisterSessionNotification: %v", err)
}
r1, _, e1 := proc.Call(uintptr(hwnd), uintptr(dwFlags))
if r1 == 0 {
if e1 != nil && e1 != syscall.Errno(0) {
return fmt.Errorf("WTSRegisterSessionNotification failed: %v", e1)
}
return fmt.Errorf("WTSRegisterSessionNotification failed")
}
return nil
}
// wtsUnRegisterSessionNotification unregisters from session notifications
func wtsUnRegisterSessionNotification(hwnd HWND) error {
wtsapi32, err := windows.LoadDLL("wtsapi32.dll")
if err != nil {
return fmt.Errorf("failed to load wtsapi32.dll: %v", err)
}
defer wtsapi32.Release()
proc, err := wtsapi32.FindProc("WTSUnRegisterSessionNotification")
if err != nil {
return fmt.Errorf("failed to find WTSUnRegisterSessionNotification: %v", err)
}
r1, _, e1 := proc.Call(uintptr(hwnd))
if r1 == 0 {
if e1 != nil && e1 != syscall.Errno(0) {
return fmt.Errorf("WTSUnRegisterSessionNotification failed: %v", e1)
}
return fmt.Errorf("WTSUnRegisterSessionNotification failed")
}
return nil
}
// wtsQuerySessionInformation queries session information
func wtsQuerySessionInformation(hServer uintptr, sessionID uint32, infoClass uint32, ppBuffer *string, pBytesReturned *uint32) bool {
wtsapi32, err := windows.LoadDLL("wtsapi32.dll")
if err != nil {
return false
}
defer wtsapi32.Release()
proc, err := wtsapi32.FindProc("WTSQuerySessionInformationW")
if err != nil {
return false
}
var buffer unsafe.Pointer
r1, _, _ := proc.Call(
uintptr(hServer),
uintptr(sessionID),
uintptr(infoClass),
uintptr(unsafe.Pointer(&buffer)),
uintptr(unsafe.Pointer(pBytesReturned)),
)
if r1 != 0 && buffer != nil {
// Convert buffer to string
bufferSize := *pBytesReturned / 2 // Size in WCHARs
if bufferSize > 0 {
// Convert to Go string
*ppBuffer = utf16PtrToString((*uint16)(buffer))
}
// Free the buffer
wtsFreeMemory(uintptr(buffer))
return true
}
return false
}
// wtsFreeMemory frees memory allocated by WTS functions
func wtsFreeMemory(pMemory uintptr) {
if pMemory == 0 {
return
}
wtsapi32, err := windows.LoadDLL("wtsapi32.dll")
if err != nil {
return
}
defer wtsapi32.Release()
proc, err := wtsapi32.FindProc("WTSFreeMemory")
if err != nil {
return
}
proc.Call(pMemory)
}
// utf16PtrToString converts a UTF16 pointer to a Go string
func utf16PtrToString(p *uint16) string {
if p == nil {
return ""
}
// Find the length of the string
n := 0
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; ptr = unsafe.Pointer(uintptr(ptr) + 2) {
n++
}
// Create a slice and copy the data
s := make([]uint16, n)
for i := 0; i < n; i++ {
s[i] = *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + uintptr(i*2)))
}
return string(utf16.Decode(s))
}

297
utils/file.go Normal file
View File

@@ -0,0 +1,297 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"unsafe"
"golang.org/x/sys/windows"
)
// IsRunningAsAdmin checks if the current process is running with administrator privileges
func IsRunningAsAdmin() bool {
var sid *windows.SID
// Create a SID for the Administrators group
err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid)
if err != nil {
return false
}
defer windows.FreeSid(sid)
// Check if the current process token is a member of the Administrators group
token := windows.Token(0)
member, err := token.IsMember(sid)
if err != nil {
return false
}
return member
}
// SecureFile sets file permissions to SYSTEM and Administrators only
func SecureFile(filePath string) error {
// Get SIDs for SYSTEM and Administrators
systemSID, err := windows.StringToSid("S-1-5-18") // SYSTEM
if err != nil {
return fmt.Errorf("failed to get SYSTEM SID: %v", err)
}
adminSID, err := windows.StringToSid("S-1-5-32-544") // Administrators
if err != nil {
return fmt.Errorf("failed to get Administrators SID: %v", err)
}
// Create a new security descriptor
sd, err := windows.NewSecurityDescriptor()
if err != nil {
return fmt.Errorf("failed to create security descriptor: %v", err)
}
// Create a new DACL
acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.GRANT_ACCESS,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(systemSID),
},
},
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.GRANT_ACCESS,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(adminSID),
},
},
}, nil)
if err != nil {
return fmt.Errorf("failed to create ACL: %v", err)
}
// Set the DACL in the security descriptor
if err := sd.SetDACL(acl, true, false); err != nil {
return fmt.Errorf("failed to set DACL: %v", err)
}
// Set the file security
err = windows.SetNamedSecurityInfo(
filePath,
windows.SE_FILE_OBJECT,
windows.DACL_SECURITY_INFORMATION,
nil,
nil,
acl,
nil,
)
if err != nil {
return fmt.Errorf("failed to set file security: %v", err)
}
return nil
}
// CopyFile copies a file from src to dst
func CopyFile(src, dst string) error {
// Read source file
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read source file: %v", err)
}
// Create parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %v", err)
}
// Write destination file
if err := os.WriteFile(dst, data, 0644); err != nil {
return fmt.Errorf("failed to write destination file: %v", err)
}
return nil
}
// CreateUninstallScript creates an uninstall script
func CreateUninstallScript(scriptPath, installDir, serviceName, exeName string) error {
content := fmt.Sprintf(`@echo off
echo User Session Monitor - Uninstall Script
echo.
REM Check for admin privileges
net session >nul 2>&1
if %%errorlevel%% neq 0 (
echo ERROR: Administrator privileges required.
echo Please run this script as Administrator.
echo.
pause
exit /b 1
)
echo Stopping and removing service...
sc stop %s
sc delete %s
timeout /t 2 /nobreak >nul
echo Terminating any running processes...
taskkill /F /IM %s 2>nul
timeout /t 1 /nobreak >nul
echo Removing files from %s...
REM Copy this batch file to temp location for final cleanup
copy "%%~f0" "%%TEMP%%\\usm_cleanup_launcher.bat" >nul
REM Create cleanup batch job to remove installation directory
echo @echo off > "%%TEMP%%\\usm_cleanup.bat"
echo :loop >> "%%TEMP%%\\usm_cleanup.bat"
echo timeout /t 2 /nobreak >> "%%TEMP%%\\usm_cleanup.bat"
echo echo Deleting files... >> "%%TEMP%%\\usm_cleanup.bat"
echo attrib -h -s "%s\\*.*" /s /d >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.exe" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.csv" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.log" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.ini" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.syso" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%s\\*.bat" 2^>nul >> "%%TEMP%%\\usm_cleanup.bat"
echo echo Removing directory... >> "%%TEMP%%\\usm_cleanup.bat"
echo rmdir /S /Q "%s" >> "%%TEMP%%\\usm_cleanup.bat"
echo if exist "%s" goto loop >> "%%TEMP%%\\usm_cleanup.bat"
echo del /F /Q "%%TEMP%%\\usm_cleanup_launcher.bat" >> "%%TEMP%%\\usm_cleanup.bat"
echo exit >> "%%TEMP%%\\usm_cleanup.bat"
echo.
echo Uninstallation complete!
echo.
echo Press any key to finish cleanup...
pause > nul
start "" /min "%%TEMP%%\\usm_cleanup.bat"
exit
`, serviceName, serviceName, exeName, installDir, installDir, installDir, installDir, installDir, installDir, installDir, installDir, installDir, installDir)
// Create parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(scriptPath), 0755); err != nil {
return fmt.Errorf("failed to create script directory: %v", err)
}
// Write script file
if err := os.WriteFile(scriptPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write script file: %v", err)
}
return nil
}
// IsDomainJoined checks if the computer is domain-joined
func IsDomainJoined() (bool, error) {
var domain *uint16
var status uint32
// Call NetGetJoinInformation
netapi32, err := windows.LoadDLL("netapi32.dll")
if err != nil {
return false, fmt.Errorf("failed to load netapi32.dll: %v", err)
}
defer netapi32.Release()
netGetJoinInfo, err := netapi32.FindProc("NetGetJoinInformation")
if err != nil {
return false, fmt.Errorf("failed to find NetGetJoinInformation: %v", err)
}
netApiBufferFree, err := netapi32.FindProc("NetApiBufferFree")
if err != nil {
return false, fmt.Errorf("failed to find NetApiBufferFree: %v", err)
}
r1, _, _ := netGetJoinInfo.Call(
0,
uintptr(unsafe.Pointer(&domain)),
uintptr(unsafe.Pointer(&status)),
)
if r1 != 0 {
return false, fmt.Errorf("NetGetJoinInformation failed: %d", r1)
}
defer netApiBufferFree.Call(uintptr(unsafe.Pointer(domain)))
// Check if domain joined (status == 1 means domain joined)
return status == 1, nil
}
// SecureConfigFile sets file permissions to SYSTEM and Administrators only with no read access for others
func SecureConfigFile(filePath string) error {
// Get SIDs for SYSTEM and Administrators
systemSID, err := windows.StringToSid("S-1-5-18") // SYSTEM
if err != nil {
return fmt.Errorf("failed to get SYSTEM SID: %v", err)
}
adminSID, err := windows.StringToSid("S-1-5-32-544") // Administrators
if err != nil {
return fmt.Errorf("failed to get Administrators SID: %v", err)
}
// Create a new security descriptor
sd, err := windows.NewSecurityDescriptor()
if err != nil {
return fmt.Errorf("failed to create security descriptor: %v", err)
}
// Create a new DACL with only SYSTEM and Administrators entries
acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.GRANT_ACCESS,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(systemSID),
},
},
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.GRANT_ACCESS,
Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(adminSID),
},
},
}, nil)
if err != nil {
return fmt.Errorf("failed to create ACL: %v", err)
}
// Set the DACL in the security descriptor with DACL_PROTECTED flag to remove inherited permissions
if err := sd.SetDACL(acl, true, true); err != nil {
return fmt.Errorf("failed to set DACL: %v", err)
}
// Set the file security with PROTECTED_DACL_SECURITY_INFORMATION to remove inheritance
err = windows.SetNamedSecurityInfo(
filePath,
windows.SE_FILE_OBJECT,
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
nil,
nil,
acl,
nil,
)
if err != nil {
return fmt.Errorf("failed to set file security: %v", err)
}
return nil
}

43
versioninfo.json Normal file
View File

@@ -0,0 +1,43 @@
{
"FixedFileInfo": {
"FileVersion": {
"Major": 1,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 1,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo": {
"Comments": "",
"CompanyName": "",
"FileDescription": "Windows User Session Monitor",
"FileVersion": "1.0.0.0",
"InternalName": "monitoring-agent-win",
"LegalCopyright": "",
"LegalTrademarks": "",
"OriginalFilename": "monitoring-agent-win.exe",
"PrivateBuild": "",
"ProductName": "Windows User Session Monitor",
"ProductVersion": "1.0.0.0",
"SpecialBuild": ""
},
"VarFileInfo": {
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
},
"IconPath": "agent_icon.ico",
"ManifestPath": ""
}