Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit d9345c2

Browse files
Merge PR #42 into 1-4-0 - resolved version conflict (set to 1.4.0)
2 parents d5874e9 + c541db0 commit d9345c2

30 files changed

Lines changed: 4770 additions & 269 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ go.work.sum
3232
# .vscode/
3333

3434
build/
35+
36+
# AI encryption key
37+
.encryption_key

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PatchMon Agent
1+
# PatchMonEnhanced Agent
22

3-
PatchMon's monitoring agent sends package and repository information to the PatchMon server.
3+
PatchMonEnhanced's monitoring agent sends package and repository information to the PatchMonEnhanced server.
44

55
## Installation
66

@@ -180,7 +180,7 @@ Logs are written to `/var/log/patchmon-agent.log` with timestamps and structured
180180
```
181181
2023-09-27T10:30:00 level=info msg="Collecting package information..."
182182
2023-09-27T10:30:01 level=info msg="Found packages" count=156
183-
2023-09-27T10:30:02 level=info msg="Sending report to PatchMon server..."
183+
2023-09-27T10:30:02 level=info msg="Sending report to PatchMonEnhanced server..."
184184
2023-09-27T10:30:03 level=info msg="Report sent successfully"
185185
```
186186

@@ -232,7 +232,7 @@ The Go implementation maintains compatibility with the existing shell script wor
232232

233233
1. **Same command structure**: All commands work identically
234234
2. **Same configuration files**: Uses the same paths and formats
235-
3. **Same API compatibility**: Works with existing PatchMon servers
235+
3. **Same API compatibility**: Works with existing PatchMonEnhanced servers
236236
4. **Improved performance**: Faster execution and better error handling
237237

238238
To migrate:

cmd/patchmon-agent/commands/report.go

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"sync"
89
"time"
910

