Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions cmd/patchmon-agent/commands/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion cmd/patchmon-agent/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,3 @@ func checkRoot() error {
}
return nil
}

8 changes: 4 additions & 4 deletions internal/integrations/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 77 additions & 8 deletions internal/network/network.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package network

import (
"encoding/hex"
"fmt"
"net"
"os"
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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 ""
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
}
Expand Down
1 change: 0 additions & 1 deletion internal/packages/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,3 @@ func (m *APKManager) extractPackageNameAndVersion(packageWithVersion string) (pa
packageName = packageWithVersion
return
}

12 changes: 6 additions & 6 deletions internal/packages/dnf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 3 additions & 4 deletions internal/repositories/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -229,4 +229,3 @@ func (m *APKManager) isValidRepoURL(url string) bool {
func (m *APKManager) isSecureURL(url string) bool {
return strings.HasPrefix(url, "https://")
}

2 changes: 0 additions & 2 deletions internal/utils/offset.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,3 @@ func hashString(s string) uint64 {
h.Write([]byte(s))
return h.Sum64()
}


15 changes: 7 additions & 8 deletions internal/utils/timezone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -92,4 +92,3 @@ func FormatTimeForDisplay(t time.Time) string {
loc := GetTimezoneLocation()
return t.In(loc).Format("2006-01-02T15:04:05")
}