first commit
This commit is contained in:
160
README.md
Normal file
160
README.md
Normal 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
BIN
agent_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 953 B |
105
api/client.go
Normal file
105
api/client.go
Normal 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
284
api/health.go
Normal 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
30
build.bat
Normal 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
BIN
build/winagentUSM.exe
Normal file
Binary file not shown.
404
config/config.go
Normal file
404
config/config.go
Normal 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
11
go.mod
Normal 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
15
go.sum
Normal 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
384
logging/csv_logger.go
Normal 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
188
logging/logger.go
Normal 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
99
logging/rotation.go
Normal 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
411
main.go
Normal 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
1
resource.rc
Normal file
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON "agent_icon.ico"
|
||||
413
service/service.go
Normal file
413
service/service.go
Normal 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
619
session/monitor.go
Normal 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
297
utils/file.go
Normal 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
43
versioninfo.json
Normal 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": ""
|
||||
}
|
||||
Reference in New Issue
Block a user