Skip to content
Merged
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
97 changes: 6 additions & 91 deletions cmd/backup/restore-hecate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,10 @@
package backup

import (
"bufio"
"fmt"
"os"
"strings"

"github.com/CodeMonkeyCybersecurity/eos/pkg/backup"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_unix"
"github.com/CodeMonkeyCybersecurity/eos/pkg/shared"
"github.com/spf13/cobra"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"go.uber.org/zap"
)

var timestampFlag string
Expand All @@ -23,10 +15,11 @@ var RestoreCmd = &cobra.Command{
Use: "restore",
Short: "Restore configuration and files from backup",
RunE: eos_cli.Wrap(func(rc *eos_io.RuntimeContext, _ *cobra.Command, _ []string) error {
// Delegate to pkg/backup for business logic
if timestampFlag != "" {
return autoRestore(rc, timestampFlag)
return backup.AutoRestore(rc, timestampFlag)
}
return interactiveRestore(rc)
return backup.InteractiveRestore(rc, timestampFlag)
}),
}

Expand All @@ -35,83 +28,5 @@ func init() {
"Backup timestamp (YYYYMMDD-HHMMSS). Omit for interactive mode.")
}

// TODO: HELPER_REFACTOR - Move to pkg/backup or pkg/hecate/backup
// Type: Business Logic
// Related functions: interactiveRestore, restoreResource
// Dependencies: eos_io, shared, fmt, zap
// TODO: Move to pkg/backup or pkg/hecate/backup
func autoRestore(rc *eos_io.RuntimeContext, ts string) error {
resources := []struct{ prefix, dest string }{
{fmt.Sprintf("%s.%s.bak", shared.DefaultConfDir, ts), shared.DefaultConfDir},
{fmt.Sprintf("%s.%s.bak", shared.DefaultCertsDir, ts), shared.DefaultCertsDir},
{fmt.Sprintf("%s.%s.bak", shared.DefaultComposeYML, ts), shared.DefaultComposeYML},
}
rc.Log.Info("Starting automatic restore", zap.String("timestamp", ts))
for _, r := range resources {
if err := restoreResource(rc, r.prefix, r.dest); err != nil {
return err
}
}
return nil
}

// TODO: HELPER_REFACTOR - Move to pkg/backup or pkg/hecate/backup
// Type: Business Logic
// Related functions: autoRestore, restoreResource
// Dependencies: eos_io, bufio, fmt, strings, os
// TODO: Move to pkg/backup or pkg/hecate/backup
func interactiveRestore(rc *eos_io.RuntimeContext) error {
logger := otelzap.Ctx(rc.Ctx)
menu := []struct {
label, prefix, dest string
}{
{"1) Configuration", shared.DefaultConfDir + ".", shared.DefaultConfDir},
{"2) Certificates", shared.DefaultCertsDir + ".", shared.DefaultCertsDir},
{"3) Compose file", shared.DefaultComposeYML + ".", shared.DefaultComposeYML},
{"4) All resources", "", ""},
}

reader := bufio.NewReader(os.Stdin)
logger.Info("terminal prompt: Select resource to restore:")
for _, m := range menu[:3] {
logger.Info("terminal prompt:", zap.String("output", fmt.Sprintf("%v", m.label)))
}
logger.Info("terminal prompt: Enter choice (1-4): ")
choice, _ := reader.ReadString('\n')
choice = strings.TrimSpace(choice)

switch choice {
case "1", "2", "3":
idx := choice[0] - '1'
return restoreResource(rc, menu[idx].prefix, menu[idx].dest)
case "4":
// require timestamp for “all”
if timestampFlag == "" {
return fmt.Errorf("must provide --timestamp to restore all")
}
return autoRestore(rc, timestampFlag)
default:
return fmt.Errorf("invalid choice %q", choice)
}
}

// TODO: HELPER_REFACTOR - Move to pkg/backup or pkg/hecate/backup
// Type: Business Logic
// Related functions: autoRestore, interactiveRestore
// Dependencies: eos_io, eos_unix, fmt, zap
// TODO: Move to pkg/backup or pkg/hecate/backup
func restoreResource(
rc *eos_io.RuntimeContext,
backupPattern, destDir string,
) error {
backup, err := eos_unix.FindLatestBackup(backupPattern)
if err != nil {
return fmt.Errorf("find backup for %s: %w", destDir, err)
}
rc.Log.Info("Restoring", zap.String("backup", backup), zap.String("to", destDir))
if err := eos_unix.Restore(rc.Ctx, backup, destDir); err != nil {
return fmt.Errorf("restore %s: %w", destDir, err)
}
rc.Log.Info("Successfully restored", zap.String("to", destDir))
return nil
}
// All business logic has been migrated to pkg/backup/restore_hecate.go
// This file now contains only Cobra orchestration as per CLAUDE.md architecture rules
20 changes: 5 additions & 15 deletions cmd/create/hecate_dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ import (
"go.uber.org/zap"
)

// TODO: refactor
// getEnvOrDefault gets environment variable or returns default value
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

