package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "regexp" "strings" "time" ) func handleCLI(args []string) bool { if len(args) == 0 { return false } arg := strings.ToLower(args[0]) switch arg { case "get-secret", "secret-get": runGetSecret(args[1:]) return true case "get-secrets", "secret-list", "secrets": runGetSecrets(args[1:]) return true case "--update", "update": runSelfUpdate(args[1:]) return true case "--reinitialize", "reinitialize", "--reinit", "reinit": runReinitialize(args[1:]) return true case "--version", "version", "-v": fmt.Println("Theta Agent " + AgentVersion) return true case "--help", "help", "-h": printUsage() return true } return false } func printUsage() { fmt.Println("Theta Agent - Unified Endpoint Management CLI") fmt.Println() fmt.Println("Usage:") fmt.Println(" theta-agent Run agent daemon in foreground") fmt.Println(" theta-agent get-secret Fetch single secret value from OpenBao") fmt.Println(" theta-agent get-secrets [flags] Fetch all host/resource secrets (flags: --json, --env)") fmt.Println(" theta-agent update Self-update binary from SSO Manager") fmt.Println(" theta-agent reinitialize [flags] Reset enrollment credentials & re-register") fmt.Println(" theta-agent version Show version info") fmt.Println() fmt.Println("Reinitialize Flags:") fmt.Println(" --join-key Supply new join key for re-enrollment") fmt.Println() } func runSelfUpdate(args []string) { configPath := "/etc/theta42/agent.yml" cm, err := NewConfigManager(configPath) if err != nil { log.Fatalf("[!] Update failed: cannot read config from %s: %v", configPath, err) } cfg := cm.Get() serverURL := strings.TrimRight(cfg.ServerURL, "/") if serverURL == "" { log.Fatalf("[!] Update failed: server_url is empty in %s", configPath) } downloadURL := fmt.Sprintf("%s/resources/theta-agent/theta-agent-linux-amd64", serverURL) log.Printf("[+] Downloading latest Theta Agent binary from %s...", downloadURL) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(downloadURL) if err != nil || resp.StatusCode != 200 { log.Fatalf("[!] Failed to download update binary from %s (HTTP %d): %v", downloadURL, resp.StatusCode, err) } defer resp.Body.Close() binPath := "/usr/local/bin/theta-agent" if selfPath, err := os.Executable(); err == nil && selfPath != "" { binPath = selfPath } tmpPath := binPath + ".tmp" out, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { log.Fatalf("[!] Cannot write binary to %s: %v", tmpPath, err) } if _, err := io.Copy(out, resp.Body); err != nil { out.Close() log.Fatalf("[!] Error writing binary update: %v", err) } out.Close() if err := os.Rename(tmpPath, binPath); err != nil { log.Fatalf("[!] Cannot replace binary at %s: %v", binPath, err) } log.Printf("[+] Binary updated successfully at %s.", binPath) exec := &SystemExecutor{} restartAffectedServices(exec) os.Exit(0) } func runReinitialize(args []string) { configPath := "/etc/theta42/agent.yml" joinKey := "" for i := 0; i < len(args); i++ { if (args[i] == "--join-key" || args[i] == "-j") && i+1 < len(args) { joinKey = args[i+1] i++ } } raw, err := os.ReadFile(configPath) if err != nil { log.Fatalf("[!] Cannot read %s: %v", configPath, err) } content := string(raw) // Clear auth_token reToken := regexp.MustCompile(`(?m)^auth_token:.*$`) content = reToken.ReplaceAllString(content, `auth_token: ""`) if joinKey != "" { reKey := regexp.MustCompile(`(?m)^join_key:.*$`) if reKey.MatchString(content) { content = reKey.ReplaceAllString(content, fmt.Sprintf(`join_key: "%s"`, joinKey)) } else { content += fmt.Sprintf("\njoin_key: \"%s\"\n", joinKey) } } if err := os.WriteFile(configPath, []byte(content), 0600); err != nil { log.Fatalf("[!] Failed to update %s: %v", configPath, err) } log.Printf("[+] Cleared token in %s and reset enrollment status.", configPath) exec := &SystemExecutor{} restartAffectedServices(exec) os.Exit(0) } func restartAffectedServices(exec Executor) { log.Printf("[+] Restarting theta-agent service...") _, _ = exec.Execute("systemctl", "restart", "theta-agent") if _, err := exec.Execute("systemctl", "is-active", "sssd"); err == nil { log.Printf("[+] Restarting sssd service...") _, _ = exec.Execute("systemctl", "restart", "sssd") } if _, err := exec.Execute("systemctl", "is-active", "sshd"); err == nil { log.Printf("[+] Reloading sshd service...") _, _ = exec.Execute("systemctl", "reload", "sshd") } else if _, err := exec.Execute("systemctl", "is-active", "ssh"); err == nil { log.Printf("[+] Reloading ssh service...") _, _ = exec.Execute("systemctl", "reload", "ssh") } } func runGetSecret(args []string) { if len(args) < 1 { fmt.Fprintf(os.Stderr, "[!] Error: secret key name required (e.g. theta-agent get-secret DB_PASSWORD)\n") os.Exit(1) } key := args[0] secrets, err := fetchAgentSecrets() if err != nil { fmt.Fprintf(os.Stderr, "[!] Error fetching secrets: %v\n", err) os.Exit(1) } val, exists := secrets[key] if !exists { fmt.Fprintf(os.Stderr, "[!] Error: secret '%s' not found for this host/resource\n", key) os.Exit(1) } // Print raw secret value to stdout without trailing newline fmt.Print(val) os.Exit(0) } func runGetSecrets(args []string) { jsonMode := false envMode := false for _, arg := range args { if arg == "--json" { jsonMode = true } else if arg == "--env" { envMode = true } } secrets, err := fetchAgentSecrets() if err != nil { fmt.Fprintf(os.Stderr, "[!] Error fetching secrets: %v\n", err) os.Exit(1) } if jsonMode { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(secrets); err != nil { fmt.Fprintf(os.Stderr, "[!] JSON encode error: %v\n", err) os.Exit(1) } os.Exit(0) } if envMode { for k, v := range secrets { escaped := strings.ReplaceAll(v, `"`, `\"`) fmt.Printf("%s=\"%s\"\n", k, escaped) } os.Exit(0) } if len(secrets) == 0 { fmt.Println("No secrets configured for this host/resource.") os.Exit(0) } fmt.Printf("%-30s %s\n", "SECRET KEY", "VALUE STATUS") fmt.Println(strings.Repeat("-", 60)) for k, v := range secrets { status := fmt.Sprintf("Configured (%d chars)", len(v)) fmt.Printf("%-30s %s\n", k, status) } os.Exit(0) } func fetchAgentSecrets() (map[string]string, error) { configPath := "/etc/theta42/agent.yml" cm, err := NewConfigManager(configPath) if err != nil { return nil, fmt.Errorf("cannot read config %s: %w", configPath, err) } cfg := cm.Get() serverURL := strings.TrimRight(cfg.ServerURL, "/") if serverURL == "" { return nil, fmt.Errorf("server_url is empty in %s", configPath) } token := cfg.AuthToken if token == "" { return nil, fmt.Errorf("agent is not enrolled (auth_token empty in %s)", configPath) } reqBody, _ := json.Marshal(map[string]interface{}{}) url := fmt.Sprintf("%s/api/v1/agent/secrets", serverURL) req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes)) } var resData struct { Status string `json:"status"` Secrets map[string]map[string]interface{} `json:"secrets"` } if err := json.NewDecoder(resp.Body).Decode(&resData); err != nil { return nil, fmt.Errorf("failed to decode JSON response: %w", err) } mergedSecrets := make(map[string]string) for _, pathMap := range resData.Secrets { for k, v := range pathMap { if strV, ok := v.(string); ok { mergedSecrets[k] = strV } else if v != nil { mergedSecrets[k] = fmt.Sprintf("%v", v) } } } return mergedSecrets, nil }