From 5eb658871dbba9694dee55431c126f9f5692ab0d Mon Sep 17 00:00:00 2001 From: Bruce Abbott <641138+abbott@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:07:08 -0400 Subject: [PATCH] Refactor command execution --- pkg/cmd/system_details_cmd.go | 2 +- pkg/infrastructure/menu_factory.go | 4 +-- pkg/menu/main_menu.go | 8 +++-- pkg/menu/system_details_menu.go | 6 +++- pkg/security/security.go | 36 +++++++------------ pkg/security/status.go | 55 +++++++++++++----------------- pkg/security/status_test.go | 19 +++++++++++ pkg/system/collectors.go | 28 ++++++--------- pkg/system/details.go | 5 ++- pkg/system/system_test.go | 38 +++++++++++++++++++++ pkg/system/user_info.go | 12 +++---- pkg/updates/updates.go | 40 ++++++++-------------- pkg/updates/updates_test.go | 21 ++++++++++++ 13 files changed, 162 insertions(+), 112 deletions(-) create mode 100644 pkg/security/status_test.go create mode 100644 pkg/system/system_test.go create mode 100644 pkg/updates/updates_test.go diff --git a/pkg/cmd/system_details_cmd.go b/pkg/cmd/system_details_cmd.go index 79d2e6e..c48c8c1 100644 --- a/pkg/cmd/system_details_cmd.go +++ b/pkg/cmd/system_details_cmd.go @@ -87,7 +87,7 @@ func runSystemDetails() error { hostInfoManager := application.NewHostInfoManager(hostInfoService) // Generate system status information with our enhanced implementation - info, err := system.GenerateSystemStatus(hostInfoManager) + info, err := system.GenerateSystemStatus(hostInfoManager, provider.Commander) if err != nil { return fmt.Errorf("failed to generate system status: %w", err) } diff --git a/pkg/infrastructure/menu_factory.go b/pkg/infrastructure/menu_factory.go index d3bb13b..1796da7 100644 --- a/pkg/infrastructure/menu_factory.go +++ b/pkg/infrastructure/menu_factory.go @@ -51,7 +51,7 @@ func (f *MenuFactory) CreateHelpMenu() *menu.HelpMenu { func (f *MenuFactory) CreateSystemDetailsMenu() *menu.SystemDetailsMenu { // Get the host info manager from the service factory hostInfoManager := f.serviceFactory.CreateHostInfoManager() - return menu.NewSystemDetailsMenu(f.config, f.osInfo, hostInfoManager) + return menu.NewSystemDetailsMenu(f.config, f.osInfo, hostInfoManager, f.serviceFactory.provider.Commander) } // CreateMainMenu creates the main menu with all dependencies wired up @@ -83,5 +83,5 @@ func (f *MenuFactory) CreateMainMenu(versionService *version.Service) *menu.Main hostInfoManager) // Create menu with all necessary fields initialized - return menu.NewMainMenu(menuManager, f.config, f.osInfo, versionService) + return menu.NewMainMenu(menuManager, f.config, f.osInfo, versionService, f.serviceFactory.provider.Commander) } diff --git a/pkg/menu/main_menu.go b/pkg/menu/main_menu.go index 8017d29..c816bc6 100644 --- a/pkg/menu/main_menu.go +++ b/pkg/menu/main_menu.go @@ -7,6 +7,7 @@ import ( "github.com/abbott/hardn/pkg/application" "github.com/abbott/hardn/pkg/config" "github.com/abbott/hardn/pkg/domain/model" + "github.com/abbott/hardn/pkg/interfaces" "github.com/abbott/hardn/pkg/osdetect" "github.com/abbott/hardn/pkg/security" "github.com/abbott/hardn/pkg/style" @@ -19,6 +20,7 @@ type MainMenu struct { menuManager *application.MenuManager config *config.Config osInfo *osdetect.OSInfo + commander interfaces.Commander // Version service for update checks versionService *version.Service @@ -40,12 +42,14 @@ func NewMainMenu( config *config.Config, osInfo *osdetect.OSInfo, versionService *version.Service, + commander interfaces.Commander, ) *MainMenu { return &MainMenu{ menuManager: menuManager, config: config, osInfo: osInfo, versionService: versionService, + commander: commander, } } @@ -391,7 +395,7 @@ func (m *MainMenu) ShowMainMenu(currentVersion, buildDate, gitCommit string) { utils.ClearScreen() // Get security status - securityStatus, err := security.CheckSecurityStatus(m.config, m.osInfo) + securityStatus, err := security.CheckSecurityStatus(m.config, m.osInfo, m.commander) // Create formatter for security status formatter := style.NewStatusFormatter([]string{ @@ -524,7 +528,7 @@ func (m *MainMenu) handleMenuChoice(choice string) bool { envMenu.Show() case "9": // Host Info - systemDetailsMenu := NewSystemDetailsMenu(m.config, m.osInfo, m.menuManager.GetHostInfoManager()) + systemDetailsMenu := NewSystemDetailsMenu(m.config, m.osInfo, m.menuManager.GetHostInfoManager(), m.commander) systemDetailsMenu.Show() case "10": // Logs diff --git a/pkg/menu/system_details_menu.go b/pkg/menu/system_details_menu.go index 2199114..4bb12bc 100644 --- a/pkg/menu/system_details_menu.go +++ b/pkg/menu/system_details_menu.go @@ -7,6 +7,7 @@ import ( "github.com/abbott/hardn/pkg/application" "github.com/abbott/hardn/pkg/config" + "github.com/abbott/hardn/pkg/interfaces" "github.com/abbott/hardn/pkg/osdetect" "github.com/abbott/hardn/pkg/style" "github.com/abbott/hardn/pkg/system" @@ -18,6 +19,7 @@ type SystemDetailsMenu struct { config *config.Config osInfo *osdetect.OSInfo hostInfoManager *application.HostInfoManager + commander interfaces.Commander } // NewSystemDetailsMenu creates a new SystemDetailsMenu @@ -25,11 +27,13 @@ func NewSystemDetailsMenu( config *config.Config, osInfo *osdetect.OSInfo, hostInfoManager *application.HostInfoManager, + commander interfaces.Commander, ) *SystemDetailsMenu { return &SystemDetailsMenu{ config: config, osInfo: osInfo, hostInfoManager: hostInfoManager, + commander: commander, } } @@ -38,7 +42,7 @@ func (m *SystemDetailsMenu) Show() { utils.ClearScreen() // Get detailed system information using our enhanced status package - systemInfo, err := system.GenerateSystemStatus(m.hostInfoManager) + systemInfo, err := system.GenerateSystemStatus(m.hostInfoManager, m.commander) if err != nil { fmt.Printf("\n%s Error retrieving system status: %v\n", style.Colored(style.Red, style.SymCrossMark), err) diff --git a/pkg/security/security.go b/pkg/security/security.go index 3022630..f74fcdd 100644 --- a/pkg/security/security.go +++ b/pkg/security/security.go @@ -3,16 +3,16 @@ package security import ( "fmt" "os" - "os/exec" "path/filepath" "github.com/abbott/hardn/pkg/config" + "github.com/abbott/hardn/pkg/interfaces" "github.com/abbott/hardn/pkg/logging" "github.com/abbott/hardn/pkg/osdetect" ) // SetupAppArmor installs and configures AppArmor -func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { +func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo, commander interfaces.Commander) error { if cfg.DryRun { logging.LogInfo("[DRY-RUN] Install and configure AppArmor:") if osInfo.OsType == "alpine" { @@ -31,19 +31,16 @@ func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { // Install AppArmor if osInfo.OsType == "alpine" { // Alpine installation - cmd := exec.Command("apk", "add", "apparmor") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("apk", "add", "apparmor"); err != nil { return fmt.Errorf("failed to install AppArmor on Alpine: %w", err) } // Enable AppArmor in OpenRC - rcUpdateCmd := exec.Command("rc-update", "add", "apparmor", "default") - if err := rcUpdateCmd.Run(); err != nil { + if _, err := commander.Execute("rc-update", "add", "apparmor", "default"); err != nil { logging.LogError("Failed to add AppArmor to Alpine boot services: %v", err) } - rcServiceCmd := exec.Command("rc-service", "apparmor", "start") - if err := rcServiceCmd.Run(); err != nil { + if _, err := commander.Execute("rc-service", "apparmor", "start"); err != nil { logging.LogError("Failed to start AppArmor service on Alpine: %v", err) } @@ -57,8 +54,7 @@ func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { for _, file := range files { if !file.IsDir() { profilePath := filepath.Join(profilesDir, file.Name()) - aaEnforceCmd := exec.Command("aa_enforce", profilePath) - if err := aaEnforceCmd.Run(); err != nil { + if _, err := commander.Execute("aa_enforce", profilePath); err != nil { logging.LogError("Failed to enforce AppArmor profile %s: %v", profilePath, err) } } @@ -67,14 +63,12 @@ func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { } } else { // Debian/Ubuntu installation - cmd := exec.Command("apt-get", "install", "-y", "apparmor") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("apt-get", "install", "-y", "apparmor"); err != nil { return fmt.Errorf("failed to install AppArmor on Debian/Ubuntu: %w", err) } // Apply profiles - aaEnforceCmd := exec.Command("aa-enforce", "/etc/apparmor.d/*") - if err := aaEnforceCmd.Run(); err != nil { + if _, err := commander.Execute("aa-enforce", "/etc/apparmor.d/*"); err != nil { logging.LogError("Failed to enforce AppArmor profiles with wildcard: %v", err) // Try individual profiles if wildcard fails profilesDir := "/etc/apparmor.d" @@ -86,8 +80,7 @@ func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { for _, file := range files { if !file.IsDir() { profilePath := filepath.Join(profilesDir, file.Name()) - aaEnforceCmd := exec.Command("aa-enforce", profilePath) - if err := aaEnforceCmd.Run(); err != nil { + if _, err := commander.Execute("aa-enforce", profilePath); err != nil { logging.LogError("Failed to enforce AppArmor profile %s: %v", profilePath, err) } } @@ -102,7 +95,7 @@ func SetupAppArmor(cfg *config.Config, osInfo *osdetect.OSInfo) error { } // SetupLynis installs and runs the Lynis security audit tool -func SetupLynis(cfg *config.Config, osInfo *osdetect.OSInfo) error { +func SetupLynis(cfg *config.Config, osInfo *osdetect.OSInfo, commander interfaces.Commander) error { if cfg.DryRun { logging.LogInfo("[DRY-RUN] Install and run Lynis security audit tool:") if osInfo.OsType == "alpine" { @@ -119,20 +112,17 @@ func SetupLynis(cfg *config.Config, osInfo *osdetect.OSInfo) error { // Install Lynis if osInfo.OsType == "alpine" { - cmd := exec.Command("apk", "add", "lynis") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("apk", "add", "lynis"); err != nil { return fmt.Errorf("failed to install Lynis on Alpine: %w", err) } } else { - cmd := exec.Command("apt-get", "install", "-y", "lynis") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("apt-get", "install", "-y", "lynis"); err != nil { return fmt.Errorf("failed to install Lynis on Debian/Ubuntu: %w", err) } } // Run Lynis audit - auditCmd := exec.Command("lynis", "audit", "system") - output, err := auditCmd.CombinedOutput() + output, err := commander.Execute("lynis", "audit", "system") if err != nil { return fmt.Errorf("failed to run Lynis audit: %w\nOutput: %s", err, string(output)) } diff --git a/pkg/security/status.go b/pkg/security/status.go index c295781..8a5327a 100644 --- a/pkg/security/status.go +++ b/pkg/security/status.go @@ -3,12 +3,12 @@ package security import ( "bufio" "os" - "os/exec" "strconv" "strings" "github.com/abbott/hardn/pkg/adapter/secondary" "github.com/abbott/hardn/pkg/config" + "github.com/abbott/hardn/pkg/interfaces" "github.com/abbott/hardn/pkg/osdetect" "github.com/abbott/hardn/pkg/style" ) @@ -27,26 +27,26 @@ type SecurityStatus struct { } // CheckSecurityStatus examines the system and returns the security status -func CheckSecurityStatus(cfg *config.Config, osInfo *osdetect.OSInfo) (*SecurityStatus, error) { +func CheckSecurityStatus(cfg *config.Config, osInfo *osdetect.OSInfo, commander interfaces.Commander) (*SecurityStatus, error) { status := &SecurityStatus{} // Check SSH root login status status.RootLoginEnabled = checkRootLoginEnabled(osInfo) // Check firewall status - status.FirewallEnabled, status.FirewallConfigured = checkFirewallStatus() + status.FirewallEnabled, status.FirewallConfigured = checkFirewallStatus(commander) // Check user security (non-root users with sudo) - status.SecureUsers = checkUserSecurity() + status.SecureUsers = checkUserSecurity(commander) // Check AppArmor status - status.AppArmorEnabled = checkAppArmorStatus(osInfo) + status.AppArmorEnabled = checkAppArmorStatus(osInfo, commander) // Check unattended upgrades - status.UnattendedUpgrades = checkUnattendedUpgrades(osInfo) + status.UnattendedUpgrades = checkUnattendedUpgrades(osInfo, commander) // Check sudo configuration - status.SudoConfigured = checkSudoConfiguration() + status.SudoConfigured = checkSudoConfiguration(commander) // Check SSH port configuration status.SshPortNonDefault = (cfg.SshPort != 22) @@ -91,8 +91,8 @@ func DisplaySecurityStatusWithCustomPrinter(cfg *config.Config, status *Security // Display sudo configuration if !status.SudoConfigured { indentedPrintFn(formatter.FormatWarning("Sudo", "Not Installed", "", "dark")) - // } else { - // indentedPrintFn(formatter.FormatConfigured("Sudo", "Installed", "", "dark")) + // } else { + // indentedPrintFn(formatter.FormatConfigured("Sudo", "Installed", "", "dark")) } // Display sudo method @@ -264,13 +264,12 @@ func checkRootLoginEnabled(osInfo *osdetect.OSInfo) bool { } // checkFirewallStatus checks if the firewall is enabled and properly configured -func checkFirewallStatus() (bool, bool) { +func checkFirewallStatus(commander interfaces.Commander) (bool, bool) { enabled := false configured := false // Check if UFW is installed and enabled - cmd := exec.Command("ufw", "status", "verbose") - output, err := cmd.CombinedOutput() + output, err := commander.Execute("ufw", "status", "verbose") if err == nil { statusOutput := string(output) enabled = strings.Contains(statusOutput, "Status: active") @@ -300,8 +299,7 @@ func checkFirewallStatus() (bool, bool) { // Check for iptables if UFW not found if !enabled { - iptablesCmd := exec.Command("iptables", "-L") - iptablesOutput, err := iptablesCmd.CombinedOutput() + iptablesOutput, err := commander.Execute("iptables", "-L") if err == nil { rules := strings.Count(string(iptablesOutput), "Chain") enabled = rules > 3 @@ -314,7 +312,7 @@ func checkFirewallStatus() (bool, bool) { } // checkUserSecurity checks if there are non-root users with sudo access -func checkUserSecurity() bool { +func checkUserSecurity(commander interfaces.Commander) bool { // Check /etc/sudoers.d for non-root user entries sudoersDir := "/etc/sudoers.d" if _, err := os.Stat(sudoersDir); err == nil { @@ -352,18 +350,16 @@ func checkUserSecurity() bool { // checkAppArmorStatus checks if AppArmor is enabled // checkAppArmorStatus checks if AppArmor is properly configured and enforcing -func checkAppArmorStatus(osInfo *osdetect.OSInfo) bool { +func checkAppArmorStatus(osInfo *osdetect.OSInfo, commander interfaces.Commander) bool { // If Alpine, check if AppArmor is installed, enabled, and has profiles if osInfo.OsType == "alpine" { // Check if AppArmor package is installed - cmd := exec.Command("apk", "info", "-e", "apparmor") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("apk", "info", "-e", "apparmor"); err != nil { return false } // Check if AppArmor is in runlevel - rcCmd := exec.Command("rc-status", "default") - output, err := rcCmd.CombinedOutput() + output, err := commander.Execute("rc-status", "default") if err != nil { return false } @@ -373,8 +369,7 @@ func checkAppArmorStatus(osInfo *osdetect.OSInfo) bool { } // Check if AppArmor is running and has profiles loaded - statusCmd := exec.Command("aa-status") - statusOutput, err := statusCmd.CombinedOutput() + statusOutput, err := commander.Execute("aa-status") if err != nil { return false } @@ -389,8 +384,7 @@ func checkAppArmorStatus(osInfo *osdetect.OSInfo) bool { return !strings.Contains(statusText, "0 profiles are in enforce mode") } else { // For Debian/Ubuntu, check AppArmor status - cmd := exec.Command("aa-status") - output, err := cmd.CombinedOutput() + output, err := commander.Execute("aa-status") if err != nil { return false } @@ -412,7 +406,7 @@ func checkAppArmorStatus(osInfo *osdetect.OSInfo) bool { } // checkUnattendedUpgrades checks if unattended upgrades are configured -func checkUnattendedUpgrades(osInfo *osdetect.OSInfo) bool { +func checkUnattendedUpgrades(osInfo *osdetect.OSInfo, commander interfaces.Commander) bool { if osInfo.OsType == "alpine" { // Check for daily cron job if _, err := os.Stat("/etc/periodic/daily/apk-upgrade"); err == nil { @@ -421,14 +415,12 @@ func checkUnattendedUpgrades(osInfo *osdetect.OSInfo) bool { return false } else { // Check for unattended-upgrades package and configuration - cmd := exec.Command("dpkg", "-l", "unattended-upgrades") - if err := cmd.Run(); err != nil { + if _, err := commander.Execute("dpkg", "-l", "unattended-upgrades"); err != nil { return false } // Check if service is enabled - svcCmd := exec.Command("systemctl", "is-enabled", "unattended-upgrades") - if err := svcCmd.Run(); err != nil { + if _, err := commander.Execute("systemctl", "is-enabled", "unattended-upgrades"); err != nil { return false } @@ -437,10 +429,9 @@ func checkUnattendedUpgrades(osInfo *osdetect.OSInfo) bool { } // checkSudoConfiguration checks if sudo is configured securely -func checkSudoConfiguration() bool { +func checkSudoConfiguration(commander interfaces.Commander) bool { // Check if sudo is installed - sudoCmd := exec.Command("which", "sudo") - if err := sudoCmd.Run(); err != nil { + if _, err := commander.Execute("which", "sudo"); err != nil { return false } diff --git a/pkg/security/status_test.go b/pkg/security/status_test.go new file mode 100644 index 0000000..c56e8e2 --- /dev/null +++ b/pkg/security/status_test.go @@ -0,0 +1,19 @@ +package security + +import ( + "testing" + + "github.com/abbott/hardn/pkg/interfaces" +) + +func TestCheckFirewallStatus_Command(t *testing.T) { + mockCmd := interfaces.NewMockCommander() + mockCmd.CommandOutputs["ufw status verbose"] = []byte("Status: active\nDefault: deny (incoming) allow (outgoing)\nALLOW IN 22/tcp") + enabled, configured := checkFirewallStatus(mockCmd) + if !enabled || !configured { + t.Fatalf("unexpected status: %v %v", enabled, configured) + } + if len(mockCmd.ExecutedCommands) != 1 || mockCmd.ExecutedCommands[0] != "ufw status verbose" { + t.Errorf("unexpected commands: %v", mockCmd.ExecutedCommands) + } +} diff --git a/pkg/system/collectors.go b/pkg/system/collectors.go index 8c83f71..2ac4d9b 100644 --- a/pkg/system/collectors.go +++ b/pkg/system/collectors.go @@ -37,7 +37,7 @@ func (m *SystemDetails) collectOSInfo(hostInfoManager *application.HostInfoManag m.Kernel = "Linux " + hostInfo.KernelInfo } else { // Fallback to direct command - kernelInfo, err := exec.Command("uname", "-r").Output() + kernelInfo, err := m.commander.Execute("uname", "-r") if err == nil { m.Kernel = "Linux " + strings.TrimSpace(string(kernelInfo)) } @@ -75,8 +75,7 @@ func (m *SystemDetails) collectOSInfo(hostInfoManager *application.HostInfoManag m.UptimeLongFormat = hostInfoManager.FormatUptime(m.Uptime) // Use the manager's formatter } else { // Fallback to old implementation if necessary - hostInfoCmd := exec.Command("uptime") - hostInfoOutput, err := hostInfoCmd.Output() + hostInfoOutput, err := m.commander.Execute("uptime") if err == nil { // Parse uptime output - this is just a fallback, so simple parsing uptimeStr := strings.TrimSpace(string(hostInfoOutput)) @@ -207,8 +206,7 @@ func (m *SystemDetails) collectCPUInfo(hostInfoManager *application.HostInfoMana cpuInfo, err := cpu.Info() if err != nil { // Further fallback to direct command - cmd := exec.Command("cat", "/proc/cpuinfo") - output, err := cmd.Output() + output, err := m.commander.Execute("cat", "/proc/cpuinfo") if err != nil { // Just set a placeholder if all methods fail m.CPUModel = "Unknown CPU" @@ -233,8 +231,7 @@ func (m *SystemDetails) collectCPUInfo(hostInfoManager *application.HostInfoMana } // Get CPU cores count - cmd := exec.Command("nproc") - output, err := cmd.Output() + output, err := m.commander.Execute("nproc") if err == nil { cores, err := strconv.Atoi(strings.TrimSpace(string(output))) if err == nil { @@ -254,8 +251,7 @@ func (m *SystemDetails) collectCPUInfo(hostInfoManager *application.HostInfoMana } // Check for hypervisor using lscpu - hypervisorCmd := exec.Command("lscpu") - hypervisorOutput, err := hypervisorCmd.Output() + hypervisorOutput, err := m.commander.Execute("lscpu") if err == nil { scanner := bufio.NewScanner(strings.NewReader(string(hypervisorOutput))) for scanner.Scan() { @@ -334,8 +330,7 @@ func (m *SystemDetails) collectMemoryInfo(hostInfoManager *application.HostInfoM } } else { // Fallback to direct command if Host Info service fails - cmd := exec.Command("free", "-b") - output, err := cmd.Output() + output, err := m.commander.Execute("free", "-b") if err != nil { return fmt.Errorf("failed to get memory info: %w", err) } @@ -382,14 +377,13 @@ func (m *SystemDetails) collectDiskInfo(hostInfoManager *application.HostInfoMan // First check if ZFS is present regardless of Host Info if _, err := exec.LookPath("zfs"); err == nil { // Try to get ZFS information - if out, err := exec.Command("zpool", "status", "-x").Output(); err == nil { + if out, err := m.commander.Execute("zpool", "status", "-x"); err == nil { if strings.Contains(string(out), "is healthy") { m.ZFSPresent = true m.ZFSHealth = "HEALTH O.K." // Get ZFS filesystem usage - cmd := exec.Command("zfs", "get", "-Hp", "available", m.ZFSFilesystem) - out, err := cmd.Output() + out, err := m.commander.Execute("zfs", "get", "-Hp", "available", m.ZFSFilesystem) if err == nil { fields := strings.Fields(string(out)) if len(fields) >= 3 { @@ -399,8 +393,7 @@ func (m *SystemDetails) collectDiskInfo(hostInfoManager *application.HostInfoMan } } - cmd = exec.Command("zfs", "get", "-Hp", "used", m.ZFSFilesystem) - out, err = cmd.Output() + out, err = m.commander.Execute("zfs", "get", "-Hp", "used", m.ZFSFilesystem) if err == nil { fields := strings.Fields(string(out)) if len(fields) >= 3 { @@ -445,8 +438,7 @@ func (m *SystemDetails) collectDiskInfo(hostInfoManager *application.HostInfoMan usage, err := disk.Usage(m.RootPartition) if err != nil { // Last resort: use df command - cmd := exec.Command("df", "-k", m.RootPartition) - output, cmdErr := cmd.Output() + output, cmdErr := m.commander.Execute("df", "-k", m.RootPartition) if cmdErr != nil { return fmt.Errorf("failed to get disk info: %w", err) } diff --git a/pkg/system/details.go b/pkg/system/details.go index 9b4e91b..eb1c4d9 100644 --- a/pkg/system/details.go +++ b/pkg/system/details.go @@ -9,12 +9,14 @@ import ( "github.com/abbott/hardn/pkg/application" "github.com/abbott/hardn/pkg/domain/model" domainports "github.com/abbott/hardn/pkg/domain/ports/secondary" + "github.com/abbott/hardn/pkg/interfaces" ) // SystemDetails represents the complete system information type SystemDetails struct { // User login port for retrieving login information userLoginPort domainports.UserLoginPort + commander interfaces.Commander // System info OSName string OSVersion string @@ -80,11 +82,12 @@ type SystemDetails struct { } // GenerateSystemStatus collects system information and returns a SystemDetails struct -func GenerateSystemStatus(hostInfoManager *application.HostInfoManager) (*SystemDetails, error) { +func GenerateSystemStatus(hostInfoManager *application.HostInfoManager, commander interfaces.Commander) (*SystemDetails, error) { info := &SystemDetails{ ZFSFilesystem: "zroot/ROOT/os", // Default ZFS filesystem RootPartition: "/", // Default root partition userLoginPort: secondary.NewLastlogCommandAdapter(), // Use lastlog adapter + commander: commander, } // Collect all system information diff --git a/pkg/system/system_test.go b/pkg/system/system_test.go new file mode 100644 index 0000000..f0dfc9d --- /dev/null +++ b/pkg/system/system_test.go @@ -0,0 +1,38 @@ +package system + +import ( + "testing" + "time" + + "github.com/abbott/hardn/pkg/application" + "github.com/abbott/hardn/pkg/domain/model" + "github.com/abbott/hardn/pkg/interfaces" +) + +type stubHostInfoService struct{} + +func (stubHostInfoService) GetHostInfo() (*model.HostInfo, error) { return &model.HostInfo{}, nil } +func (stubHostInfoService) GetIPAddresses() ([]string, error) { return nil, nil } +func (stubHostInfoService) GetDNSServers() ([]string, error) { return nil, nil } +func (stubHostInfoService) GetHostname() (string, string, error) { return "", "", nil } +func (stubHostInfoService) GetNonSystemUsers() ([]model.User, error) { return nil, nil } +func (stubHostInfoService) GetNonSystemGroups() ([]string, error) { return nil, nil } +func (stubHostInfoService) GetUptime() (time.Duration, error) { return 0, nil } + +func TestCollectOSInfo_UsesCommander(t *testing.T) { + mockCmd := interfaces.NewMockCommander() + hostMgr := application.NewHostInfoManager(stubHostInfoService{}) + sd := &SystemDetails{commander: mockCmd} + if err := sd.collectOSInfo(hostMgr); err != nil { + t.Fatalf("err: %v", err) + } + found := false + for _, cmd := range mockCmd.ExecutedCommands { + if cmd == "uname -r" { + found = true + } + } + if !found { + t.Errorf("uname not executed: %v", mockCmd.ExecutedCommands) + } +} diff --git a/pkg/system/user_info.go b/pkg/system/user_info.go index 6387d61..5eb1931 100644 --- a/pkg/system/user_info.go +++ b/pkg/system/user_info.go @@ -5,12 +5,12 @@ import ( "bufio" "fmt" "os" - "os/exec" "strconv" "strings" "github.com/abbott/hardn/pkg/application" "github.com/abbott/hardn/pkg/domain/model" + "github.com/abbott/hardn/pkg/interfaces" ) // collectUserInfo gathers non-system user information @@ -62,7 +62,7 @@ func getNonSystemUsers() ([]model.User, error) { } // Check if user has sudo access (either in sudo group or in sudoers file) - hasSudo := checkSudoAccess(username) + hasSudo := checkSudoAccess(username, m.commander) users = append(users, model.User{ Username: username, @@ -75,19 +75,17 @@ func getNonSystemUsers() ([]model.User, error) { } // checkSudoAccess checks if a user has sudo access -func checkSudoAccess(username string) bool { +func checkSudoAccess(username string, commander interfaces.Commander) bool { // Check if user is in sudo/wheel/admin group for _, group := range []string{"sudo", "wheel", "admin"} { - cmd := exec.Command("groups", username) - output, err := cmd.Output() + output, err := commander.Execute("groups", username) if err == nil && strings.Contains(string(output), group) { return true } } // Check sudoers file - cmd := exec.Command("sudo", "-l", "-U", username) - output, err := cmd.Output() + output, err := commander.Execute("sudo", "-l", "-U", username) if err == nil && !strings.Contains(string(output), "not allowed to run sudo") { return true } diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index dcd4a09..f8189d8 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -3,16 +3,16 @@ package updates import ( "fmt" "os" - "os/exec" "strings" "github.com/abbott/hardn/pkg/config" + "github.com/abbott/hardn/pkg/interfaces" "github.com/abbott/hardn/pkg/logging" "github.com/abbott/hardn/pkg/osdetect" ) // SetupUnattendedUpgrades configures automatic system updates -func SetupUnattendedUpgrades(cfg *config.Config, osInfo *osdetect.OSInfo) error { +func SetupUnattendedUpgrades(cfg *config.Config, osInfo *osdetect.OSInfo, commander interfaces.Commander) error { if cfg.DryRun { logging.LogInfo("[DRY-RUN] Configure automatic security updates:") if osInfo.OsType == "alpine" { @@ -47,21 +47,18 @@ apk update && apk upgrade --available } // Make sure crond is running - rcUpdateCmd := exec.Command("rc-update", "add", "crond", "default") - if err := rcUpdateCmd.Run(); err != nil { + if _, err := commander.Execute("rc-update", "add", "crond", "default"); err != nil { logging.LogError("Failed to add crond to Alpine boot services: %v", err) } - rcServiceCmd := exec.Command("rc-service", "crond", "start") - if err := rcServiceCmd.Run(); err != nil { + if _, err := commander.Execute("rc-service", "crond", "start"); err != nil { logging.LogError("Failed to start crond service on Alpine: %v", err) } logging.LogSuccess("Alpine periodic updates configured") } else { // Install unattended-upgrades on Debian/Ubuntu - installCmd := exec.Command("apt-get", "install", "-y", "unattended-upgrades") - if err := installCmd.Run(); err != nil { + if _, err := commander.Execute("apt-get", "install", "-y", "unattended-upgrades"); err != nil { return fmt.Errorf("failed to install unattended-upgrades package: %w", err) } @@ -70,23 +67,20 @@ apk update && apk upgrade --available os.Setenv("DEBIAN_FRONTEND", "noninteractive") // Use debconf-set-selections to configure unattended-upgrades - debconfCmd := exec.Command("debconf-set-selections") - debconfCmd.Stdin = strings.NewReader(`unattended-upgrades unattended-upgrades/enable_auto_updates boolean true + debconfCmdInput := `unattended-upgrades unattended-upgrades/enable_auto_updates boolean true unattended-upgrades unattended-upgrades/origins_pattern string origin=Debian,codename=${distro_codename},label=Debian-Security -`) - if err := debconfCmd.Run(); err != nil { +` + if _, err := commander.ExecuteWithInput(debconfCmdInput, "debconf-set-selections"); err != nil { logging.LogError("Failed to set unattended-upgrades preferences: %v", err) } // Run dpkg-reconfigure - reconfigureCmd := exec.Command("dpkg-reconfigure", "-f", "noninteractive", "unattended-upgrades") - if err := reconfigureCmd.Run(); err != nil { + if _, err := commander.Execute("dpkg-reconfigure", "-f", "noninteractive", "unattended-upgrades"); err != nil { return fmt.Errorf("failed to reconfigure unattended-upgrades: %w", err) } // Enable the unattended-upgrades service - enableCmd := exec.Command("systemctl", "enable", "unattended-upgrades") - if err := enableCmd.Run(); err != nil { + if _, err := commander.Execute("systemctl", "enable", "unattended-upgrades"); err != nil { logging.LogError("Failed to enable unattended-upgrades service: %v", err) } @@ -97,29 +91,25 @@ unattended-upgrades unattended-upgrades/origins_pattern string origin=Debian,cod } // UpdateSystem performs a manual system update -func UpdateSystem(osInfo *osdetect.OSInfo) error { +func UpdateSystem(osInfo *osdetect.OSInfo, commander interfaces.Commander) error { logging.LogInfo("Updating system packages...") if osInfo.OsType == "alpine" { // Alpine update - updateCmd := exec.Command("apk", "update") - if err := updateCmd.Run(); err != nil { + if _, err := commander.Execute("apk", "update"); err != nil { return fmt.Errorf("failed to update Alpine package list: %w", err) } - upgradeCmd := exec.Command("apk", "upgrade") - if err := upgradeCmd.Run(); err != nil { + if _, err := commander.Execute("apk", "upgrade"); err != nil { return fmt.Errorf("failed to upgrade Alpine packages: %w", err) } } else { // Debian/Ubuntu update - updateCmd := exec.Command("apt-get", "update") - if err := updateCmd.Run(); err != nil { + if _, err := commander.Execute("apt-get", "update"); err != nil { return fmt.Errorf("failed to update Debian/Ubuntu package list: %w", err) } - upgradeCmd := exec.Command("apt-get", "upgrade", "-y") - if err := upgradeCmd.Run(); err != nil { + if _, err := commander.Execute("apt-get", "upgrade", "-y"); err != nil { return fmt.Errorf("failed to upgrade Debian/Ubuntu packages: %w", err) } } diff --git a/pkg/updates/updates_test.go b/pkg/updates/updates_test.go new file mode 100644 index 0000000..27c2832 --- /dev/null +++ b/pkg/updates/updates_test.go @@ -0,0 +1,21 @@ +package updates + +import ( + "reflect" + "testing" + + "github.com/abbott/hardn/pkg/interfaces" + "github.com/abbott/hardn/pkg/osdetect" +) + +func TestUpdateSystem_Commands(t *testing.T) { + mockCmd := interfaces.NewMockCommander() + osInfo := &osdetect.OSInfo{OsType: "debian"} + if err := UpdateSystem(osInfo, mockCmd); err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := []string{"apt-get update", "apt-get upgrade -y"} + if !reflect.DeepEqual(mockCmd.ExecutedCommands, expected) { + t.Errorf("commands executed %v, expected %v", mockCmd.ExecutedCommands, expected) + } +}