// TODO move to pkg/ to DRY up this code base but putting it with other similar functions
var (
hetznerDNSDomain string
hetznerDNSIP string
Expand Down Expand Up @@ -196,11 +186,11 @@ func runCreateHecateDNS(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []st

// Initialize Hecate client
config := &hecate.ClientConfig{
CaddyAdminAddr: getEnvOrDefault("CADDY_ADMIN_ADDR", "http://localhost:2019"),
ConsulAddr: getEnvOrDefault("CONSUL_ADDR", "localhost:8500"),
VaultAddr: getEnvOrDefault("VAULT_ADDR", fmt.Sprintf("http://localhost:%d", shared.PortVault)),
VaultToken: getEnvOrDefault("VAULT_TOKEN", ""),
TerraformWorkspace: getEnvOrDefault("TERRAFORM_WORKSPACE", "/var/lib/hecate/terraform"),
CaddyAdminAddr: shared.GetEnvOrDefault("CADDY_ADMIN_ADDR", "http://localhost:2019"),
ConsulAddr: shared.GetEnvOrDefault("CONSUL_ADDR", "localhost:8500"),
VaultAddr: shared.GetEnvOrDefault("VAULT_ADDR", fmt.Sprintf("http://localhost:%d", shared.PortVault)),
VaultToken: shared.GetEnvOrDefault("VAULT_TOKEN", ""),
TerraformWorkspace: shared.GetEnvOrDefault("TERRAFORM_WORKSPACE", "/var/lib/hecate/terraform"),
}

client, err := hecate.NewHecateClient(rc, config)
Expand Down
71 changes: 15 additions & 56 deletions cmd/update/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ package update

import (
"fmt"
"net"
"os/exec"
"regexp"

eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
"github.com/CodeMonkeyCybersecurity/eos/pkg/ldap"
"github.com/spf13/cobra"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"go.uber.org/zap"
)

// TODO move to pkg/ to DRY up this code base but putting it with other similar functions
var (
ipSAN string
dryRun bool
ldapIPSAN string
ldapDryRun bool
)

var UpdateLDAPCmd = &cobra.Command{
Expand All @@ -26,62 +21,26 @@ var UpdateLDAPCmd = &cobra.Command{
Long: `Regenerates the TLS certificate for your LDAP server, including the IP address
in the SAN field. Useful when clients (like Wazuh/Wazuh) need to connect via IP.`,
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
logger := otelzap.Ctx(rc.Ctx)
if ipSAN == "" {
// Validate required flag
if ldapIPSAN == "" {
return fmt.Errorf("--ip flag is required to set SAN IP")
}

// Validate IP address to prevent command injection
if err := validateIPAddress(ipSAN); err != nil {
return fmt.Errorf("invalid IP address: %w", err)
// Delegate to pkg/ldap for business logic
config := &ldap.RegenerateTLSCertificateConfig{
IPSAN: ldapIPSAN,
DryRun: ldapDryRun,
}

cmds := []string{
"mkdir -p /etc/ldap/certs",
fmt.Sprintf(`openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-subj "/CN=%s" \
-keyout /etc/ldap/certs/ldap.key \
-out /etc/ldap/certs/ldap.crt \
-addext "subjectAltName = IP:%s"`, ipSAN, ipSAN),
}

for _, c := range cmds {
logger.Info("terminal prompt: Executing command", zap.String("cmd", c))
if !dryRun {
cmd := exec.Command("bash", "-c", c)
cmd.Stdout = cmd.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run command: %s: %w", c, err)
}
}
}

logger.Info("terminal prompt: LDAP TLS certificate regenerated with IP SAN.")
logger.Info("terminal prompt: Path: /etc/ldap/certs/ldap.crt and ldap.key")
logger.Info("terminal prompt: 🧠 Reminder: Restart your LDAP server to use the new certificate.")

return nil
return ldap.RegenerateTLSCertificate(rc, config)
}),
}

// validateIPAddress validates that the input is a valid IP address and doesn't contain injection characters
func validateIPAddress(ip string) error {
// Check for basic IP format
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return fmt.Errorf("invalid IP address format")
}

// Additional security check: ensure no shell metacharacters
if matched, _ := regexp.MatchString(`[;&|<>$()\x00-\x1f\x7f-\x9f]`, ip); matched {
return fmt.Errorf("IP address contains forbidden characters")
}

return nil
}

func init() {
UpdateCmd.AddCommand(UpdateLDAPCmd)
UpdateLDAPCmd.Flags().StringVar(&ipSAN, "ip", "", "IP address to include in SAN")
UpdateLDAPCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show commands without executing them")
UpdateLDAPCmd.Flags().StringVar(&ldapIPSAN, "ip", "", "IP address to include in SAN")
UpdateLDAPCmd.Flags().BoolVar(&ldapDryRun, "dry-run", false, "Show commands without executing them")
}

// All business logic has been migrated to pkg/ldap/certificate.go
// This file now contains only Cobra orchestration as per CLAUDE.md architecture rules
Loading
Loading