diff --git a/cmd/patchmon-agent/commands/report.go b/cmd/patchmon-agent/commands/report.go index 65453cc..a735319 100644 --- a/cmd/patchmon-agent/commands/report.go +++ b/cmd/patchmon-agent/commands/report.go @@ -102,10 +102,10 @@ func sendReport(outputJson bool) error { needsReboot, rebootReason := systemDetector.CheckRebootRequired() installedKernel := systemDetector.GetLatestInstalledKernel() logger.WithFields(logrus.Fields{ - "needs_reboot": needsReboot, - "reason": rebootReason, - "installed_kernel": installedKernel, - "running_kernel": systemInfo.KernelVersion, + "needs_reboot": needsReboot, + "reason": rebootReason, + "installed_kernel": installedKernel, + "running_kernel": systemInfo.KernelVersion, }).Info("Reboot status check completed") // Get package information @@ -172,31 +172,31 @@ func sendReport(outputJson bool) error { // Create payload payload := &models.ReportPayload{ - Packages: packageList, - Repositories: repoList, - OSType: osType, - OSVersion: osVersion, - Hostname: hostname, - IP: ipAddress, - Architecture: architecture, - AgentVersion: version.Version, - MachineID: systemDetector.GetMachineID(), - KernelVersion: systemInfo.KernelVersion, + Packages: packageList, + Repositories: repoList, + OSType: osType, + OSVersion: osVersion, + Hostname: hostname, + IP: ipAddress, + Architecture: architecture, + AgentVersion: version.Version, + MachineID: systemDetector.GetMachineID(), + KernelVersion: systemInfo.KernelVersion, InstalledKernelVersion: installedKernel, - SELinuxStatus: systemInfo.SELinuxStatus, - SystemUptime: systemInfo.SystemUptime, - LoadAverage: systemInfo.LoadAverage, - CPUModel: hardwareInfo.CPUModel, - CPUCores: hardwareInfo.CPUCores, - RAMInstalled: hardwareInfo.RAMInstalled, - SwapSize: hardwareInfo.SwapSize, - DiskDetails: hardwareInfo.DiskDetails, - GatewayIP: networkInfo.GatewayIP, - DNSServers: networkInfo.DNSServers, - NetworkInterfaces: networkInfo.NetworkInterfaces, - ExecutionTime: executionTime, - NeedsReboot: needsReboot, - RebootReason: rebootReason, + SELinuxStatus: systemInfo.SELinuxStatus, + SystemUptime: systemInfo.SystemUptime, + LoadAverage: systemInfo.LoadAverage, + CPUModel: hardwareInfo.CPUModel, + CPUCores: hardwareInfo.CPUCores, + RAMInstalled: hardwareInfo.RAMInstalled, + SwapSize: hardwareInfo.SwapSize, + DiskDetails: hardwareInfo.DiskDetails, + GatewayIP: networkInfo.GatewayIP, + DNSServers: networkInfo.DNSServers, + NetworkInterfaces: networkInfo.NetworkInterfaces, + ExecutionTime: executionTime, + NeedsReboot: needsReboot, + RebootReason: rebootReason, } // If --report-json flag is set, output JSON and exit @@ -247,7 +247,7 @@ func sendReport(outputJson bool) error { // Add a delay to prevent immediate checks after service restart // This gives the new process time to fully initialize time.Sleep(5 * time.Second) - + logger.Info("Checking for agent updates...") versionInfo, err := getServerVersionInfo() if err != nil { diff --git a/cmd/patchmon-agent/commands/root.go b/cmd/patchmon-agent/commands/root.go index caef737..58d3423 100644 --- a/cmd/patchmon-agent/commands/root.go +++ b/cmd/patchmon-agent/commands/root.go @@ -132,4 +132,3 @@ func checkRoot() error { } return nil } - diff --git a/internal/integrations/manager.go b/internal/integrations/manager.go index cadf826..574c07e 100644 --- a/internal/integrations/manager.go +++ b/internal/integrations/manager.go @@ -15,10 +15,10 @@ import ( // Manager orchestrates integration discovery and data collection type Manager struct { - integrations []Integration - logger *logrus.Logger - mu sync.RWMutex - isEnabledChecker func(string) bool // Optional function to check if integration is enabled + integrations []Integration + logger *logrus.Logger + mu sync.RWMutex + isEnabledChecker func(string) bool // Optional function to check if integration is enabled } // NewManager creates a new integration manager diff --git a/internal/network/network.go b/internal/network/network.go index 63c15ba..2055960 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -1,6 +1,7 @@ package network import ( + "encoding/hex" "fmt" "net" "os" @@ -43,20 +44,37 @@ func (m *Manager) GetNetworkInfo() models.NetworkInfo { return info } -// getGatewayIP gets the default gateway IP from routing table file +// getGatewayIP tries to get the default gateway IP (IPv4 first, then IPv6) func (m *Manager) getGatewayIP() string { + // Try IPv4 first + if gw := m.getIPv4GatewayIP(); gw != "" { + return gw + } + + // If no IPv4 gateway found, try IPv6 + if gw := m.getIPv6GatewayIP(); gw != "" { + return gw + } + + return "" +} + +// getIPv4GatewayIP gets the default gateway IP from IPv4 routing table +func (m *Manager) getIPv4GatewayIP() string { // Read /proc/net/route to find default gateway data, err := os.ReadFile("/proc/net/route") if err != nil { - m.logger.WithError(err).Warn("Failed to read /proc/net/route") + // Log as debug because on an IPv6-only system, this file might exist but be irrelevant, or fail cleanly + m.logger.WithError(err).Debug("Failed to read /proc/net/route") return "" } for line := range strings.SplitSeq(string(data), "\n") { fields := strings.Fields(line) + // Field 1 is Destination, Field 2 is Gateway if len(fields) >= 3 && fields[1] == "00000000" { // Default route // Convert hex gateway to IP - if gateway := m.hexToIP(fields[2]); gateway != "" { + if gateway := m.hexToIPv4(fields[2]); gateway != "" { return gateway } } @@ -65,8 +83,46 @@ func (m *Manager) getGatewayIP() string { return "" } -// hexToIP converts hex IP address to dotted decimal notation -func (m *Manager) hexToIP(hexIP string) string { +// getIPv6GatewayIP gets the default gateway IP from IPv6 routing table +func (m *Manager) getIPv6GatewayIP() string { + // Read /proc/net/ipv6_route to find default gateway + data, err := os.ReadFile("/proc/net/ipv6_route") + if err != nil { + m.logger.WithError(err).Debug("Failed to read /proc/net/ipv6_route") + return "" + } + + // Format of /proc/net/ipv6_route: + // 1. Dest network (32 hex chars) + // 2. Prefix length (2 hex chars) + // 3. Source network (32 hex chars) + // 4. Source prefix length (2 hex chars) + // 5. Next hop / Gateway (32 hex chars) + // ... other flags ... + + for line := range strings.SplitSeq(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) >= 5 { + dest := fields[0] + prefixLen := fields[1] + gatewayHex := fields[4] + + // Check for default route: Dest is all zeros and prefix length is 00 + if dest == "00000000000000000000000000000000" && prefixLen == "00" { + // Ignore if gateway is also 0 (means on-link) unless that's what we want, + // but usually we want the router address. + if gatewayHex != "00000000000000000000000000000000" { + return m.hexToIPv6(gatewayHex) + } + } + } + } + + return "" +} + +// hexToIPv4 converts hex IP address to dotted decimal notation +func (m *Manager) hexToIPv4(hexIP string) string { if len(hexIP) != 8 { return "" } @@ -84,6 +140,19 @@ func (m *Manager) hexToIP(hexIP string) string { return net.IP(ip).String() } +// hexToIPv6 converts standard hex IPv6 string (32 chars) to IP string +func (m *Manager) hexToIPv6(hexIP string) string { + if len(hexIP) != 32 { + return "" + } + // IPv6 in /proc/net/ipv6_route is simply a 32-char hex string (Big Endian usually) + bytes, err := hex.DecodeString(hexIP) + if err != nil { + return "" + } + return net.IP(bytes).String() +} + // parseHexByte parses a 2-character hex string to byte func parseHexByte(hex string) (byte, error) { var result byte @@ -160,7 +229,7 @@ func (m *Manager) getNetworkInterfaces() []models.NetworkInterface { if ipnet, ok := addr.(*net.IPNet); ok { var family string var gateway string - + if ipnet.IP.To4() != nil { family = constants.IPFamilyIPv4 gateway = ipv4Gateway @@ -243,7 +312,7 @@ func (m *Manager) getInterfaceGateway(interfaceName string, ipv6 bool) string { // Use ip route (defaults to IPv4) cmd = exec.Command("ip", "route", "show", "dev", interfaceName) } - + output, err := cmd.Output() if err == nil { lines := strings.Split(string(output), "\n") @@ -277,7 +346,7 @@ func (m *Manager) getInterfaceGateway(interfaceName string, ipv6 bool) string { fields := strings.Fields(line) if len(fields) >= 3 && fields[0] == interfaceName && fields[1] == "00000000" { // Default route for this interface - if gateway := m.hexToIP(fields[2]); gateway != "" { + if gateway := m.hexToIPv4(fields[2]); gateway != "" { return gateway } } diff --git a/internal/packages/apk.go b/internal/packages/apk.go index babacdc..28ae2fd 100644 --- a/internal/packages/apk.go +++ b/internal/packages/apk.go @@ -211,4 +211,3 @@ func (m *APKManager) extractPackageNameAndVersion(packageWithVersion string) (pa packageName = packageWithVersion return } - diff --git a/internal/packages/dnf_test.go b/internal/packages/dnf_test.go index a212699..3fe4480 100644 --- a/internal/packages/dnf_test.go +++ b/internal/packages/dnf_test.go @@ -48,13 +48,13 @@ func TestDNFManager_parseUpgradablePackages(t *testing.T) { manager := NewDNFManager(logger) tests := []struct { - name string - input string - pkgMgr string + name string + input string + pkgMgr string installedPackages map[string]string - securityPackages map[string]bool - expected int - expectedSecurity int + securityPackages map[string]bool + expected int + expectedSecurity int }{ { name: "upgradable packages", diff --git a/internal/repositories/apk.go b/internal/repositories/apk.go index d14ed70..0be33b4 100644 --- a/internal/repositories/apk.go +++ b/internal/repositories/apk.go @@ -59,7 +59,7 @@ func (m *APKManager) GetRepositories() ([]models.Repository, error) { // findRepoFile locates the APK repositories file func (m *APKManager) findRepoFile() (string, error) { repoFile := "/etc/apk/repositories" - + // Check if file exists if _, err := os.Stat(repoFile); err != nil { if os.IsNotExist(err) { @@ -91,7 +91,7 @@ func (m *APKManager) parseRepoFile(filename string) ([]models.Repository, error) // Regex to match repository URL pattern // Matches: http://... or https://... followed by path urlRegex := regexp.MustCompile(`^(@\S+\s+)?(https?://[^\s]+)`) - + scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -161,7 +161,7 @@ func (m *APKManager) parseRepoLine(line string, urlRegex *regexp.Regexp) *models func (m *APKManager) extractDistributionAndComponents(url string) (distribution, components string) { // Split URL by "/" parts := strings.Split(url, "/") - + // Find "alpine" in the path alpineIndex := -1 for i, part := range parts { @@ -229,4 +229,3 @@ func (m *APKManager) isValidRepoURL(url string) bool { func (m *APKManager) isSecureURL(url string) bool { return strings.HasPrefix(url, "https://") } - diff --git a/internal/utils/offset.go b/internal/utils/offset.go index c9efd7e..609c965 100644 --- a/internal/utils/offset.go +++ b/internal/utils/offset.go @@ -39,5 +39,3 @@ func hashString(s string) uint64 { h.Write([]byte(s)) return h.Sum64() } - - diff --git a/internal/utils/timezone.go b/internal/utils/timezone.go index 3881437..1205512 100644 --- a/internal/utils/timezone.go +++ b/internal/utils/timezone.go @@ -22,19 +22,19 @@ func GetTimezone() string { // Defaults to UTC if not set or invalid func GetTimezoneLocation() *time.Location { tz := GetTimezone() - + // Handle UTC explicitly if tz == "UTC" || tz == "Etc/UTC" { return time.UTC } - + // Try to load the timezone loc, err := time.LoadLocation(tz) if err != nil { // Fallback to UTC if timezone is invalid return time.UTC } - + return loc } @@ -64,25 +64,25 @@ func ParseTime(timeStr string) (time.Time, error) { if t, err := time.Parse(time.RFC3339, timeStr); err == nil { return t, nil } - + // Try RFC3339Nano if t, err := time.Parse(time.RFC3339Nano, timeStr); err == nil { return t, nil } - + // Try common formats formats := []string{ "2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02T15:04:05Z07:00", } - + for _, format := range formats { if t, err := time.Parse(format, timeStr); err == nil { return t, nil } } - + // If all else fails, return zero time return time.Time{}, nil } @@ -92,4 +92,3 @@ func FormatTimeForDisplay(t time.Time) string { loc := GetTimezoneLocation() return t.In(loc).Format("2006-01-02T15:04:05") } -