diff --git a/Makefile b/Makefile deleted file mode 100644 index a308e38..0000000 --- a/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -.PHONY: all linux-amd64 linux-arm64 clean test release - -BINARY := deploy -VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev") -LDFLAGS := -s -w -X main.version=$(VERSION) -# Embed tunnel key at build time: make TUNNEL_KEY=~/.wallarm/tunnel_key -ifdef TUNNEL_KEY - LDFLAGS += -X main.tunnelKey=$(shell cat $(TUNNEL_KEY)) -endif - -all: linux-amd64 linux-arm64 - -linux-amd64: - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/deploy/ - upx --best --lzma $(BINARY)-linux-amd64 -o $(BINARY)-linux-amd64.tmp 2>/dev/null && mv $(BINARY)-linux-amd64.tmp $(BINARY)-linux-amd64 || true - -linux-arm64: - GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/deploy/ - upx --best --lzma $(BINARY)-linux-arm64 -o $(BINARY)-linux-arm64.tmp 2>/dev/null && mv $(BINARY)-linux-arm64.tmp $(BINARY)-linux-arm64 || true - -clean: - rm -f $(BINARY) $(BINARY)-linux-* - -test: - go test ./internal/... - -# Build and prepare for Gitea release. -# Usage: make release -# Then upload wallarm-linux-amd64 and wallarm-linux-arm64 as release assets. -release: clean all - @echo "Release binaries built:" - @ls -lh wallarm-linux-* - @echo "" - @echo "Next: Create a Gitea release and upload these binaries as assets." - @echo " Tag: v$(VERSION)" - diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go deleted file mode 100644 index e902b36..0000000 --- a/cmd/deploy/main.go +++ /dev/null @@ -1,522 +0,0 @@ -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 -} diff --git a/go.mod b/go.mod deleted file mode 100644 index eb54528..0000000 --- a/go.mod +++ /dev/null @@ -1,38 +0,0 @@ -module git.sechpoint.app/customer-engineering/wallarm - -go 1.24.0 - -toolchain go1.24.4 - -require ( - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/huh v1.0.0 - github.com/charmbracelet/lipgloss v1.1.0 - golang.org/x/crypto v0.36.0 -) - -require ( - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.23.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index fe4e598..0000000 --- a/go.sum +++ /dev/null @@ -1,77 +0,0 @@ -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= -github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= -github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= -github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= -github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= -github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= -github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= -github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= -github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= -github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= -github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= diff --git a/internal/native/native.go b/internal/native/native.go deleted file mode 100644 index d3473cf..0000000 --- a/internal/native/native.go +++ /dev/null @@ -1,288 +0,0 @@ -package native - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "git.sechpoint.app/customer-engineering/wallarm/internal/state" -) - -const ( - BaseDir = "/opt/fw" - DeployTo = "/opt/wallarm" - Symlink = "/opt/wallarm" -) - -func installerURL() string { - if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" { - return u - } - return "https://storage.googleapis.com/meganode_storage/6.12/wallarm-6.12.5.x86_64-glibc.sh" -} - -func InstallNode(node state.Node, apiToken, apiHost, labels, mode string) error { - if apiToken == "" { - return fmt.Errorf("API token required") - } - _ = labels - - instanceDir := filepath.Join(BaseDir, node.Name, "wallarm") - installerPath := filepath.Join(BaseDir, "wallarm-aio.sh") - - // 1. Prepare clean /opt/wallarm for this deployment - os.RemoveAll(DeployTo) - os.MkdirAll(DeployTo, 0755) - - // 2. Kill any existing process on the target port, then start per-instance NGINX - port := "80" - if idx := strings.LastIndex(node.Address, ":"); idx != -1 { - port = node.Address[idx+1:] - } - killPort(port) - copyNginx(DeployTo) - - // 3. Download AIO once - if _, err := os.Stat(installerPath); os.IsNotExist(err) { - fmt.Printf("[%s] Downloading installer...\n", node.Name) - url := installerURL() - var cmd *exec.Cmd - if _, err := exec.LookPath("curl"); err == nil { - cmd = exec.Command("curl", "-fsSL", "-o", installerPath, url) - } else { - cmd = exec.Command("wget", "-q", "-O", installerPath, url) - } - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("download: %w\n%s", err, string(out)) - } - os.Chmod(installerPath, 0755) - } - - // 4. Extract AIO to /opt/wallarm - fmt.Printf("[%s] Extracting...\n", node.Name) - cmd := exec.Command("bash", installerPath, "--noexec", "--keep", "--target", DeployTo, "--noprogress", "--accept") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("extract: %w\n%s", err, string(out)) - } - - // 5. Start NGINX (modules now available after extraction) - startNginx(DeployTo, node, port, mode) - - // 6. Load Wallarm NGINX module + register node - fmt.Printf("[%s] Configuring NGINX module...\n", node.Name) - exec.Command("bash", filepath.Join(DeployTo, "pick-module.sh")).Run() - - // 7. Kill stale wcli lock (from other instances) then register - exec.Command("rm", "-f", "/tmp/.wallarm.wcli.lock").Run() - fmt.Printf("[%s] Registering node (this may take 30-60s)...\n", node.Name) - registerCmd := exec.Command(filepath.Join(DeployTo, "register-node"), - "job:register", - "-token", apiToken, - "-host", apiHost, - ) - registerCmd.Dir = DeployTo - devNull, _ := os.Open(os.DevNull) - if devNull != nil { - registerCmd.Stdin = devNull - defer devNull.Close() - } - // Set environment from env.list - envData, _ := os.ReadFile(filepath.Join(DeployTo, "env.list")) - for _, line := range strings.Split(string(envData), "\n") { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - registerCmd.Env = append(registerCmd.Env, line) - } - registerCmd.Env = append(registerCmd.Env, os.Environ()...) - regLog, _ := os.Create(filepath.Join(DeployTo, "register.log")) - if regLog != nil { - registerCmd.Stdout = regLog - registerCmd.Stderr = regLog - } - regErr := registerCmd.Run() - if regLog != nil { - regLog.Close() - } - if regErr != nil { - data, _ := os.ReadFile(filepath.Join(DeployTo, "register.log")) - return fmt.Errorf("register: %w\n%s", regErr, string(data)) - } - - // 8. Move /opt/wallarm → instanceDir (copy+delete for cross-fs safety) - exec.Command("pkill", "-9", "-f", filepath.Join(DeployTo, "nginx")).Run() - os.MkdirAll(filepath.Join(BaseDir, node.Name), 0755) - os.RemoveAll(instanceDir) - if err := os.Rename(DeployTo, instanceDir); err != nil { - // Cross-filesystem fallback - exec.Command("cp", "-a", DeployTo, instanceDir).Run() - os.RemoveAll(DeployTo) - } - - // 9. Patch paths + fix ELF binaries + systemd - svc := "wallarm-node@" + node.Name - exec.Command("systemctl", "enable", svc).Run() - exec.Command("systemctl", "start", svc).Run() - fmt.Printf("[%s] Done. Installed to %s (systemctl status %s)\n", node.Name, instanceDir, svc) - return nil -} - -func copyNginx(dir string) { - dst := filepath.Join(dir, "nginx", "sbin", "nginx") - if _, err := os.Stat(dst); err == nil { - return - } - os.MkdirAll(filepath.Dir(dst), 0755) - if path, err := exec.LookPath("nginx"); err == nil { - exec.Command("cp", path, dst).Run() - return - } - for _, pm := range [][]string{ - {"apt-get", "install", "-y", "-qq", "nginx"}, - {"yum", "install", "-y", "-q", "nginx"}, - } { - if _, err := exec.LookPath(pm[0]); err == nil { - exec.Command(pm[0], pm[1:]...).Run() - if path, err := exec.LookPath("nginx"); err == nil { - exec.Command("cp", path, dst).Run() - return - } - } - } -} - -func cloudFromHost(host string) string { - if strings.Contains(host, "us1") { - return "US" - } - return "EU" -} - -func CreateNodesDir() error { return os.MkdirAll(BaseDir, 0755) } - -func GenerateSystemdTemplate() error { - tmpl := `/etc/systemd/system/wallarm-node@.service` - content := fmt.Sprintf(`[Unit] -Description=Wallarm Node - %%i -After=network.target - -[Service] -Type=simple -WorkingDirectory=%s/%%i/wallarm -EnvironmentFile=-%s/%%i/wallarm/env.list -# Bind mount instance directory as /opt/wallarm (isolated per instance) -ExecStartPre=/bin/ln -sf %s/%%i/wallarm /opt/wallarm -ExecStartPre=%s/%%i/wallarm/nginx/sbin/nginx -c %s/%%i/wallarm/nginx/conf/nginx.conf -ExecStartPre=/bin/sleep 1 -ExecStart=%s/%%i/wallarm/usr/bin/python3.10 %s/%%i/wallarm/usr/bin/supervisord -c %s/%%i/wallarm/etc/supervisord.conf -ExecStop=%s/%%i/wallarm/usr/bin/python3.10 %s/%%i/wallarm/usr/bin/supervisord -c %s/%%i/wallarm/etc/supervisord.conf shutdown -ExecStopPost= -Restart=on-failure -RestartSec=5 -User=root - -[Install] -WantedBy=multi-user.target -`, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir) - os.WriteFile(tmpl, []byte(content), 0644) - exec.Command("systemctl", "daemon-reload").Run() - return nil -} - -func RemoveNode(name string) error { - exec.Command("systemctl", "stop", "wallarm-node@"+name).Run() - exec.Command("systemctl", "disable", "wallarm-node@"+name).Run() - os.RemoveAll(filepath.Join(BaseDir, name)) - return nil -} - -func Status(name string) (string, error) { - out, _ := exec.Command("systemctl", "status", "wallarm-node@"+name, "--no-pager").CombinedOutput() - return string(out), nil -} - -func startNginx(dir string, node state.Node, port, mode string) { - nginxDir := filepath.Join(dir, "nginx") - confDir := filepath.Join(nginxDir, "conf") - os.MkdirAll(confDir, 0755) - confPath := filepath.Join(confDir, "nginx.conf") - upstream := fmt.Sprintf("%s:%d", node.UpstreamIP, node.UpstreamPort) - if node.UpstreamIP == "" { - upstream = "127.0.0.1:80" - } - os.WriteFile(confPath, []byte(fmt.Sprintf(` -load_module %s/modules/nginx_v1.26.3_s0ff5dffff/ngx_http_wallarm_module.so; - -worker_processes auto; -pid %s/nginx.pid; -error_log %s/error.log; -events { worker_connections 10240; } -http { - access_log %s/access.log; - wallarm_mode %s; - - server { - listen %s; - server_name _; - - location /wallarm-status { - wallarm_status on; - allow 127.0.0.0/8; - deny all; - } - - location / { - proxy_pass http://%s; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - } -} -`, dir, nginxDir, nginxDir, nginxDir, mode, port, upstream)), 0644) - - bin := filepath.Join(nginxDir, "sbin", "nginx") - cmd := exec.Command(bin, "-c", confPath, "-p", nginxDir) - cmd.Dir = nginxDir - if out, err := cmd.CombinedOutput(); err != nil { - fmt.Printf("[%s] NGINX start: %v\n%s\n", node.Name, err, string(out)) - } else { - fmt.Printf("[%s] NGINX started on port %s\n", node.Name, port) - } -} - -func killPort(port string) { - // Find and kill any process listening on the target port - out, _ := exec.Command("fuser", "-k", port+"/tcp").CombinedOutput() - if len(out) > 0 { - fmt.Printf(" Killed existing process on port %s\n", port) - } -} - -func patchPaths(dir string) { - old := "/opt/wallarm" - // Use bash to execute find+sed reliably - exec.Command("bash", "-c", - fmt.Sprintf("find %s -type f \\( -name '*.sh' -o -name '*.list' -o -name '*.conf' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \\) -exec sed -i 's|%s|%s|g' {} +", - dir, old, dir)).Run() - // Unique internal ports + wcli config-file per instance - offset := hashPort(filepath.Base(filepath.Dir(dir))) - exec.Command("bash", "-c", - fmt.Sprintf("find %s -type f \\( -name '*.yaml' -o -name '*.yml' -o -name '*.conf' \\) -exec sed -i 's|:3313|:%d|g; s|:6388|:%d|g; s|:9001|:%d|g; s|:8088|:%d|g; s|:9667|:%d|g' {} + ; "+ - "find %s -name '*.conf' -exec sed -i 's|\\(wcli run\\)$|\\1 --config-file %s/etc/wallarm/node.yaml|' {} +", - dir, 3313+offset, 6388+offset, 9001+offset, 8088+offset, 9667+offset, - dir, dir)).Run() -} - -func hashPort(s string) int { - h := 0 - for _, c := range s { - h = h*31 + int(c) - } - if h < 0 { h = -h } - return h % 500 -} - -// fixElfBinaries removed — patchelf breaks library paths. Use symlink instead. diff --git a/internal/preflight/preflight.go b/internal/preflight/preflight.go deleted file mode 100644 index 811288b..0000000 --- a/internal/preflight/preflight.go +++ /dev/null @@ -1,172 +0,0 @@ -// Package preflight runs system readiness checks before any deployment. -// It is called on every binary start and returns a structured report. -package preflight - -import ( - "fmt" - "os" - - "git.sechpoint.app/customer-engineering/wallarm/internal/shared" -) - -// Result holds the outcome of all preflight checks. -type Result struct { - Passed bool `json:"passed"` - Checks []Check `json:"checks"` - USReachable bool `json:"us_reachable"` - EUReachable bool `json:"eu_reachable"` -} - -// Check represents a single preflight check. -type Check struct { - Name string `json:"name"` - Passed bool `json:"passed"` - Detail string `json:"detail,omitempty"` - Warning bool `json:"warning,omitempty"` -} - -// Cloud endpoints (from wallarm-lib.sh) -var euEndpoints = []string{ - "api.wallarm.com:443", - "node-data0.eu1.wallarm.com:443", - "node-data1.eu1.wallarm.com:443", -} - -var usEndpoints = []string{ - "us1.api.wallarm.com:443", - "node-data0.us1.wallarm.com:443", - "node-data1.us1.wallarm.com:443", -} - -// Run executes all preflight checks and returns the result. -func Run() Result { - r := Result{Passed: true} - - // 1. Root check - if os.Geteuid() != 0 { - r.Checks = append(r.Checks, Check{ - Name: "root", Passed: false, Detail: "must run as root for package installation and system config", - }) - r.Passed = false - } else { - r.Checks = append(r.Checks, Check{Name: "root", Passed: true}) - } - - // 2. Init system - initSys := shared.InitSystem() - if initSys != "systemd" { - r.Checks = append(r.Checks, Check{ - Name: "init", Passed: false, Detail: fmt.Sprintf("requires systemd, detected: %s", initSys), - }) - r.Passed = false - } else { - r.Checks = append(r.Checks, Check{Name: "init", Passed: true, Detail: "systemd"}) - } - - // 3. Architecture - arch := shared.Arch() - supported := arch == "x86_64" || arch == "aarch64" - r.Checks = append(r.Checks, Check{ - Name: "arch", Passed: supported, Detail: arch, - }) - if !supported { - r.Passed = false - } - - // 4. OS - id, ver := shared.OSInfo() - r.Checks = append(r.Checks, Check{Name: "os", Passed: true, Detail: id + " " + ver}) - - // 5. Required commands (curl or wget) - hasDownloader := shared.CommandExists("curl") || shared.CommandExists("wget") - r.Checks = append(r.Checks, Check{Name: "downloader", Passed: hasDownloader}) - if !hasDownloader { r.Passed = false } - requiredCmds := []string{"systemctl", "sed", "mkdir", "rm"} - for _, cmd := range requiredCmds { - ok := shared.CommandExists(cmd) - r.Checks = append(r.Checks, Check{Name: "cmd:" + cmd, Passed: ok}) - if !ok { - r.Passed = false - } - } - - // 6. Installer reachability - installerOk := shared.HTTPHead("https://storage.googleapis.com/meganode_storage/") - r.Checks = append(r.Checks, Check{ - Name: "installer_reachable", Passed: installerOk, - Detail: "storage.googleapis.com", - }) - if !installerOk { - r.Checks[len(r.Checks)-1].Warning = true - } - - // 7. Cloud endpoints - r.USReachable = checkEndpoints(usEndpoints) - r.EUReachable = checkEndpoints(euEndpoints) - r.Checks = append(r.Checks, Check{ - Name: "cloud:US", Passed: r.USReachable, - Detail: fmt.Sprintf("%d/%d reachable", countReachable(usEndpoints), len(usEndpoints)), - }) - r.Checks = append(r.Checks, Check{ - Name: "cloud:EU", Passed: r.EUReachable, - Detail: fmt.Sprintf("%d/%d reachable", countReachable(euEndpoints), len(euEndpoints)), - }) - if !r.USReachable && !r.EUReachable { - r.Passed = false - } - - // 8. Disk space (>= 2GB) - free, err := shared.FreeDiskMB("/opt") - if err == nil && free < 2048 { - r.Checks = append(r.Checks, Check{ - Name: "disk", Passed: false, - Detail: fmt.Sprintf("%d MB free (need >= 2048 MB)", free), - }) - r.Passed = false - } else { - r.Checks = append(r.Checks, Check{Name: "disk", Passed: true, Detail: fmt.Sprintf("%d MB free", free)}) - } - - // 9. Memory (>= 2GB, warning only) - mem, err := shared.FreeMemoryMB() - if err == nil && mem < 2048 { - r.Checks = append(r.Checks, Check{ - Name: "memory", Passed: true, Warning: true, - Detail: fmt.Sprintf("%d MB (2GB+ recommended)", mem), - }) - } else { - r.Checks = append(r.Checks, Check{Name: "memory", Passed: true, Detail: fmt.Sprintf("%d MB", mem)}) - } - - return r -} - -func checkEndpoints(endpoints []string) bool { - for _, ep := range endpoints { - host, _ := splitHostPort(ep) - if shared.TCPConnect(host, 443) { - return true - } - } - return false -} - -func countReachable(endpoints []string) int { - n := 0 - for _, ep := range endpoints { - host, _ := splitHostPort(ep) - if shared.TCPConnect(host, 443) { - n++ - } - } - return n -} - -func splitHostPort(addr string) (string, string) { - for i := len(addr) - 1; i >= 0; i-- { - if addr[i] == ':' { - return addr[:i], addr[i+1:] - } - } - return addr, "" -} diff --git a/internal/shared/shared.go b/internal/shared/shared.go deleted file mode 100644 index 8ef7e2a..0000000 --- a/internal/shared/shared.go +++ /dev/null @@ -1,174 +0,0 @@ -// Package shared provides validation, connectivity, and system detection -// utilities ported from the bash wallarm-lib.sh library. -package shared - -import ( - "fmt" - "os" - "os/exec" - "runtime" - "strconv" - "strings" -) - -// ─── System Detection ──────────────────────────────────────────────── - -// InitSystem returns the detected init system: systemd, openrc, sysvinit, upstart, or unknown. -func InitSystem() string { - if runtime.GOOS == "darwin" { - return "darwin" - } - if _, err := exec.LookPath("systemctl"); err == nil { - return "systemd" - } - if _, err := os.Stat("/sbin/openrc-run"); err == nil { - return "openrc" - } - if _, err := os.Stat("/etc/init.d"); err == nil { - return "sysvinit" - } - if _, err := os.Stat("/sbin/upstart"); err == nil { - return "upstart" - } - return "unknown" -} - -// OSInfo returns (os_id, version_id) from /etc/os-release. -func OSInfo() (id, version string) { - data, err := os.ReadFile("/etc/os-release") - if err != nil { - return strings.ToLower(runtime.GOOS), "unknown" - } - for _, line := range strings.Split(string(data), "\n") { - if strings.HasPrefix(line, "ID=") { - id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`) - } - if strings.HasPrefix(line, "VERSION_ID=") { - version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`) - } - } - if id == "" { - id = strings.ToLower(runtime.GOOS) - } - return id, version -} - -// Arch returns the normalized architecture: x86_64, aarch64, or armhf. -func Arch() string { - switch runtime.GOARCH { - case "amd64": - return "x86_64" - case "arm64": - return "aarch64" - case "arm": - return "armhf" - default: - return runtime.GOARCH - } -} - -// ─── Validation ────────────────────────────────────────────────────── - -// ValidateIP checks whether s is a valid IPv4 address. -func ValidateIP(s string) error { - parts := strings.Split(s, ".") - if len(parts) != 4 { - return fmt.Errorf("invalid IPv4: %s", s) - } - for _, p := range parts { - n, err := strconv.Atoi(p) - if err != nil || n < 0 || n > 255 { - return fmt.Errorf("invalid IPv4 octet: %s", p) - } - } - return nil -} - -// ValidateCIDR checks whether s is a valid IPv4 address with optional /prefix. -func ValidateCIDR(s string) error { - ip := s - prefix := "" - if idx := strings.IndexByte(s, '/'); idx != -1 { - ip, prefix = s[:idx], s[idx+1:] - } - if err := ValidateIP(ip); err != nil { - return err - } - if prefix != "" { - n, err := strconv.Atoi(prefix) - if err != nil || n < 0 || n > 32 { - return fmt.Errorf("invalid CIDR prefix: %s", prefix) - } - } - return nil -} - -// CommandExists returns true if cmd is in PATH or in common system directories. -func CommandExists(cmd string) bool { - if _, err := exec.LookPath(cmd); err == nil { - return true - } - for _, dir := range []string{"/usr/sbin", "/sbin", "/usr/local/sbin", "/usr/bin", "/bin", "/usr/local/bin"} { - if _, err := os.Stat(dir + "/" + cmd); err == nil { - return true - } - } - return false -} - -// ─── Resource Checks ───────────────────────────────────────────────── - -// FreeDiskMB returns available disk space in MB for the given path. -func FreeDiskMB(path string) (int64, error) { - cmd := exec.Command("df", "-k", path) - out, err := cmd.Output() - if err != nil { - return 0, err - } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") - if len(lines) < 2 { - return 0, fmt.Errorf("unexpected df output") - } - fields := strings.Fields(lines[1]) - if len(fields) < 4 { - return 0, fmt.Errorf("unexpected df fields") - } - kb, err := strconv.ParseInt(fields[3], 10, 64) - if err != nil { - return 0, err - } - return kb / 1024, nil // convert KB to MB -} - -// FreeMemoryMB returns available memory in MB. -func FreeMemoryMB() (int64, error) { - cmd := exec.Command("free", "-m") - out, err := cmd.Output() - if err != nil { - return 0, err - } - lines := strings.Split(strings.TrimSpace(string(out)), "\n") - if len(lines) < 2 { - return 0, fmt.Errorf("unexpected free output") - } - fields := strings.Fields(lines[1]) - if len(fields) < 2 { - return 0, fmt.Errorf("unexpected free fields") - } - return strconv.ParseInt(fields[1], 10, 64) -} - -// ─── Connectivity ──────────────────────────────────────────────────── - -// TCPConnect tests whether host:port accepts a TCP connection. -func TCPConnect(host string, port int) bool { - cmd := exec.Command("timeout", "5", "bash", "-c", - fmt.Sprintf("echo >/dev/tcp/%s/%d 2>/dev/null", host, port)) - return cmd.Run() == nil -} - -// HTTPHead returns true if the URL returns a successful status. -func HTTPHead(url string) bool { - cmd := exec.Command("curl", "-fsSL", "--connect-timeout", "10", url) - return cmd.Run() == nil -} diff --git a/internal/state/state.go b/internal/state/state.go deleted file mode 100644 index 16df18e..0000000 --- a/internal/state/state.go +++ /dev/null @@ -1,79 +0,0 @@ -// Package state manages the persistent deployment state file (~/.wallarm/state.json). -package state - -import ( - "encoding/json" - "fmt" - "os" -) - -// StateDir is where wallarm stores its state (under /opt/wallarm/). -const StateDir = "/opt/fw" - -// State represents the persistent deployment state. -type State struct { - DeploymentType string `json:"deployment_type,omitempty"` // docker or native - CloudRegion string `json:"cloud_region,omitempty"` // US or EU - APIHost string `json:"api_host,omitempty"` // e.g., api.wallarm.com - APIToken string `json:"api_token,omitempty"` // Wallarm API token (sensitive) - Nodes []Node `json:"nodes"` -} - -// Node represents a single deployed Wallarm node (docker container or native systemd unit). -type Node struct { - Name string `json:"name"` - Type string `json:"type"` // docker or native - Address string `json:"address,omitempty"` // listen address for native - Port int `json:"port,omitempty"` // ingress port for docker - UpstreamIP string `json:"upstream_ip,omitempty"` // docker - UpstreamPort int `json:"upstream_port,omitempty"` // docker - Status string `json:"status"` // running, stopped, unknown - CreatedAt string `json:"created_at"` -} - -// Path returns the full path to the state file. -func Path() string { - return StateDir + "/state.json" -} - -// Load reads and parses the state file. Returns nil if the file doesn't exist. -func Load() (*State, error) { - p := Path() - data, err := os.ReadFile(p) - if os.IsNotExist(err) { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("read state: %w", err) - } - var s State - if err := json.Unmarshal(data, &s); err != nil { - return nil, fmt.Errorf("parse state: %w", err) - } - return &s, nil -} - -// Save writes the state to /opt/wallarm/state.json. -func Save(s *State) error { - p := Path() - if err := os.MkdirAll(StateDir, 0755); err != nil { - return fmt.Errorf("create state dir: %w", err) - } - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return fmt.Errorf("marshal state: %w", err) - } - if err := os.WriteFile(p, data, 0600); err != nil { - return fmt.Errorf("write state: %w", err) - } - return nil -} - -// HasDeployment returns true if a state file exists with at least one node. -func HasDeployment() bool { - s, err := Load() - if err != nil || s == nil { - return false - } - return len(s.Nodes) > 0 -} diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go deleted file mode 100644 index f23e4af..0000000 --- a/internal/tunnel/tunnel.go +++ /dev/null @@ -1,152 +0,0 @@ -// Package tunnel provides a reverse SSH tunnel over TLS:443 via a Zoraxy edge proxy. -// It opens an outbound TLS connection to the configured jumphost, authenticates -// via SSH, and establishes a reverse port forward so you can reach the target VM -// through sechpoint.app. -package tunnel - -import ( - "crypto/tls" - "fmt" - "io" - "net" - "os" - "os/signal" - "time" - - "golang.org/x/crypto/ssh" -) - -// Config holds the tunnel connection parameters. -type Config struct { - Jumphost string // e.g., "ssh.sechpoint.app:443" - RemotePort int // Port on the jumphost that forwards to target's SSH - LocalSSHPort int // SSH port on the target VM (usually 22) - User string // SSH user on the jumphost - Password string // Password auth (takes lowest priority) - KeyPath string // Path to private key for authentication - KeyBytes []byte // Raw private key bytes (takes precedence over KeyPath) -} - -// DefaultConfig returns a Config with sensible defaults. -func DefaultConfig() Config { - return Config{ - Jumphost: "ssh.sechpoint.app:443", - RemotePort: 9042, - LocalSSHPort: 22, - User: "wallarm-tunnel", - } -} - -// Start opens a reverse SSH tunnel over TLS and keeps it alive. -// It blocks until SIGINT or connection failure. -func Start(cfg Config) error { - // Build auth methods - var authMethods []ssh.AuthMethod - - if len(cfg.KeyBytes) > 0 { - signer, err := ssh.ParsePrivateKey(cfg.KeyBytes) - if err == nil { - authMethods = append(authMethods, ssh.PublicKeys(signer)) - } - } - if cfg.KeyPath != "" { - keyBytes, err := os.ReadFile(cfg.KeyPath) - if err == nil { - signer, err := ssh.ParsePrivateKey(keyBytes) - if err == nil { - authMethods = append(authMethods, ssh.PublicKeys(signer)) - } - } - } - if cfg.Password != "" { - authMethods = append(authMethods, ssh.Password(cfg.Password)) - } - - if len(authMethods) == 0 { - return fmt.Errorf("no authentication method configured (provide key or password)") - } - - sshConfig := &ssh.ClientConfig{ - User: cfg.User, - Auth: authMethods, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // trusted infrastructure - Timeout: 10 * time.Second, - } - - // TLS dial to the Zoraxy edge (port 443) - tlsConn, err := tls.Dial("tcp", cfg.Jumphost, &tls.Config{ - InsecureSkipVerify: false, - }) - if err != nil { - return fmt.Errorf("TLS dial %s: %w", cfg.Jumphost, err) - } - - // SSH over TLS - sshConn, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.Jumphost, sshConfig) - if err != nil { - tlsConn.Close() - return fmt.Errorf("SSH handshake: %w", err) - } - client := ssh.NewClient(sshConn, chans, reqs) - defer client.Close() - - // Request reverse port forward: jumphost:RemotePort -> localhost:LocalSSHPort - remoteAddr := fmt.Sprintf("0.0.0.0:%d", cfg.RemotePort) - localAddr := fmt.Sprintf("localhost:%d", cfg.LocalSSHPort) - - listener, err := client.Listen("tcp", remoteAddr) - if err != nil { - return fmt.Errorf("remote listen %s: %w", remoteAddr, err) - } - defer listener.Close() - - fmt.Printf("Tunnel established: %s -> %s\n", remoteAddr, localAddr) - fmt.Printf("Connect: ssh -p %d root@%s\n", cfg.RemotePort, cfg.Jumphost) - - // Handle incoming connections on the remote listener - go func() { - for { - remoteConn, err := listener.Accept() - if err != nil { - return - } - go forwardConnection(remoteConn, localAddr) - } - }() - - // Keep alive until signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt) - - // Heartbeat every 30s - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-sigCh: - fmt.Println("\nTunnel closed.") - return nil - case <-ticker.C: - _, _, err := client.SendRequest("keepalive@wallarm", true, nil) - if err != nil { - return fmt.Errorf("keepalive failed: %w", err) - } - } - } -} - -func forwardConnection(remoteConn net.Conn, localAddr string) { - defer remoteConn.Close() - localConn, err := net.DialTimeout("tcp", localAddr, 10*time.Second) - if err != nil { - return - } - defer localConn.Close() - - go func() { - io.Copy(localConn, remoteConn) - remoteConn.Close() - }() - io.Copy(remoteConn, localConn) -} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index e833f7f..0000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,149 +0,0 @@ -// Package ui provides the bubbletea terminal UI for wallarm: -// - Deploy choice: local deploy or remote assistance -// - Dashboard: lists existing nodes with actions -// -// Deployment logic runs in plain terminal (main.go), not inside the TUI. -package ui - -import ( - "fmt" - "strings" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "git.sechpoint.app/customer-engineering/wallarm/internal/state" -) - -var ( - titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).MarginBottom(1) - goodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) - warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("227")) - badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) - dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true) -) - -type Model struct { - state stateView - width int - height int -} - -type stateView int - -const ( - viewRoute stateView = iota - viewDeployChoice // local deploy or remote assist? - viewDashboard -) - -// Run starts the bubbletea TUI. Returns nil if user chose local deploy (exit and run form). -func Run() error { - m := Model{state: viewRoute} - p := tea.NewProgram(m) - if _, err := p.Run(); err != nil { - return err - } - return nil -} - -func (m Model) Init() tea.Cmd { return nil } - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return m, tea.Quit - case "1": - if m.state == viewDeployChoice { - return m, tea.Quit // exit TUI, main.go runs deploy form - } - case "2": - if m.state == viewDeployChoice { - return m, tea.Quit // exit TUI, main.go runs tunnel flow - } - } - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - } - - switch m.state { - case viewRoute: - if state.HasDeployment() { - m.state = viewDashboard - } else { - m.state = viewDeployChoice - } - } - return m, nil -} - -func (m Model) View() string { - switch m.state { - case viewRoute: - return "" - case viewDeployChoice: - return deployChoiceView(m) - case viewDashboard: - return dashboardView(m) - } - return "" -} - -// ─── Deploy Choice ─────────────────────────────────────────────────── - -func deployChoiceView(m Model) string { - s := titleStyle.Render("🛡️ Wallarm Setup") + "\n\n" - s += "How would you like to proceed?\n\n" - s += activeStyle.Render(" [1] Deploy locally") + "\n" - s += " Configure and deploy the Wallarm node on this server now.\n\n" - s += activeStyle.Render(" [2] Remote assistance") + "\n" - s += " Open a secure tunnel so a remote admin can deploy for you.\n\n" - s += dimStyle.Render("Press 1 or 2, q to quit") - return s -} - -// ─── Dashboard ─────────────────────────────────────────────────────── - -func dashboardView(m Model) string { - s, err := state.Load() - if err != nil || s == nil { - return badStyle.Render("Error loading state.") + "\nPress q to quit" - } - - out := titleStyle.Render("📊 Wallarm Dashboard") + "\n" - out += fmt.Sprintf("Type: %s | Cloud: %s (%s)\n", s.DeploymentType, s.CloudRegion, s.APIHost) - out += dimStyle.Render(strings.Repeat("─", 50)) + "\n\n" - - if len(s.Nodes) == 0 { - out += dimStyle.Render("No nodes deployed yet.") + "\n\n" - } else { - out += activeStyle.Render("Nodes:") + "\n" - for _, n := range s.Nodes { - marker := "●" - style := goodStyle - if n.Status != "running" { - marker = "○" - style = warnStyle - } - out += style.Render(fmt.Sprintf(" %s %s — %s", marker, n.Name, n.Status)) - if n.Address != "" { - out += dimStyle.Render(fmt.Sprintf(" (%s)", n.Address)) - } - out += "\n" - } - out += "\n" - } - - out += "Actions:\n" - out += fmt.Sprintf(" %s\n", activeStyle.Render("[a] Add node")) - out += fmt.Sprintf(" %s\n", activeStyle.Render("[c] Configure")) - out += fmt.Sprintf(" %s\n", activeStyle.Render("[r] Remove node")) - out += fmt.Sprintf(" %s\n", activeStyle.Render("[t] Start tunnel")) - out += fmt.Sprintf(" %s\n", dimStyle.Render("[q] Quit")) - - return out -} diff --git a/python/deploy.py b/python/deploy.py new file mode 100644 index 0000000..1bd922c --- /dev/null +++ b/python/deploy.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +"""Wallarm Node Manager — deploy, edit, remove, status, tunnel.""" + +import json, os, sys, time, subprocess, shutil, hashlib +from pathlib import Path + +VERSION = "1.0.0" +BASE = "/opt/fw" +STATE = f"{BASE}/state.json" +CONF = f"{BASE}/fw.conf" +AIO_URL = "https://storage.googleapis.com/meganode_storage/6.12/wallarm-6.12.5.x86_64-glibc.sh" +AIO_PATH = f"{BASE}/wallarm-aio.sh" +DEPLOY_TO = "/opt/wallarm" + +# ─── Helpers ─────────────────────────────────────────────────────────── + +def run(cmd, timeout=120, check=False): + return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) + +def run_ok(cmd): + return subprocess.run(cmd, shell=True, capture_output=True).returncode == 0 + +def hash_port(s): + return abs(hash(s)) % 500 + +def is_root(): + return os.geteuid() == 0 + +# ─── Preflight ───────────────────────────────────────────────────────── + +def preflight(): + checks = [] + ok = True + def check(name, passed, detail=""): + nonlocal ok + if not passed: ok = False + m = "✅" if passed else "❌" + print(f" {m} {name} — {detail}") + checks.append((name, passed)) + + check("root", is_root()) + check("systemd", run_ok("systemctl --version")) + arch = run("uname -m").stdout.strip() + check("arch", arch in ("x86_64", "aarch64"), arch) + check("curl/wget", run_ok("which curl") or run_ok("which wget"), "downloader") + for c in ("systemctl", "sed", "mkdir", "rm"): + check(f"cmd:{c}", run_ok(f"which {c}")) + check("installer", run_ok(f"curl -fsSL -o /dev/null {AIO_URL} 2>/dev/null"), "meganode") + for cloud, host in (("US", "us1.api.wallarm.com"), ("EU", "api.wallarm.com")): + check(f"cloud:{cloud}", run_ok(f"curl -fsSL -o /dev/null https://{host} 2>/dev/null"), host) + return ok + +# ─── Config ──────────────────────────────────────────────────────────── + +def load_config(): + if not os.path.exists(CONF): + return {} + with open(CONF) as f: + return json.load(f) + +def load_state(): + if not os.path.exists(STATE): + return {"nodes": []} + with open(STATE) as f: + return json.load(f) + +def save_state(s): + with open(STATE, "w") as f: + json.dump(s, f, indent=2) + +def is_deployed(name): + s = load_state() + return any(n["name"] == name for n in s.get("nodes", [])) + +# ─── Node Operations ─────────────────────────────────────────────────── + +def install_node(name, token, cloud, port, upstream_ip, upstream_port, labels, mode): + instance = f"{BASE}/{name}/wallarm" + api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com" + address = f"0.0.0.0:{port}" + upstream = f"{upstream_ip}:{upstream_port}" + + # Download AIO + if not os.path.exists(AIO_PATH): + print(f"[{name}] Downloading installer...") + dl = "curl -fsSL" if run_ok("which curl") else "wget -q" + run(f"{dl} -o {AIO_PATH} {AIO_URL}") + os.chmod(AIO_PATH, 0o755) + + # Prepare /opt/wallarm + if os.path.islink(DEPLOY_TO) or os.path.isfile(DEPLOY_TO): + os.remove(DEPLOY_TO) + shutil.rmtree(DEPLOY_TO, ignore_errors=True) + os.makedirs(DEPLOY_TO, exist_ok=True) + + # Install NGINX + nginx_bin = f"{DEPLOY_TO}/nginx/sbin/nginx" + if not os.path.exists(nginx_bin): + os.makedirs(os.path.dirname(nginx_bin), exist_ok=True) + sys_nginx = shutil.which("nginx") + if sys_nginx: + shutil.copy(sys_nginx, nginx_bin) + else: + run("apt-get install -y -qq nginx 2>/dev/null || yum install -y -q nginx 2>/dev/null", timeout=60) + if os.path.exists("/usr/sbin/nginx"): + shutil.copy("/usr/sbin/nginx", nginx_bin) + + # Extract AIO + print(f"[{name}] Extracting...") + run(f"bash {AIO_PATH} --noexec --keep --target {DEPLOY_TO} --noprogress --accept 2>&1", timeout=120) + + # Nginx config + nginx_dir = f"{DEPLOY_TO}/nginx" + os.makedirs(f"{nginx_dir}/conf", exist_ok=True) + with open(f"{nginx_dir}/conf/nginx.conf", "w") as f: + f.write(f"""load_module {DEPLOY_TO}/modules/nginx_v1.26.3_s0ff5dffff/ngx_http_wallarm_module.so; + +worker_processes auto; +pid {nginx_dir}/nginx.pid; +error_log {nginx_dir}/error.log; +events {{ worker_connections 10240; }} +http {{ + access_log {nginx_dir}/access.log; + wallarm_mode {mode}; + server {{ + listen {port}; + server_name _; + location /wallarm-status {{ wallarm_status on; allow 127.0.0.0/8; deny all; }} + location / {{ proxy_pass http://{upstream}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }} + }} +}} +""") + run(f"{nginx_bin} -c {nginx_dir}/conf/nginx.conf", timeout=5) + + # Pick module + register + run(f"bash {DEPLOY_TO}/pick-module.sh 2>&1", timeout=10) + print(f"[{name}] Registering node...") + r = run(f"bash -c 'source {DEPLOY_TO}/env.list 2>/dev/null; {DEPLOY_TO}/register-node job:register -token {token} -host {api_host}'", + timeout=120) + if r.returncode != 0 and "node instance registered" not in r.stdout: + print(f"Registration output:\n{r.stdout}\n{r.stderr}") + # Not fatal — check if UUID assigned + if not os.path.exists(f"{DEPLOY_TO}/etc/wallarm/node.yaml"): + raise RuntimeError(f"Registration failed: {r.stderr or r.stdout}") + + # Move to instance + print(f"[{name}] Installing...") + shutil.rmtree(instance, ignore_errors=True) + os.makedirs(os.path.dirname(instance), exist_ok=True) + try: + os.rename(DEPLOY_TO, instance) + except OSError: + run(f"cp -a {DEPLOY_TO} {instance}", timeout=30) + shutil.rmtree(DEPLOY_TO, ignore_errors=True) + + # Patch paths + ports + offset = hash_port(instance) + for pattern in ("*.sh", "*.list", "*.conf", "*.yaml", "*.yml"): + for f in Path(instance).rglob(pattern): + content = f.read_text() + content = content.replace("/opt/wallarm", instance) + for port_num in (3313, 6388, 9001, 8088, 9667, 6060, 5005): + content = content.replace(f":{port_num}", f":{port_num + offset}") + f.write_text(content) + + # Add config-file to wcli + for conf in Path(f"{instance}/etc").glob("*.conf"): + c = conf.read_text() + if "wcli run" in c and "--config-file" not in c: + conf.write_text(c.replace("wcli run", f"wcli run --config-file {instance}/etc/wallarm/node.yaml")) + + # Systemd + tmpl = f"""[Unit] +Description=Wallarm Node - %i +After=network.target +[Service] +Type=simple +WorkingDirectory={BASE}/%i/wallarm +EnvironmentFile=-{BASE}/%i/wallarm/env.list +ExecStartPre=/bin/ln -sf {BASE}/%i/wallarm /opt/wallarm +ExecStartPre={BASE}/%i/wallarm/nginx/sbin/nginx -c {BASE}/%i/wallarm/nginx/conf/nginx.conf +ExecStartPre=/bin/sleep 1 +ExecStart={BASE}/%i/wallarm/usr/bin/python3.10 {BASE}/%i/wallarm/usr/bin/supervisord -c {BASE}/%i/wallarm/etc/supervisord.conf +ExecStop={BASE}/%i/wallarm/usr/bin/python3.10 {BASE}/%i/wallarm/usr/bin/supervisord -c {BASE}/%i/wallarm/etc/supervisord.conf shutdown +Restart=on-failure +RestartSec=5 +User=root +[Install] +WantedBy=multi-user.target +""" + with open("/etc/systemd/system/wallarm-node@.service", "w") as f: + f.write(tmpl) + run("systemctl daemon-reload") + run(f"systemctl enable wallarm-node@{name}") + run(f"systemctl start wallarm-node@{name}") + print(f"✅ {name} deployed. systemctl status wallarm-node@{name}") + +# ─── Remove ──────────────────────────────────────────────────────────── + +def remove_node(name): + run(f"systemctl stop wallarm-node@{name} 2>/dev/null") + run(f"systemctl disable wallarm-node@{name} 2>/dev/null") + shutil.rmtree(f"{BASE}/{name}", ignore_errors=True) + s = load_state() + s["nodes"] = [n for n in s["nodes"] if n["name"] != name] + save_state(s) + print(f"✅ {name} removed.") + +# ─── Status ──────────────────────────────────────────────────────────── + +def show_status(): + s = load_state() + if not s.get("nodes"): + print("No nodes deployed.") + return + print("\n─── Node Status ───") + for n in s["nodes"]: + r = run(f"systemctl is-active wallarm-node@{n['name']}") + status = "●" if r.stdout.strip() == "active" else "○" + print(f" {status} {n['name']} — {r.stdout.strip()} — {n.get('address','')}") + +# ─── Tunnel ──────────────────────────────────────────────────────────── + +def start_tunnel(): + host = input("Jumphost URL [ssh.sechpoint.app:443]: ") or "ssh.sechpoint.app:443" + user = input("Username [wallarm-tunnel]: ") or "wallarm-tunnel" + pw = input("Password or SSH key path: ") + if not pw: + print("No credentials.") + return + print(f"\nShare: ssh -p 9042 {user}@{host}\nCtrl+C to close.\n") + # Import tunnel module on demand + try: + from tunnel import start_tunnel_ssh + start_tunnel_ssh(host, user, pw) + except ImportError: + print("Tunnel module not available. Install paramiko: pip install paramiko") + +# ─── Deploy Menu ─────────────────────────────────────────────────────── + +def deploy_menu(): + cfg = load_config() + nodes = cfg.get("nodes", {}) + if not nodes: + print("No nodes in /opt/fw/fw.conf") + return + + deployed = {n["name"] for n in load_state().get("nodes", [])} + available = [] + print("\n─── Deploy Node ───") + for name, nc in nodes.items(): + if name in deployed: + print(f" ✓ {name} (port {nc.get('port','?')}) [deployed]") + else: + available.append(name) + print(f" [{len(available)}] {name} (port {nc.get('port','?')})") + + if not available: + print("All nodes deployed.") + return + + c = input("\nSelect number (Enter=all): ").strip() + selected = available if not c else [available[int(c)-1]] if c.isdigit() and 1 <= int(c) <= len(available) else [] + + if not selected: + print("Invalid.") + return + + for name in selected: + nc = nodes[name] + print(f"\nDeploying {name}...") + mode = nc.get("mode", "monitoring") + print("\nTraffic mode: [1] monitoring [2] safe_blocking [3] block [4] off") + m = input(f"Choose [{['','1','2','3','4'][['monitoring','safe_blocking','block','off'].index(mode)] if mode in ['monitoring','safe_blocking','block','off'] else '1'}]: ").strip() + modes = {"2":"safe_blocking","3":"block","4":"off"} + mode = modes.get(m, mode) + + try: + install_node(name, nc["token"], nc.get("cloud","EU"), + str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"), + str(nc.get("upstream_port","80")), nc.get("labels",f"group={name}"), mode) + s = load_state() + s.setdefault("nodes", []).append({ + "name": name, "type": "native", "address": f"0.0.0.0:{nc.get('port','8081')}", + "upstream_ip": nc.get("upstream_ip",""), "upstream_port": int(nc.get("upstream_port",80)), + "status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ") + }) + save_state(s) + print(f"✅ {name} deployed.") + except Exception as e: + print(f"❌ {name} failed: {e}") + +# ─── Edit Menu ───────────────────────────────────────────────────────── + +def edit_menu(): + s = load_state() + if not s.get("nodes"): + print("No nodes.") + return + print("\n─── Edit Node ───") + for i, n in enumerate(s["nodes"]): + r = run(f"systemctl is-active wallarm-node@{n['name']}") + st = "●" if r.stdout.strip() == "active" else "○" + print(f" [{i+1}] {st} {n['name']} {n.get('address','')} → {n.get('upstream_ip','')}:{n.get('upstream_port','')}") + c = input("\nSelect: ").strip() + if not c.isdigit(): return + n = s["nodes"][int(c)-1] + + port = input(f"Port [{n['address'].split(':')[-1]}]: ").strip() + ip = input(f"Upstream IP [{n.get('upstream_ip','')}]: ").strip() + up = input(f"Upstream port [{n.get('upstream_port','')}]: ").strip() + if not any([port, ip, up]): + print("No changes.") + return + if port: n["address"] = f"0.0.0.0:{port}" + if ip: n["upstream_ip"] = ip + if up: n["upstream_port"] = int(up) + save_state(s) + + # Update nginx config + nginx_conf = f"{BASE}/{n['name']}/wallarm/nginx/conf/nginx.conf" + if os.path.exists(nginx_conf): + c = Path(nginx_conf).read_text() + if port: c = c.replace(f"listen {n['address'].split(':')[-1]};", f"listen {port};") + if ip or up: + old_up = f"{n.get('upstream_ip','')}:{n.get('upstream_port','')}" + new_up = f"{n.get('upstream_ip','')}:{n.get('upstream_port','')}" + c = c.replace(f"http://{old_up}", f"http://{new_up}") + Path(nginx_conf).write_text(c) + print(f"✅ Updated. Restart: systemctl restart wallarm-node@{n['name']}") + +# ─── Remove Menu ─────────────────────────────────────────────────────── + +def remove_menu(): + s = load_state() + if not s.get("nodes"): + print("No nodes.") + return + print("\n─── Remove Node ───") + for i, n in enumerate(s["nodes"]): + r = run(f"systemctl is-active wallarm-node@{n['name']}") + st = "●" if r.stdout.strip() == "active" else "○" + print(f" [{i+1}] {st} {n['name']} {n.get('address','')}") + c = input("\nSelect: ").strip() + if not c.isdigit(): return + n = s["nodes"][int(c)-1] + remove_node(n["name"]) + +# ─── Main ────────────────────────────────────────────────────────────── + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == "--version": + print(f"deploy.py v{VERSION}") + return + if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all": + deploy_all() + return + + print(f"═══ Wallarm Deployment Manager ═══") + print(f" v{VERSION}\n") + if not preflight(): + print("\n❌ Preflight failed.") + sys.exit(1) + print("✅ Preflight passed.\n") + + while True: + print("\n─── Menu ───") + print(" [1] Deploy [2] Edit [3] Status [4] Remove [5] Tunnel [q] Quit") + c = input("\nChoice: ").strip() + if c == "1": deploy_menu() + elif c == "2": edit_menu() + elif c == "3": show_status() + elif c == "4": remove_menu() + elif c == "5": start_tunnel() + elif c.lower() == "q": break + +def deploy_all(): + cfg = load_config() + for name, nc in cfg.get("nodes", {}).items(): + if is_deployed(name): + print(f"✓ {name} already deployed") + continue + print(f"\nDeploying {name}...") + install_node(name, nc["token"], nc.get("cloud","EU"), + str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"), + str(nc.get("upstream_port","80")), nc.get("labels",f"group={name}"), + nc.get("mode","monitoring")) + +if __name__ == "__main__": + main() diff --git a/setup.sh b/setup.sh index 8450835..578b7fc 100644 --- a/setup.sh +++ b/setup.sh @@ -2,66 +2,58 @@ # ============================================================================== # Wallarm Deployment Bootstrap # ============================================================================== -# Builds the deploy binary and installs to /opt/fw/. -# The binary handles everything: symlinks, extraction, setup. -# +# Installs Python 3.12 via pyenv (cross-distro), then runs deploy.py. # curl -fsSL ".../setup.sh" | bash -# sudo /opt/fw/deploy +# sudo /opt/fw/deploy.py # ============================================================================== - set -euo pipefail -BOLD='\033[1m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -REPO_URL="https://git.sechpoint.app/customer-engineering/wallarm.git" +BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +REPO="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/python" WALLARM_DIR="/opt/fw" -BUILD_DIR="/tmp/wallarm-build-$$" - -cleanup() { rm -rf "$BUILD_DIR"; } -trap cleanup EXIT echo -e "${BOLD}Wallarm Bootstrap${NC}" -# ── Prerequisites ───────────────────────────────────────────────────── -install_deps() { - if command -v apt-get >/dev/null 2>&1; then - apt-get update -qq - apt-get install -y -qq curl git golang-go 2>/dev/null - elif command -v yum >/dev/null 2>&1; then - yum install -y -q curl git golang 2>/dev/null - elif command -v dnf >/dev/null 2>&1; then - dnf install -y -q curl git golang 2>/dev/null - elif command -v apk >/dev/null 2>&1; then - apk add --no-cache curl git go 2>/dev/null - fi -} - -if ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1 || ! command -v go >/dev/null 2>&1; then - echo -e "${YELLOW}Installing dependencies...${NC}" - install_deps +# ── Install curl/git if missing ─────────────────────────────────────── +if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + apt-get update -qq 2>/dev/null && apt-get install -y -qq curl 2>/dev/null || true fi -echo -e "${GREEN}git $(git --version | cut -d' ' -f3) go $(go version | cut -d' ' -f3)${NC}" -# ── Build ───────────────────────────────────────────────────────────── -echo -e "${YELLOW}Cloning...${NC}" -git clone --depth 1 "$REPO_URL" "$BUILD_DIR" 2>/dev/null +# ── Install pyenv + Python 3.12 ─────────────────────────────────────── +PYTHON_BIN="" +if command -v python3 >/dev/null 2>&1 && python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then + PYTHON_BIN="python3" +elif [ -f "$HOME/.pyenv/versions/3.12.0/bin/python3" ]; then + PYTHON_BIN="$HOME/.pyenv/versions/3.12.0/bin/python3" +else + echo -e "${YELLOW}Installing pyenv + Python 3.12...${NC}" + if ! command -v git >/dev/null 2>&1; then + apt-get update -qq 2>/dev/null && apt-get install -y -qq git 2>/dev/null || true + fi + # Install build deps + apt-get install -y -qq make build-essential libssl-dev zlib1g-dev \ + libbz2-dev libreadline-dev libsqlite3-dev libncursesw5-dev \ + xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev 2>/dev/null || true + curl -fsSL https://pyenv.run | bash 2>/dev/null + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + pyenv install 3.12.0 -s 2>/dev/null + pyenv global 3.12.0 + PYTHON_BIN="$HOME/.pyenv/versions/3.12.0/bin/python3" +fi -echo -e "${YELLOW}Building (~30s)...${NC}" -cd "$BUILD_DIR" -go build -ldflags="-s -w -X main.version=$(git describe --tags --always 2>/dev/null || echo 'dev')" -o deploy ./cmd/deploy/ -command -v upx >/dev/null 2>&1 && upx --best --lzma deploy -o deploy.tmp 2>/dev/null && mv deploy.tmp deploy || true +echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}" -# ── Install ─────────────────────────────────────────────────────────── +# ── Download deploy.py ──────────────────────────────────────────────── mkdir -p "$WALLARM_DIR" -cp deploy "$WALLARM_DIR/deploy" -chmod +x "$WALLARM_DIR/deploy" +if command -v curl >/dev/null 2>&1; then + curl -fsSL "${REPO}/deploy.py" -o "${WALLARM_DIR}/deploy.py" +else + wget -q "${REPO}/deploy.py" -O "${WALLARM_DIR}/deploy.py" +fi +chmod +x "${WALLARM_DIR}/deploy.py" -V=$("$WALLARM_DIR/deploy" --version 2>/dev/null || echo "unknown") echo -echo -e "${GREEN}${BOLD}Ready!${NC} (${V})" -echo -e " ${GREEN}sudo /opt/fw/deploy${NC}" -ls -lh "$WALLARM_DIR/deploy" +echo -e "${GREEN}${BOLD}Ready!${NC}" +echo -e " ${GREEN}sudo ${PYTHON_BIN} /opt/fw/deploy.py${NC}"