package main import ( "bufio" "encoding/json" "flag" "fmt" "os" "os/exec" "strconv" "strings" "time" "git.sechpoint.app/customer-engineering/wallarm/internal/native" "git.sechpoint.app/customer-engineering/wallarm/internal/preflight" "git.sechpoint.app/customer-engineering/wallarm/internal/state" "git.sechpoint.app/customer-engineering/wallarm/internal/tunnel" "git.sechpoint.app/customer-engineering/wallarm/internal/ui" ) var tunnelKey string var version = "dev" func main() { ver := flag.Bool("version", false, "Show version") tun := flag.Bool("tunnel", false, "Start reverse SSH tunnel") all := flag.Bool("deploy-all", false, "Deploy all undeployed nodes from fw.conf") flag.Parse() if *ver { fmt.Println("deploy version", version) return } if *tun { startTunnel() return } if *all { deployAll() return } // Run preflight, then interactive menu result := preflight.Run() fmt.Println("═══ Wallarm Deployment Manager ═══") fmt.Printf(" version %s\n\n", version) for _, c := range result.Checks { m := "✅" if !c.Passed { m = "❌" } else if c.Warning { m = "⚠️" } fmt.Printf(" %s %s — %s\n", m, c.Name, c.Detail) } if !result.Passed { fmt.Println("\n❌ Preflight checks failed.") os.Exit(1) } fmt.Println("✅ Preflight checks passed.") fmt.Println() showMenu() } func showMenu() { reader := bufio.NewReader(os.Stdin) for { fmt.Println() fmt.Println("─── Main Menu ───") fmt.Println(" [1] Deploy a node") fmt.Println(" [2] Edit a node") fmt.Println(" [3] Show status") fmt.Println(" [4] Remove a node") fmt.Println(" [5] Remote tunnel") fmt.Println(" [6] Dashboard (TUI)") fmt.Println(" [q] Quit") fmt.Print("\nChoice: ") c, _ := reader.ReadString('\n') switch strings.TrimSpace(c) { case "1": deployMenu() case "2": editMenu() case "3": showStatus() case "4": removeMenu() case "5": startTunnel() case "6": if err := ui.Run(); err != nil { fmt.Println("TUI error:", err) } case "q", "Q": return } } } // ─── Config (fw.conf JSON) ────────────────────────────────────────── type nodeConfig struct { Name string Token string Cloud string Port string UpstreamIP string UpstreamPort string Labels string Mode string } type fwConfig struct { Nodes map[string]struct { Token string `json:"token"` Cloud string `json:"cloud"` Port string `json:"port"` UpstreamIP string `json:"upstream_ip"` UpstreamPort string `json:"upstream_port"` Labels string `json:"labels"` Mode string `json:"mode"` } `json:"nodes"` } func loadConfig() (fwConfig, error) { var cfg fwConfig data, err := os.ReadFile("/opt/fw/fw.conf") if os.IsNotExist(err) { // Fall back to legacy .env return fwConfig{}, fmt.Errorf("no fw.conf") } if err != nil { return cfg, err } if err := json.Unmarshal(data, &cfg); err != nil { return cfg, err } return cfg, nil } func loadNodes() []nodeConfig { cfg, err := loadConfig() if err != nil { // Fall back to legacy .env return loadNodesLegacy() } var nodes []nodeConfig for name, n := range cfg.Nodes { mode := n.Mode if mode == "" { mode = "monitoring" } nodes = append(nodes, nodeConfig{ Name: name, Token: n.Token, Cloud: n.Cloud, Port: n.Port, UpstreamIP: n.UpstreamIP, UpstreamPort: n.UpstreamPort, Labels: n.Labels, Mode: mode, }) } return nodes } // loadNodesLegacy reads the old WALLARM_TOKEN_srv1=... format func loadNodesLegacy() []nodeConfig { data, _ := os.ReadFile("/opt/fw/.env") env := parseEnv(string(data)) var names []string if v := env["WALLARM_NODES"]; v != "" { names = strings.Split(v, ",") } else if v := env["WALLARM_NODE"]; v != "" { names = []string{v} } var nodes []nodeConfig for _, n := range names { n = strings.TrimSpace(n) if n == "" { continue } nodes = append(nodes, nodeConfig{ Name: n, Token: env["WALLARM_TOKEN_"+n], Cloud: env["WALLARM_CLOUD_"+n], Port: env["WALLARM_PORT_"+n], UpstreamIP: env["WALLARM_UPSTREAM_IP_"+n], UpstreamPort: env["WALLARM_UPSTREAM_PORT_"+n], Labels: env["WALLARM_LABELS_"+n], Mode: env["WALLARM_MODE_"+n], }) } if len(nodes) == 0 && env["WALLARM_TOKEN"] != "" { nodes = append(nodes, nodeConfig{ Name: env["WALLARM_NODE"], Token: env["WALLARM_TOKEN"], Cloud: env["WALLARM_CLOUD"], Port: env["WALLARM_PORT"], UpstreamIP: env["WALLARM_UPSTREAM_IP"], UpstreamPort: env["WALLARM_UPSTREAM_PORT"], Labels: env["WALLARM_LABELS"], Mode: env["WALLARM_MODE"], }) } return nodes } func isDeployed(name string) bool { s, _ := state.Load() if s == nil { return false } for _, n := range s.Nodes { if n.Name == name { return true } } return false } // ─── Deploy All ──────────────────────────────────────────────────────── func deployAll() { nodes := loadNodes() if len(nodes) == 0 { fmt.Println("No nodes configured in /opt/fw/fw.conf") return } for _, nc := range nodes { if isDeployed(nc.Name) { fmt.Printf("✓ %s already deployed\n", nc.Name) continue } fmt.Printf("\nDeploying %s...\n", nc.Name) deployOne(nc) } } // ─── Deploy ─────────────────────────────────────────────────────────── func deployMenu() { nodes := loadNodes() if len(nodes) == 0 { fmt.Println("No nodes configured in /opt/fw/.env") fmt.Println("Create one with: WALLARM_TOKEN=... WALLARM_NODE=... etc") return } s, _ := state.Load() deployed := map[string]bool{} if s != nil { for _, n := range s.Nodes { deployed[n.Name] = true } } fmt.Println("\n─── Deploy Node ───") available := []int{} for i, nc := range nodes { if deployed[nc.Name] { fmt.Printf(" ✓ %s (port %s) [deployed]\n", nc.Name, nc.Port) } else { available = append(available, i) fmt.Printf(" [%d] %s (port %s)\n", len(available), nc.Name, nc.Port) } } if len(available) == 0 { fmt.Println("All nodes deployed.") return } fmt.Print("\nSelect node number (or Enter for all): ") reader := bufio.NewReader(os.Stdin) choice, _ := reader.ReadString('\n') choice = strings.TrimSpace(choice) var selected []nodeConfig if choice == "" { for _, i := range available { selected = append(selected, nodes[i]) } } else { idx, err := strconv.Atoi(choice) if err != nil || idx < 1 || idx > len(available) { fmt.Println("Invalid choice") return } selected = append(selected, nodes[available[idx-1]]) } for _, nc := range selected { fmt.Printf("\nDeploying %s...\n", nc.Name) deployOne(nc) } } func deployOne(nc nodeConfig) { if nc.Token == "" || nc.Name == "" { fmt.Printf(" Skipped: missing token or name\n") return } if nc.Port == "" { nc.Port = "8081" } if nc.UpstreamIP == "" { nc.UpstreamIP = "127.0.0.1" } if nc.UpstreamPort == "" { nc.UpstreamPort = "80" } if nc.Cloud == "" { nc.Cloud = "EU" } if nc.Labels == "" { nc.Labels = "group=" + nc.Name } // Ask for traffic mode reader := bufio.NewReader(os.Stdin) mode := nc.Mode if mode == "" { mode = "monitoring" } def := "1" switch mode { case "safe_blocking": def = "2" case "block": def = "3" case "off": def = "4" } fmt.Println("\nTraffic processing mode:") fmt.Println(" [1] monitoring — detect attacks, do not block") fmt.Println(" [2] safe_blocking — block only definitely malicious requests") fmt.Println(" [3] block — block all detected attacks") fmt.Println(" [4] off — disable traffic analysis") fmt.Printf("Choose mode [%s]: ", def) ans, _ := reader.ReadString('\n') ans = strings.TrimSpace(ans) if ans == "" { ans = def } switch ans { case "2": mode = "safe_blocking" case "3": mode = "block" case "4": mode = "off" default: mode = "monitoring" } apiHost := "api.wallarm.com" if nc.Cloud == "US" { apiHost = "us1.api.wallarm.com" } portNum, _ := strconv.Atoi(nc.UpstreamPort) node := state.Node{ Name: nc.Name, Type: "native", Address: "0.0.0.0:" + nc.Port, UpstreamIP: nc.UpstreamIP, UpstreamPort: portNum, Status: "deploying", CreatedAt: time.Now().Format(time.RFC3339), } // Save state before deploy s, _ := state.Load() if s == nil { s = &state.State{DeploymentType: "native"} } s.Nodes = append(s.Nodes, node) state.Save(s) fmt.Println("Starting deployment...") if err := native.InstallNode(node, nc.Token, apiHost, nc.Labels, mode); err != nil { fmt.Fprintf(os.Stderr, "Deployment failed: %v\n", err) return } // Update status for i := range s.Nodes { if s.Nodes[i].Name == nc.Name { s.Nodes[i].Status = "running" } } state.Save(s) fmt.Printf("✅ %s deployed.\n", nc.Name) } // ─── Remove ─────────────────────────────────────────────────────────── func removeMenu() { s, _ := state.Load() if s == nil || len(s.Nodes) == 0 { fmt.Println("No nodes deployed.") return } fmt.Println("\n─── Remove Node ───") for i, n := range s.Nodes { status := "●" out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput() if strings.TrimSpace(string(out)) != "active" { status = "○" } fmt.Printf(" [%d] %s %s %s\n", i+1, status, n.Name, n.Address) } fmt.Print("\nSelect node number: ") reader := bufio.NewReader(os.Stdin) choice, _ := reader.ReadString('\n') idx, err := strconv.Atoi(strings.TrimSpace(choice)) if err != nil || idx < 1 || idx > len(s.Nodes) { fmt.Println("Invalid choice") return } n := s.Nodes[idx-1] fmt.Printf("Removing %s...\n", n.Name) if err := native.RemoveNode(n.Name); err != nil { fmt.Println("Error:", err) return } // Update state s.Nodes = append(s.Nodes[:idx-1], s.Nodes[idx:]...) state.Save(s) fmt.Printf("✅ %s removed.\n", n.Name) } // ─── Edit ───────────────────────────────────────────────────────────── func editMenu() { s, _ := state.Load() if s == nil || len(s.Nodes) == 0 { fmt.Println("No nodes deployed.") return } fmt.Println("\n─── Edit Node ───") for i, n := range s.Nodes { out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput() status := "●" if strings.TrimSpace(string(out)) != "active" { status = "○" } fmt.Printf(" [%d] %s %s %s → %s:%d\n", i+1, status, n.Name, n.Address, n.UpstreamIP, n.UpstreamPort) } fmt.Print("\nSelect node number: ") reader := bufio.NewReader(os.Stdin) choice, _ := reader.ReadString('\n') idx, err := strconv.Atoi(strings.TrimSpace(choice)) if err != nil || idx < 1 || idx > len(s.Nodes) { fmt.Println("Invalid choice") return } n := &s.Nodes[idx-1] fmt.Println("\nCurrent config:") fmt.Printf(" Name: %s\n", n.Name) fmt.Printf(" Listen: %s\n", n.Address) fmt.Printf(" Upstream: %s:%d\n", n.UpstreamIP, n.UpstreamPort) fmt.Println("\nEnter new values (leave blank to keep current):") fmt.Print("Listen port [" + strings.TrimPrefix(n.Address, "0.0.0.0:") + "]: ") port, _ := reader.ReadString('\n') port = strings.TrimSpace(port) fmt.Printf("Upstream IP [%s]: ", n.UpstreamIP) ip, _ := reader.ReadString('\n') ip = strings.TrimSpace(ip) fmt.Printf("Upstream port [%d]: ", n.UpstreamPort) portStr, _ := reader.ReadString('\n') portStr = strings.TrimSpace(portStr) if port == "" && ip == "" && portStr == "" { fmt.Println("No changes.") return } if port != "" { n.Address = "0.0.0.0:" + port } if ip != "" { n.UpstreamIP = ip } if portStr != "" { p, _ := strconv.Atoi(portStr) if p > 0 { n.UpstreamPort = p } } // Update nginx config base := "/opt/fw/" + n.Name + "/wallarm" nginxConf := base + "/nginx/conf/nginx.conf" if _, err := os.Stat(nginxConf); err == nil { data, _ := os.ReadFile(nginxConf) newData := string(data) if port != "" { newData = strings.ReplaceAll(newData, n.Address, "0.0.0.0:"+port) } if ip != "" || portStr != "" { oldUp := fmt.Sprintf("%s:%d", n.UpstreamIP, n.UpstreamPort) newUp := fmt.Sprintf("%s:%d", n.UpstreamIP, n.UpstreamPort) newData = strings.ReplaceAll(newData, oldUp, newUp) } os.WriteFile(nginxConf, []byte(newData), 0644) } state.Save(s) fmt.Println("✅ Config updated. Restart the node to apply:") fmt.Printf(" systemctl restart wallarm-node@%s\n", n.Name) } // ─── Status ─────────────────────────────────────────────────────────── func showStatus() { s, _ := state.Load() if s == nil || len(s.Nodes) == 0 { fmt.Println("No nodes deployed.") return } fmt.Println("\n─── Node Status ───") for _, n := range s.Nodes { out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput() status := strings.TrimSpace(string(out)) marker := "●" if status != "active" { marker = "○" } fmt.Printf(" %s %s — %s — %s\n", marker, n.Name, status, n.Address) } } // ─── Tunnel ─────────────────────────────────────────────────────────── func startTunnel() { reader := bufio.NewReader(os.Stdin) fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ") h, _ := reader.ReadString('\n') h = strings.TrimSpace(h) if h == "" { h = "ssh.sechpoint.app:443" } fmt.Print("Username [wallarm-tunnel]: ") u, _ := reader.ReadString('\n') u = strings.TrimSpace(u) if u == "" { u = "wallarm-tunnel" } fmt.Print("Password or SSH key path: ") p, _ := reader.ReadString('\n') p = strings.TrimSpace(p) cfg := tunnel.Config{Jumphost: h, User: u, RemotePort: 9042, LocalSSHPort: 22} if strings.HasPrefix(p, "/") { cfg.KeyPath = p } else { cfg.Password = p } fmt.Printf("\nShare: ssh -p 9042 %s@%s\nCtrl+C to close.\n\n", u, h) tunnel.Start(cfg) } // ─── Helpers ────────────────────────────────────────────────────────── func parseEnv(data string) map[string]string { m := map[string]string{} for _, line := range strings.Split(data, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } p := strings.SplitN(line, "=", 2) if len(p) == 2 { m[strings.TrimSpace(p[0])] = strings.TrimSpace(p[1]) } } return m }