commit cb99f3b399defc58a17dee9ac95005b4cef0edd6 Author: Armando Fracassi Date: Mon Apr 20 10:27:34 2026 +0200 chore: initial commit Sposta installer di prima fase e Web Setup Wizard dal repo argos al nuovo repo pubblico argos-setup. File provenienti da argos/scripts/: - install.sh -> first-setup.sh (rinominato) - setup_server.py - setup.html - gen_config.py Rimane in argos/scripts/: - update.sh (continua a vivere nel codice runtime) Il nuovo repo e' pubblico: il contenuto non include secret ne' codice proprietario di ARGOS, solo il tooling di installazione. diff --git a/README.md b/README.md new file mode 100644 index 0000000..67fd242 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# ARGOS SOC — Setup & Installer + +Tecnotel Servizi SRL — [www.tecnotelsrl.com](https://www.tecnotelsrl.com) + +Repository pubblico contenente l'installer di prima fase e il Web Setup Wizard di ARGOS SOC. + +## Contenuto + +| File | Scopo | +|---|---| +| `first-setup.sh` | Installer ambiente di prima fase: sistema base, utenti, firewall, virtualenv, nginx. | +| `setup_server.py` | Backend Flask del Web Installer (porta 8888). | +| `setup.html` | Frontend del Web Installer, wizard in 5 step. | +| `gen_config.py` | Helper per generazione iniziale di `argos.json`. | + +## Flusso d'installazione + +1. Il cliente esegue `first-setup.sh` per predisporre l'ambiente base (Ubuntu 24.04). +2. Al termine dello script viene avviato il Web Installer su `http://IP:8888`. +3. Il wizard completa la configurazione: cliente, rete/SSL, SIEM, utente admin. +4. Al termine del wizard, `argos-setup` richiede la licenza ARGOS per sbloccare il clone del repository privato `tecnotel/argos` e completare l'installazione dei componenti runtime. + +## Repository correlati + +- [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato). + +## Versioning + +Questo repository segue lo stesso schema di versioning di `argos` (SemVer tag `vX.Y.Z`). diff --git a/first-setup.sh b/first-setup.sh new file mode 100644 index 0000000..9d2b704 --- /dev/null +++ b/first-setup.sh @@ -0,0 +1,199 @@ +#!/bin/bash +# ══════════════════════════════════════════════════════════════════════════════ +# ARGOS SOC — Installer ambiente +# Tecnotel Servizi SRL — Ubuntu 24.04 LTS +# Uso: sudo bash install.sh +# ══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +section() { echo -e "\n${BLUE}══════════════════════════════════════${NC}"; echo -e "${BLUE} $1${NC}"; echo -e "${BLUE}══════════════════════════════════════${NC}"; } + +[[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash install.sh" +. /etc/os-release +[[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS" + +echo "" +echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ ARGOS SOC — Installer v1.0.0 ║${NC}" +echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}" +echo "" + + +GITEA_REPO="https://3eefb5a2802e8c5a9395396b1bb98e2d5fe46101@argos-update.tecnotelsrl.com:3443/tecnotel/argos.git" + +# ══════════════════════════════════════════════════════════════════════════════ +section "1. Sistema base" +# ══════════════════════════════════════════════════════════════════════════════ +timedatectl set-timezone Europe/Rome +apt-get update -qq +apt-get upgrade -y -qq +apt-get install -y -qq \ + curl wget git vim htop unzip jq \ + python3 python3-pip python3-venv \ + nginx certbot python3-certbot-nginx \ + ufw fail2ban \ + build-essential libssl-dev libffi-dev python3-dev \ + sqlite3 net-tools dnsutils lsof tcpdump nmap \ + ca-certificates gnupg apt-transport-https +success "Pacchetti sistema installati" + +# ══════════════════════════════════════════════════════════════════════════════ +section "2. Node.js 20 LTS" +# ══════════════════════════════════════════════════════════════════════════════ +if ! node --version 2>/dev/null | grep -q "v2[02]"; then + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1 + apt-get install -y nodejs -qq +fi +success "Node.js $(node --version) installato" + +# ══════════════════════════════════════════════════════════════════════════════ +section "3. Utente applicazione" +# ══════════════════════════════════════════════════════════════════════════════ +if ! id "argos" &>/dev/null; then + useradd -r -s /bin/bash -m -d /home/argos argos + success "Utente argos creato" +else + warn "Utente argos già esistente" +fi +# Aggiungi argos al gruppo systemd-journal per lettura log via journalctl +# (necessario per la pagina 'Log Servizi' in UI) +usermod -aG systemd-journal argos +success "Utente argos aggiunto al gruppo systemd-journal" + +# Permetti a user 'argos' di restart dei servizi via UI (Backup & Restore) +# Scope ristretto: solo restart dei 4 demoni ARGOS, nessun altro comando. +cat > /etc/sudoers.d/argos-systemctl <<'SUDOEOF' +argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-backend +argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-sync +argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-ops +argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-analytics +SUDOEOF +chmod 440 /etc/sudoers.d/argos-systemctl +visudo -cf /etc/sudoers.d/argos-systemctl > /dev/null +success "Sudoers per restart servizi configurato" + +# ══════════════════════════════════════════════════════════════════════════════ +section "4. Struttura cartelle" +# ══════════════════════════════════════════════════════════════════════════════ +mkdir -p /opt/argos/{app,config,data,feeds,logs,certs,backups,setup} +mkdir -p /opt/argos/config/assets +mkdir -p /opt/argos/data/{reports,models} +chown -R argos:argos /opt/argos +chmod -R 750 /opt/argos +# /opt/argos/feeds: pubblicamente leggibili (FortiGate ETF via nginx/www-data) +chmod 755 /opt/argos/feeds +success "Struttura /opt/argos/ creata" + +# ══════════════════════════════════════════════════════════════════════════════ +section "5. Clone repository" +# ══════════════════════════════════════════════════════════════════════════════ +git config --global --add safe.directory /opt/argos/app 2>/dev/null || true +if [[ -d /opt/argos/app/.git ]]; then + warn "Repository già presente — aggiorno" + git -C /opt/argos/app pull origin main +else + git clone "$GITEA_REPO" /opt/argos/app +fi +chown -R argos:argos /opt/argos/app +success "Repository clonato in /opt/argos/app/" + +# ══════════════════════════════════════════════════════════════════════════════ +section "6. Python virtualenv" +# ══════════════════════════════════════════════════════════════════════════════ +python3 -m venv /opt/argos/app/backend/venv +/opt/argos/app/backend/venv/bin/pip install --upgrade pip -q +if [[ -f /opt/argos/app/backend/requirements.txt ]]; then + /opt/argos/app/backend/venv/bin/pip install -r /opt/argos/app/backend/requirements.txt -q + success "Dipendenze Python installate" +else + warn "requirements.txt non trovato — installare manualmente dopo" +fi +chown -R argos:argos /opt/argos/app/backend/venv + +# ══════════════════════════════════════════════════════════════════════════════ +section "7. Firewall UFW" +# ══════════════════════════════════════════════════════════════════════════════ +ufw --force reset >/dev/null +ufw default deny incoming >/dev/null +ufw default allow outgoing >/dev/null +ufw allow 22/tcp comment 'SSH' >/dev/null +ufw allow 80/tcp comment 'HTTP' >/dev/null +ufw allow 443/tcp comment 'HTTPS' >/dev/null +ufw allow 8888/tcp comment 'ARGOS Web Installer (temporaneo)' >/dev/null +ufw --force enable >/dev/null +success "Firewall UFW configurato" + +# ══════════════════════════════════════════════════════════════════════════════ +section "8. Fail2ban" +# ══════════════════════════════════════════════════════════════════════════════ +systemctl enable --now fail2ban >/dev/null 2>&1 +success "Fail2ban attivo" + +# ══════════════════════════════════════════════════════════════════════════════ +section "9. Nginx temporaneo" +# ══════════════════════════════════════════════════════════════════════════════ +rm -f /etc/nginx/sites-enabled/default +cat > /etc/nginx/sites-available/argos-setup << 'NGINX' +server { + listen 80 default_server; + server_name _; + return 200 'ARGOS SOC Setup in corso — vai a http://IP:8888'; + add_header Content-Type text/plain; +} +NGINX +ln -sf /etc/nginx/sites-available/argos-setup /etc/nginx/sites-enabled/ +nginx -t && systemctl restart nginx +success "Nginx temporaneo configurato" + +# ══════════════════════════════════════════════════════════════════════════════ +section "10. Web Installer" +# ══════════════════════════════════════════════════════════════════════════════ +# Copia file setup +cp /opt/argos/app/scripts/setup_server.py /opt/argos/setup/ +cp /opt/argos/app/scripts/setup.html /opt/argos/setup/ + +# Systemd service web installer +cat > /etc/systemd/system/argos-setup.service << 'EOF' +[Unit] +Description=ARGOS SOC Web Installer +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/argos/setup +ExecStart=/opt/argos/app/backend/venv/bin/python3 /opt/argos/setup/setup_server.py +Restart=on-failure +RestartSec=3 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=argos-setup + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable --now argos-setup +success "Web installer avviato" + +SERVER_IP=$(hostname -I | awk '{print $1}') + +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Ambiente pronto! ║${NC}" +echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e " ${CYAN}Completa la configurazione aprendo nel browser:${NC}" +echo -e " ${YELLOW}→ http://${SERVER_IP}:8888${NC}" +echo "" +echo -e " ${YELLOW}NOTA:${NC} La porta 8888 verrà chiusa automaticamente" +echo -e " al termine dell'installazione." +echo "" diff --git a/gen_config.py b/gen_config.py new file mode 100644 index 0000000..2288df2 --- /dev/null +++ b/gen_config.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +ARGOS SOC — Generatore argos.json +Legge le variabili d'ambiente impostate dall'installer e genera /opt/argos/config/argos.json +""" +import json +import os +from datetime import datetime, timezone + +def e(key, default=""): + return os.environ.get(key, default) + +config = { + "_version": "1.0", + "_cliente": e("CLIENTE"), + "_domain": e("DOMAIN"), + "_installed": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + + "cliente": { + "name": e("CLIENTE"), + "full_name": e("CLIENTE_FULL"), + "domain": e("CLIENTE_DOMAIN"), + "type": e("CLIENTE_TYPE", "enterprise"), + "sharepoint_tenant": e("SP_TENANT") + }, + + "system": { + "secret_key": e("SECRET_KEY"), + "internal_api_key": e("INTERNAL_KEY"), + "anthropic_key": e("ANTHROPIC_KEY"), + "ai_context": e("AI_CONTEXT", "ARGOS SOC"), + "tz": "Europe/Rome" + }, + + "paths": { + "data_dir": "/opt/argos/data", + "feeds_dir": "/opt/argos/feeds", + "logs_dir": "/opt/argos/logs", + "config_dir": "/opt/argos/config", + "analytics_db": "/opt/argos/data/analytics.db", + "analytics_exclude_entities": "" + }, + + "ports": { + "backend": 8080, + "sync": 8081, + "ops": 8082, + "analytics": 8083 + }, + + "opensearch": { + "url": e("OS_URL"), + "user": e("OS_USER", "admin"), + "password": e("OS_PASS") + }, + + "wazuh": { + "api_url": e("WAZUH_API_URL"), + "api_user": e("WAZUH_API_USER", "wazuh"), + "api_pass": e("WAZUH_API_PASS"), + "manager_name": e("WAZUH_MANAGER", "wazuh") + }, + + "entra": { + "tenant_id": e("ENTRA_TENANT"), + "client_id": e("ENTRA_CLIENT"), + "client_secret": e("ENTRA_SECRET") + }, + + "eset": { + "region": "eu", + "api_user": e("ESET_USER"), + "api_pass": e("ESET_PASS") + }, + + "fortigate": { + "hosts": e("FGT_HOSTS"), + "port": e("FGT_PORT", "443"), + "tokens": e("FGT_TOKENS"), + "names": e("FGT_NAMES") + }, + + "smtp": { + "host": e("SMTP_HOST"), + "port": int(e("SMTP_PORT", "587")), + "user": e("SMTP_USER"), + "password": e("SMTP_PASS"), + "from_email": e("FROM_EMAIL") + }, + + "threat_intel": { + "abuseipdb_key": e("ABUSEIPDB_KEY"), + "maltiverse_key": e("MALTIVERSE_KEY") + }, + + "pdf": { + "org_name": e("PDF_ORG_NAME"), + "author": e("PDF_AUTHOR", "Tecnotel Servizi SRL"), + "motto": e("PDF_MOTTO", "Controllo totale. Difesa continua."), + "classification": e("PDF_CLASS", "RISERVATO - USO INTERNO"), + "app_logo": "/opt/argos/app/frontend/dist/logo_argos_bianco.png", + "client_logo": "/opt/argos/data/logo_cliente_pdf.png" + } +} + +out = "/opt/argos/config/argos.json" +with open(out, "w") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + +print(f"argos.json generato: {out}") diff --git a/setup.html b/setup.html new file mode 100644 index 0000000..2aca48b --- /dev/null +++ b/setup.html @@ -0,0 +1,419 @@ + + + + + +ARGOS SOC — Installazione + + + + +
+
+ +
+
Setup Wizard — Tecnotel Servizi SRL
+
v1.0.0
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
👤 Informazioni cliente
+
Dati dell'organizzazione che utilizzerà ARGOS SOC.
+
+
Usato nei titoli e intestazioni
+
+
+
Dominio utenti (es. nome@azienda.it)
+
Solo se Microsoft 365. Vuoto se non usato.
+
Logo cliente (per i PDF report)
+
+ +
🖼️
+
Trascina il logo o clicca per selezionare
+
PNG, JPG — max 2MB — opzionale
+
+
+
+ +
+ + +
+
🌐 Rete & SSL
+
Configurazione dominio e certificato SSL. Il DNS deve già puntare a questo server.
+
+
+
Separati da spazio — opzionale
+
Certificato SSL
+
+
🔒 Let's EncryptAutomatico e gratuito
+
📄 Certificato esistenteCarica .crt e .key
+
+
Per notifiche di scadenza certificato
+ +
+ +
+ + +
+
🔍 SIEM — OpenSearch
+
Connessione al backend SIEM. Le altre integrazioni si configurano dall'interfaccia ARGOS dopo l'installazione.
+
+
+
+
+
ℹ️ Le integrazioni (ESET, FortiGate, Entra ID, ecc.) si configurano dopo il primo accesso dalla sezione Integrazioni di ARGOS.
+
+ +
+ + +
+
🔑 Utente amministratore
+
Crea il primo account admin per accedere a ARGOS SOC.
+
+
+
+
+
+ +
✓ Potrai creare altri utenti con ruoli diversi dall'interfaccia ARGOS dopo l'installazione.
+
+ +
+ + +
+
🚀 Riepilogo & Installazione
+
Verifica i dati e avvia l'installazione.
+ + +
+ + + + + + + + + + + + +
+ +
+
+
ARGOS SOC — Tecnotel Servizi SRL  ·  Web Installer v1.0.0  ·  La porta 8888 verrà chiusa al termine
+
+ + + + diff --git a/setup_server.py b/setup_server.py new file mode 100644 index 0000000..f9eb864 --- /dev/null +++ b/setup_server.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 +""" +ARGOS SOC — Web Installer Server +Tecnotel Servizi SRL +""" +import json +import os +import subprocess +import secrets +import shutil +import hashlib +import threading +import signal +from datetime import datetime, timezone +from pathlib import Path +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse + +APP_DIR = Path("/opt/argos/app") +CONFIG_DIR = Path("/opt/argos/config") +DATA_DIR = Path("/opt/argos/data") +FEEDS_DIR = Path("/opt/argos/feeds") +LOGS_DIR = Path("/opt/argos/logs") +CERTS_DIR = Path("/opt/argos/certs") +SETUP_DIR = Path("/opt/argos/setup") +APP_USER = "argos" +PORT = 8888 + +install_log = [] +install_done = False +install_error = False + + +def log(msg): + ts = datetime.now().strftime("%H:%M:%S") + line = f"[{ts}] {msg}" + install_log.append(line) + print(line, flush=True) + + +def run(cmd, check=True): + log(f"$ {cmd}") + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if result.stdout.strip(): log(result.stdout.strip()) + if result.stderr.strip(): log(result.stderr.strip()) + if check and result.returncode != 0: + raise RuntimeError(f"Comando fallito (exit {result.returncode}): {cmd}") + return result + + +def chown(path): + run(f"chown -R {APP_USER}:{APP_USER} {path}", check=False) + + +def generate_argos_json(data): + # Hostname primario dal wizard (campo "domain" nella UI = FQDN tipo soc.azienda.it) + hostname = data.get("domain", "").strip().lower() + # Alias dal wizard: stringa separata da spazi -> lista pulita + aliases_raw = data.get("aliases", "").strip() + aliases = [a.strip().lower() for a in aliases_raw.split() if a.strip()] + + return { + "_version": "1.2", + "_installed": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "cliente": { + "name": data.get("cliente_name", ""), + "full_name": data.get("cliente_full", ""), + "domain": data.get("cliente_domain", ""), + "type": data.get("cliente_type", "enterprise"), + "ai_context": "", + "sharepoint_tenant": "", + "console_url": "" + }, + "network": { + "hostname": hostname, + "aliases": aliases + }, + "system": { + "secret_key": secrets.token_hex(32), + "internal_api_key": secrets.token_hex(24), + "tz": "Europe/Rome" + }, + "opensearch": { + "url": data.get("os_url", ""), + "user": data.get("os_user", "admin"), + "password": data.get("os_pass", "") + }, + "paths": { + "data_dir": str(DATA_DIR), + "feeds_dir": str(FEEDS_DIR), + "logs_dir": str(LOGS_DIR), + "config_dir": str(CONFIG_DIR), + "analytics_db": str(DATA_DIR / "analytics.db") + }, + "ports": { + "backend": 8080, + "sync": 8081, + "ops": 8082, + "analytics": 8083 + } + } + + +def generate_integrations_json(): + example = APP_DIR / "config/integrations.json.example" + if example.exists(): + with open(example) as f: + return json.load(f) + # fallback minimale + return { + "_version": "1.0", + "endpoint": {}, "firewall": {}, "identity": {}, + "notifications": {}, "threat_intel": {}, + "pdf": { + "org_name": "", "author": "Tecnotel Servizi SRL", + "motto": "Controllo totale. Difesa continua.", + "classification": "RISERVATO - USO INTERNO", + "app_logo": str(APP_DIR / "frontend/dist/logo_argos_bianco.png"), + "client_logo": str(CONFIG_DIR / "assets" / "logo_cliente.png") + } + } + + +def create_admin_user(data): + username = data.get("admin_username", "admin").strip() + email = data.get("admin_email_user", "").strip() + password = data.get("admin_password", "").strip() + + if not username or not password: + log("WARN: credenziali admin mancanti — skip creazione utente") + return + + import os as _os + # Hash compatibile con auth.py: salt:sha256(salt+pw+"argos-2026") + salt = _os.urandom(16).hex() + pw_hash = f"{salt}:{hashlib.sha256((salt + password + 'argos-2026').encode()).hexdigest()}" + + users_file = DATA_DIR / "argos_users.json" + DATA_DIR.mkdir(parents=True, exist_ok=True) + + # Leggi utenti esistenti o crea struttura vuota + try: + with open(users_file) as f: + users_data = json.load(f) + except Exception: + users_data = {"users": {}} + + users_data["users"][username.lower()] = { + "password": pw_hash, + "role": "admin", + "name": username, + "email": email, + "enabled": True + } + + with open(users_file, "w") as f: + json.dump(users_data, f, indent=2) + + os.chmod(users_file, 0o600) + chown(users_file) + log(f"Utente admin '{username}' creato in argos_users.json") + + +def install(data): + global install_done, install_error + try: + log("=== AVVIO INSTALLAZIONE ARGOS SOC ===") + + # 1. argos.json + log("── Generazione argos.json ──") + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + config = generate_argos_json(data) + cfg_path = CONFIG_DIR / "argos.json" + with open(cfg_path, "w") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + os.chmod(cfg_path, 0o600) + chown(CONFIG_DIR) + log("argos.json creato") + + # 2. integrations.json + log("── Generazione integrations.json ──") + integ = generate_integrations_json() + # Aggiorna pdf con nome organizzazione + integ.setdefault("pdf", {}) + integ["pdf"]["org_name"] = data.get("cliente_full") or data.get("cliente_name", "") + integ["pdf"]["client_logo"] = str(CONFIG_DIR / "assets" / "logo_cliente.png") + integ_path = CONFIG_DIR / "integrations.json" + with open(integ_path, "w") as f: + json.dump(integ, f, indent=2, ensure_ascii=False) + os.chmod(integ_path, 0o600) + chown(integ_path) + log("integrations.json creato") + + # 3. modules.json + mods = APP_DIR / "config/modules.json.example" + if mods.exists(): + shutil.copy(mods, CONFIG_DIR / "modules.json") + chown(CONFIG_DIR / "modules.json") + log("modules.json copiato") + + # 4. Logo cliente + logo_src = SETUP_DIR / "logo_cliente.png" + if logo_src.exists(): + DATA_DIR.mkdir(parents=True, exist_ok=True) + shutil.copy(logo_src, CONFIG_DIR / "assets" / "logo_cliente.png") + chown(CONFIG_DIR / "assets" / "logo_cliente.png") + log("Logo cliente copiato") + + # 5. Build frontend + log("── Build frontend React ──") + run(f"cd {APP_DIR}/frontend && npm install --silent") + run(f"cd {APP_DIR}/frontend && npm run build") + chown(APP_DIR / "frontend") + # Permessi lettura per nginx (www-data) + run(f"chmod 755 /opt/argos /opt/argos/app /opt/argos/app/frontend {APP_DIR}/frontend/dist") + run(f"chmod -R 755 {APP_DIR}/frontend/dist/") + log("Frontend buildato") + + # 6. Dipendenze Python + log("── Dipendenze Python ──") + venv_pip = APP_DIR / "backend/venv/bin/pip" + req = APP_DIR / "backend/requirements.txt" + if req.exists(): + run(f"{venv_pip} install -r {req} -q") + log("Dipendenze Python installate") + + # 7. Utente admin + log("── Creazione utente admin ──") + create_admin_user(data) + + # 8. SSL + log("── Configurazione SSL ──") + domain = data.get("domain", "").strip() + aliases = data.get("aliases", "").strip() + ssl_mode = data.get("ssl_mode", "letsencrypt") + all_names = (domain + " " + aliases).strip() + + if ssl_mode == "manual": + crt_src = SETUP_DIR / "uploaded.crt" + key_src = SETUP_DIR / "uploaded.key" + if not crt_src.exists() or not key_src.exists(): + raise RuntimeError("File SSL .crt o .key non trovati in /opt/argos/setup/") + CERTS_DIR.mkdir(parents=True, exist_ok=True) + shutil.copy(crt_src, CERTS_DIR / "fullchain.pem") + shutil.copy(key_src, CERTS_DIR / "privkey.pem") + os.chmod(CERTS_DIR / "privkey.pem", 0o600) + ssl_crt = str(CERTS_DIR / "fullchain.pem") + ssl_key = str(CERTS_DIR / "privkey.pem") + log("Certificato SSL manuale copiato") + else: + _write_nginx_http(all_names) + run("nginx -t && systemctl restart nginx") + certbot_d = " ".join(f"-d {n}" for n in all_names.split()) + email = data.get("admin_email", "admin@tecnotelsrl.com") + run(f"certbot --nginx {certbot_d} --non-interactive --agree-tos -m {email}") + ssl_crt = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem" + log("Certificato Let's Encrypt ottenuto") + + # 9. Nginx finale + log("── Nginx configurazione finale ──") + _write_nginx_final(all_names, ssl_crt, ssl_key) + run("nginx -t && systemctl restart nginx") + log("Nginx configurato") + + # 10. Systemd services + log("── Creazione e avvio servizi ──") + _write_services() + run("systemctl daemon-reload") + for svc in ["argos-backend", "argos-sync", "argos-ops", "argos-analytics"]: + run(f"systemctl enable --now {svc}") + log(f"{svc} avviato") + + # 11. Chiudi web installer + log("── Chiusura web installer ──") + run("systemctl disable --now argos-setup", check=False) + run("ufw delete allow 8888/tcp", check=False) + log("Porta 8888 chiusa — web installer disabilitato") + + log("=== INSTALLAZIONE COMPLETATA ===") + install_done = True + + # Spegni il processo dopo 8s (tempo per inviare la risposta al browser) + def shutdown(): + import time + time.sleep(15) + os.kill(os.getpid(), signal.SIGTERM) + threading.Thread(target=shutdown, daemon=True).start() + + except Exception as e: + log(f"ERRORE: {e}") + install_log.append(f"__ERROR__: {e}") + install_error = True + + +def _write_nginx_http(all_names): + conf = f"""server {{ + listen 80; + server_name {all_names}; + location /.well-known/acme-challenge/ {{ root /var/www/html; }} + location / {{ return 301 https://$host$request_uri; }} +}} +""" + _write_nginx_conf(conf) + + +def _write_nginx_final(all_names, ssl_crt, ssl_key): + conf = f"""limit_req_zone $binary_remote_addr zone=argos:10m rate=20r/s; + +server {{ + listen 80; + server_name {all_names}; + location / {{ return 301 https://$host$request_uri; }} + location /.well-known/acme-challenge/ {{ root /var/www/html; }} +}} + +server {{ + listen 443 ssl http2; + server_name {all_names}; + + ssl_certificate {ssl_crt}; + ssl_certificate_key {ssl_key}; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header Strict-Transport-Security "max-age=31536000" always; + + client_max_body_size 50m; + + location /api/analytics/ {{ + limit_req zone=argos burst=40 nodelay; + proxy_pass http://127.0.0.1:8083; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 120s; + }} + location /feeds/ {{ + alias {FEEDS_DIR}/; + autoindex off; + default_type "text/plain; charset=utf-8"; + access_log {LOGS_DIR}/nginx-feeds.log; + }} + location /api/ {{ + limit_req zone=argos burst=40 nodelay; + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 120s; + }} + + location / {{ + root {APP_DIR}/frontend/dist; + try_files $uri $uri/ /index.html; + expires 1h; + }} + + access_log {LOGS_DIR}/nginx-access.log; + error_log {LOGS_DIR}/nginx-error.log; +}} +""" + _write_nginx_conf(conf) + + +def _write_nginx_conf(conf): + with open("/etc/nginx/sites-available/argos", "w") as f: + f.write(conf) + p = Path("/etc/nginx/sites-enabled/argos") + if not p.exists(): + p.symlink_to("/etc/nginx/sites-available/argos") + # Rimuovi default e setup temporaneo + for f in ["/etc/nginx/sites-enabled/default", "/etc/nginx/sites-enabled/argos-setup"]: + if Path(f).exists(): Path(f).unlink() + + +def _write_services(): + venv_py = str(APP_DIR / "backend/venv/bin/python3") + backend_dir = str(APP_DIR / "backend") + services = { + "backend": ("services/argos_backend.py", "Backend API"), + "sync": ("services/argos_sync.py", "Sync Daemon"), + "ops": ("services/argos_ops.py", "Ops Daemon"), + "analytics": ("services/argos_analytics.py", "Analytics Daemon"), + } + + for svc, (script, desc) in services.items(): + unit = f"""[Unit] +Description=ARGOS {desc} +After=network.target + +[Service] +Type=simple +User={APP_USER} +Group={APP_USER} +WorkingDirectory={backend_dir} +Environment=PYTHONPATH={backend_dir} +Environment=CONFIG_FILE={CONFIG_DIR}/argos.json +Environment=INTEGRATIONS_FILE={CONFIG_DIR}/integrations.json +Environment=DATA_DIR={DATA_DIR} +Environment=FEEDS_DIR={FEEDS_DIR} +Environment=LOGS_DIR={LOGS_DIR} +ExecStart={venv_py} {backend_dir}/{script} +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=argos-{svc} + +[Install] +WantedBy=multi-user.target +""" + with open(f"/etc/systemd/system/argos-{svc}.service", "w") as f: + f.write(unit) + + +class SetupHandler(BaseHTTPRequestHandler): + def log_message(self, *args): pass + + def do_GET(self): + path = urlparse(self.path).path + if path in ("/", "/setup"): + html_path = Path(__file__).parent / "setup.html" + if not html_path.exists(): + self.send_response(404); self.end_headers(); return + html = html_path.read_bytes() + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", len(html)) + self.end_headers() + self.wfile.write(html) + elif path == "/api/status": + self._json({"done": install_done, "error": install_error, "log": install_log[-60:]}) + else: + self.send_response(404); self.end_headers() + + def do_POST(self): + path = urlparse(self.path).path + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + + if path == "/api/install": + try: + data = json.loads(body) + threading.Thread(target=install, args=(data,), daemon=True).start() + self._json({"ok": True}) + except Exception as e: + self._json({"ok": False, "error": str(e)}, 400) + elif path == "/api/upload/cert": + SETUP_DIR.mkdir(parents=True, exist_ok=True) + (SETUP_DIR / "uploaded.crt").write_bytes(body) + self._json({"ok": True}) + elif path == "/api/upload/key": + SETUP_DIR.mkdir(parents=True, exist_ok=True) + (SETUP_DIR / "uploaded.key").write_bytes(body) + self._json({"ok": True}) + elif path == "/api/upload/logo": + SETUP_DIR.mkdir(parents=True, exist_ok=True) + (SETUP_DIR / "logo_cliente.png").write_bytes(body) + self._json({"ok": True}) + else: + self.send_response(404); self.end_headers() + + def _json(self, data, code=200): + body = json.dumps(data).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", len(body)) + self.end_headers() + self.wfile.write(body) + + +if __name__ == "__main__": + print(f"\n{'='*55}") + print(f" ARGOS SOC — Web Installer") + print(f" Tecnotel Servizi SRL") + print(f" Apri: http://:{PORT}") + print(f"{'='*55}\n") + HTTPServer(("0.0.0.0", PORT), SetupHandler).serve_forever()