1011
"patchmon-agent/internal/client"
1112
"patchmon-agent/internal/hardware"
1213
"patchmon-agent/internal/integrations"
14+
"patchmon-agent/internal/integrations/compliance"
1315
"patchmon-agent/internal/integrations/docker"
1416
"patchmon-agent/internal/network"
1517
"patchmon-agent/internal/packages"
@@ -102,10 +104,10 @@ func sendReport(outputJson bool) error {
102104
needsReboot, rebootReason := systemDetector.CheckRebootRequired()
103105
installedKernel := systemDetector.GetLatestInstalledKernel()
104106
logger.WithFields(logrus.Fields{
105-
"needs_reboot": needsReboot,
106-
"reason": rebootReason,
107-
"installed_kernel": installedKernel,
108-
"running_kernel": systemInfo.KernelVersion,
107+
"needs_reboot": needsReboot,
108+
"reason": rebootReason,
109+
"installed_kernel": installedKernel,
110+
"running_kernel": systemInfo.KernelVersion,
109111
}).Info("Reboot status check completed")
110112

111113
// Get package information
@@ -172,31 +174,31 @@ func sendReport(outputJson bool) error {
172174

173175
// Create payload
174176
payload := &models.ReportPayload{
175-
Packages: packageList,
176-
Repositories: repoList,
177-
OSType: osType,
178-
OSVersion: osVersion,
179-
Hostname: hostname,
180-
IP: ipAddress,
181-
Architecture: architecture,
182-
AgentVersion: version.Version,
183-
MachineID: systemDetector.GetMachineID(),
184-
KernelVersion: systemInfo.KernelVersion,
177+
Packages: packageList,
178+
Repositories: repoList,
179+
OSType: osType,
180+
OSVersion: osVersion,
181+
Hostname: hostname,
182+
IP: ipAddress,
183+
Architecture: architecture,
184+
AgentVersion: version.Version,
185+
MachineID: systemDetector.GetMachineID(),
186+
KernelVersion: systemInfo.KernelVersion,
185187
InstalledKernelVersion: installedKernel,
186-
SELinuxStatus: systemInfo.SELinuxStatus,
187-
SystemUptime: systemInfo.SystemUptime,
188-
LoadAverage: systemInfo.LoadAverage,
189-
CPUModel: hardwareInfo.CPUModel,
190-
CPUCores: hardwareInfo.CPUCores,
191-
RAMInstalled: hardwareInfo.RAMInstalled,
192-
SwapSize: hardwareInfo.SwapSize,
193-
DiskDetails: hardwareInfo.DiskDetails,
194-
GatewayIP: networkInfo.GatewayIP,
195-
DNSServers: networkInfo.DNSServers,
196-
NetworkInterfaces: networkInfo.NetworkInterfaces,
197-
ExecutionTime: executionTime,
198-
NeedsReboot: needsReboot,
199-
RebootReason: rebootReason,
188+
SELinuxStatus: systemInfo.SELinuxStatus,
189+
SystemUptime: systemInfo.SystemUptime,
190+
LoadAverage: systemInfo.LoadAverage,
191+
CPUModel: hardwareInfo.CPUModel,
192+
CPUCores: hardwareInfo.CPUCores,
193+
RAMInstalled: hardwareInfo.RAMInstalled,
194+
SwapSize: hardwareInfo.SwapSize,
195+
DiskDetails: hardwareInfo.DiskDetails,
196+
GatewayIP: networkInfo.GatewayIP,
197+
DNSServers: networkInfo.DNSServers,
198+
NetworkInterfaces: networkInfo.NetworkInterfaces,
199+
ExecutionTime: executionTime,
200+
NeedsReboot: needsReboot,
201+
RebootReason: rebootReason,
200202
}
201203

202204
// If --report-json flag is set, output JSON and exit
@@ -241,13 +243,27 @@ func sendReport(outputJson bool) error {
241243
return nil
242244
}
243245
} else {
244-
// Proactive update check after report (non-blocking with timeout)
245-
// Run in a goroutine to avoid blocking the report completion
246+
// Proactive update check after report (with timeout to prevent hanging)
247+
// Use a WaitGroup to ensure the goroutine completes before function returns
248+
var wg sync.WaitGroup
249+
wg.Add(1)
246250
go func() {
251+
defer wg.Done()
252+
253+
// Create a context with timeout to prevent indefinite hanging
254+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
255+
defer cancel()
256+
247257
// Add a delay to prevent immediate checks after service restart
248258
// This gives the new process time to fully initialize
249-
time.Sleep(5 * time.Second)
250-
259+
select {
260+
case <-time.After(5 * time.Second):
261+
// Continue with update check
262+
case <-ctx.Done():
263+
logger.Debug("Update check cancelled due to timeout")
264+
return
265+
}
266+
251267
logger.Info("Checking for agent updates...")
252268
versionInfo, err := getServerVersionInfo()
253269
if err != nil {
@@ -277,6 +293,8 @@ func sendReport(outputJson bool) error {
277293
logger.WithField("version", versionInfo.CurrentVersion).Info("Agent is up to date")
278294
}
279295
}()
296+
// Wait for the update check to complete (with the internal timeout)
297+
wg.Wait()
280298
}
281299

282300
// Collect and send integration data (Docker, etc.) separately
@@ -305,11 +323,23 @@ func sendIntegrationData() {
305323

306324
// Register available integrations
307325
integrationMgr.Register(docker.New(logger))
326+
327+
// Only register compliance integration if not set to on-demand only
328+
// When compliance_on_demand_only is true, compliance scans will only run when triggered from the UI
329+
if !cfgManager.IsComplianceOnDemandOnly() {
330+
complianceInteg := compliance.New(logger)
331+
complianceInteg.SetDockerIntegrationEnabled(cfgManager.IsIntegrationEnabled("docker"))
332+
integrationMgr.Register(complianceInteg)
333+
} else {
334+
logger.Info("Skipping compliance scan during scheduled report (compliance_on_demand_only=true)")
335+
}
308336
// Future: integrationMgr.Register(proxmox.New(logger))
309337
// Future: integrationMgr.Register(kubernetes.New(logger))
310338

311339
// Discover and collect from all available integrations
312-
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
340+
// 25 minute timeout to allow OpenSCAP scans to complete (they can take 15+ minutes on complex systems)
341+
// This gives time for both OpenSCAP and Docker Bench to complete
342+
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Minute)
313343
defer cancel()
314344

315345
integrationData := integrationMgr.CollectAll(ctx)
@@ -332,6 +362,11 @@ func sendIntegrationData() {
332362
sendDockerData(httpClient, dockerData, hostname, machineID)
333363
}
334364

365+
// Send Compliance data if available
366+
if complianceData, exists := integrationData["compliance"]; exists && complianceData.Error == "" {
367+
sendComplianceData(httpClient, complianceData, hostname, machineID)
368+
}
369+
335370
// Future: Send other integration data here
336371
}
337372

@@ -375,3 +410,49 @@ func sendDockerData(httpClient *client.Client, integrationData *models.Integrati
375410
"updates": response.UpdatesFound,
376411
}).Info("Docker data sent successfully")
377412
}
413+
414+
// sendComplianceData sends compliance scan data to server
415+
func sendComplianceData(httpClient *client.Client, integrationData *models.IntegrationData, hostname, machineID string) {
416+
// Extract Compliance data from integration data
417+
complianceData, ok := integrationData.Data.(*models.ComplianceData)
418+
if !ok {
419+
logger.Warn("Failed to extract compliance data from integration")
420+
return
421+
}
422+
423+
if len(complianceData.Scans) == 0 {
424+
logger.Debug("No compliance scans to send")
425+
return
426+
}
427+
428+
payload := &models.CompliancePayload{
429+
ComplianceData: *complianceData,
430+
Hostname: hostname,
431+
MachineID: machineID,
432+
AgentVersion: version.Version,
433+
}
434+
435+
totalRules := 0
436+
for _, scan := range complianceData.Scans {
437+
totalRules += scan.TotalRules
438+
}
439+
440+
logger.WithFields(logrus.Fields{
441+
"scans": len(complianceData.Scans),
442+
"total_rules": totalRules,
443+
}).Info("Sending compliance data to server...")
444+
445+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) // Longer timeout for compliance
446+
defer cancel()
447+
448+
response, err := httpClient.SendComplianceData(ctx, payload)
449+
if err != nil {
450+
logger.WithError(err).Warn("Failed to send compliance data (will retry on next report)")
451+
return
452+
}
453+
454+
logger.WithFields(logrus.Fields{
455+
"scans_received": response.ScansReceived,
456+
"message": response.Message,
457+
}).Info("Compliance data sent successfully")
458+
}

cmd/patchmon-agent/commands/root.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ var (
2424

2525
// rootCmd represents the base command when called without any subcommands
2626
var rootCmd = &cobra.Command{
27-
Use: "patchmon-agent",
28-
Short: "PatchMon Agent for package monitoring",
27+
Use: "patchmon-agent",
28+
Short: "PatchMon Agent for package monitoring",
29+
Version: version.Version,
2930
Long: `PatchMon Agent v` + version.Version + `
3031
3132
A monitoring agent that sends package information to PatchMon.`,
@@ -87,7 +88,8 @@ func initialiseAgent() {
8788
if logFile == "" {
8889
logFile = config.DefaultLogFile
8990
}
90-
_ = os.MkdirAll(filepath.Dir(logFile), 0755)
91+
// SECURITY: Use 0750 for log directory (no world access)
92+
_ = os.MkdirAll(filepath.Dir(logFile), 0750)
9193
logger.SetOutput(&lumberjack.Logger{Filename: logFile, MaxSize: 10, MaxBackups: 5, MaxAge: 14, Compress: true})
9294
}
9395

@@ -132,4 +134,3 @@ func checkRoot() error {
132134
}
133135
return nil
134136
}
135-

0 commit comments

Comments
 (0)