feat(phase6): wizard licenza + bootstrap one-liner

- first-setup.sh: rimosso token hardcoded + clone/venv spostati nel wizard
- setup_server.py: step licenza (verify Ed25519, machine_id match) + clone
  argos con URL autenticato da license.json
- setup.html: nuovo step 1 'Licenza ARGOS' con machine_id display + upload
- bootstrap.sh: one-liner installer per setup rapido su VM vergine
- README: documentazione flusso nuovo
This commit is contained in:
Tecnotel 2026-04-20 14:27:10 +02:00
parent 7d2c1d8809
commit 5e9a916515
6 changed files with 497 additions and 76 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.backup/
__pycache__/
*.pyc
.DS_Store

View File

@ -4,25 +4,61 @@ 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. Repository pubblico contenente l'installer di prima fase e il Web Setup Wizard di ARGOS SOC.
## Installazione rapida (one-liner)
Su una macchina Ubuntu 24.04 LTS vergine, esegui:
```bash
curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash
```
Il bootstrap scaricherà il repository e avvierà `first-setup.sh`.
## Installazione manuale
Se preferisci eseguire gli step separatamente:
```bash
sudo apt update && sudo apt install -y git
git clone https://argos-update.tecnotelsrl.com/tecnotel/argos-setup.git /opt/argos-setup-pkg
cd /opt/argos-setup-pkg
sudo bash first-setup.sh
```
## Contenuto ## Contenuto
| File | Scopo | | File | Scopo |
|---|---| |---|---|
| `first-setup.sh` | Installer ambiente di prima fase: sistema base, utenti, firewall, virtualenv, nginx. | | `bootstrap.sh` | One-liner installer: scarica il repo e avvia first-setup.sh |
| `setup_server.py` | Backend Flask del Web Installer (porta 8888). | | `first-setup.sh` | Installer ambiente base: sistema, utenti, firewall, nginx temp, avvia wizard web |
| `setup.html` | Frontend del Web Installer, wizard in 5 step. | | `setup_server.py` | Backend Python del Web Installer (porta 8888, self-contained) |
| `gen_config.py` | Helper per generazione iniziale di `argos.json`. | | `setup.html` | Frontend del Web Installer — wizard in 6 step |
| `gen_config.py` | Helper per generazione iniziale di `argos.json` |
## Flusso d'installazione ## Flusso d'installazione
1. Il cliente esegue `first-setup.sh` per predisporre l'ambiente base (Ubuntu 24.04). 1. Il cliente esegue il bootstrap (o il clone manuale + first-setup.sh).
2. Al termine dello script viene avviato il Web Installer su `http://IP:8888`. 2. `first-setup.sh` predispone l'ambiente base Ubuntu 24.04 (pacchetti, utente argos, cartelle, firewall) e avvia il Web Installer su `http://IP:8888`.
3. Il wizard completa la configurazione: cliente, rete/SSL, SIEM, utente admin. 3. Il cliente apre il browser e segue il wizard in 6 step:
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. 1. **Licenza ARGOS** — il wizard mostra il `machine_id` di questo server. Il cliente lo invia a Tecnotel, riceve `license.json`, lo carica nel wizard.
2. **Informazioni cliente** (nome, dominio, logo)
3. **Rete & SSL** (hostname, certificato)
4. **SIEM** (OpenSearch)
5. **Utente admin**
6. **Riepilogo & Installazione** — il wizard clona il repository privato `tecnotel/argos` usando il token della licenza, crea virtualenv, configura servizi e avvia ARGOS SOC.
Al termine il Web Installer viene disattivato automaticamente e la porta 8888 chiusa.
## Requisiti
- Ubuntu 24.04 LTS (verificato dallo script)
- Accesso root (sudo)
- Connessione internet verso `argos-update.tecnotelsrl.com`
- Una licenza ARGOS valida emessa da Tecnotel per il `machine_id` del server
## Repository correlati ## Repository correlati
- [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato). - [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato, accessibile solo con licenza).
## Versioning ## Versioning

78
bootstrap.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# ARGOS SOC — Bootstrap Installer (one-liner)
# Tecnotel Servizi SRL — Ubuntu 24.04 LTS
#
# Uso tramite one-liner:
# curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash
#
# Oppure manuale (raccomandato per verifica):
# curl -fsSLo /tmp/argos-bootstrap.sh https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh
# less /tmp/argos-bootstrap.sh # verifica cosa fa
# sudo bash /tmp/argos-bootstrap.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" >&2; exit 1; }
# ── Check privilegi e OS ──────────────────────────────────────────────────────
[[ $EUID -ne 0 ]] && error "Eseguire come root (sudo)."
if [[ ! -f /etc/os-release ]]; then
error "OS non riconosciuto: /etc/os-release mancante."
fi
. /etc/os-release
if [[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]]; then
error "Richiesto Ubuntu 24.04 LTS (trovato: $ID $VERSION_ID)."
fi
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ ARGOS SOC — Bootstrap Installer ║${NC}"
echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}"
echo ""
# ── Variabili ────────────────────────────────────────────────────────────────
SETUP_REPO_URL="https://argos-update.tecnotelsrl.com/tecnotel/argos-setup.git"
SETUP_DIR="/opt/argos-setup-pkg"
# ── Install git se mancante ──────────────────────────────────────────────────
if ! command -v git >/dev/null 2>&1; then
info "Git non installato — installo..."
apt-get update -qq
apt-get install -y -qq git
success "Git installato"
else
info "Git gia' presente ($(git --version | awk '{print $3}'))"
fi
# ── Scarica o aggiorna argos-setup ───────────────────────────────────────────
if [[ -d "$SETUP_DIR/.git" ]]; then
info "argos-setup gia' presente in $SETUP_DIR — aggiorno..."
git -C "$SETUP_DIR" pull --ff-only origin main
success "argos-setup aggiornato"
else
info "Scarico argos-setup da $SETUP_REPO_URL..."
rm -rf "$SETUP_DIR"
git clone --depth=1 "$SETUP_REPO_URL" "$SETUP_DIR"
success "argos-setup scaricato in $SETUP_DIR"
fi
# ── Verifica presenza file attesi ────────────────────────────────────────────
for f in first-setup.sh setup_server.py setup.html; do
if [[ ! -f "$SETUP_DIR/$f" ]]; then
error "File $f mancante in $SETUP_DIR — repo argos-setup incompleto?"
fi
done
# ── Lancia first-setup.sh ────────────────────────────────────────────────────
info "Avvio installer principale..."
echo ""
cd "$SETUP_DIR"
exec bash ./first-setup.sh

59
first-setup.sh Normal file → Executable file
View File

@ -2,7 +2,7 @@
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
# ARGOS SOC — Installer ambiente # ARGOS SOC — Installer ambiente
# Tecnotel Servizi SRL — Ubuntu 24.04 LTS # Tecnotel Servizi SRL — Ubuntu 24.04 LTS
# Uso: sudo bash install.sh # Uso: sudo bash first-setup.sh
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
set -euo pipefail set -euo pipefail
@ -14,20 +14,18 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 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}"; } 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" [[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash first-setup.sh"
. /etc/os-release . /etc/os-release
[[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS" [[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS"
echo "" echo ""
echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}" echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ ARGOS SOC — Installer v1.0.0 ${NC}" echo -e "${BLUE}║ ARGOS SOC — Setup ambiente base${NC}"
echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}" echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}" echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}"
echo "" echo ""
GITEA_REPO="https://3eefb5a2802e8c5a9395396b1bb98e2d5fe46101@argos-update.tecnotelsrl.com:3443/tecnotel/argos.git"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "1. Sistema base" section "1. Sistema base"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
@ -36,7 +34,7 @@ apt-get update -qq
apt-get upgrade -y -qq apt-get upgrade -y -qq
apt-get install -y -qq \ apt-get install -y -qq \
curl wget git vim htop unzip jq \ curl wget git vim htop unzip jq \
python3 python3-pip python3-venv \ python3 python3-pip python3-venv python3-cryptography \
nginx certbot python3-certbot-nginx \ nginx certbot python3-certbot-nginx \
ufw fail2ban \ ufw fail2ban \
build-essential libssl-dev libffi-dev python3-dev \ build-essential libssl-dev libffi-dev python3-dev \
@ -92,33 +90,7 @@ chmod 755 /opt/argos/feeds
success "Struttura /opt/argos/ creata" success "Struttura /opt/argos/ creata"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "5. Clone repository" section "5. Firewall UFW"
# ══════════════════════════════════════════════════════════════════════════════
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 --force reset >/dev/null
ufw default deny incoming >/dev/null ufw default deny incoming >/dev/null
@ -131,13 +103,13 @@ ufw --force enable >/dev/null
success "Firewall UFW configurato" success "Firewall UFW configurato"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "8. Fail2ban" section "6. Fail2ban"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
systemctl enable --now fail2ban >/dev/null 2>&1 systemctl enable --now fail2ban >/dev/null 2>&1
success "Fail2ban attivo" success "Fail2ban attivo"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "9. Nginx temporaneo" section "7. Nginx temporaneo"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
rm -f /etc/nginx/sites-enabled/default rm -f /etc/nginx/sites-enabled/default
cat > /etc/nginx/sites-available/argos-setup << 'NGINX' cat > /etc/nginx/sites-available/argos-setup << 'NGINX'
@ -153,11 +125,18 @@ nginx -t && systemctl restart nginx
success "Nginx temporaneo configurato" success "Nginx temporaneo configurato"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
section "10. Web Installer" section "8. Web Installer"
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
# Copia file setup # Copia file setup dalla directory dello script (argos-setup tarball)
cp /opt/argos/app/scripts/setup_server.py /opt/argos/setup/ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp /opt/argos/app/scripts/setup.html /opt/argos/setup/ for f in setup_server.py setup.html gen_config.py; do
if [[ -f "$SCRIPT_DIR/$f" ]]; then
cp "$SCRIPT_DIR/$f" /opt/argos/setup/
else
error "File $f non trovato in $SCRIPT_DIR — installare argos-setup completo"
fi
done
chown -R root:root /opt/argos/setup
# Systemd service web installer # Systemd service web installer
cat > /etc/systemd/system/argos-setup.service << 'EOF' cat > /etc/systemd/system/argos-setup.service << 'EOF'
@ -169,7 +148,7 @@ After=network.target
Type=simple Type=simple
User=root User=root
WorkingDirectory=/opt/argos/setup WorkingDirectory=/opt/argos/setup
ExecStart=/opt/argos/app/backend/venv/bin/python3 /opt/argos/setup/setup_server.py ExecStart=/usr/bin/python3 /opt/argos/setup/setup_server.py
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
StandardOutput=journal StandardOutput=journal

View File

@ -95,11 +95,12 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div class="main"> <div class="main">
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-title">Configurazione</div> <div class="sidebar-title">Configurazione</div>
<div class="tab-item active" onclick="goTab(0)" id="tab-0"><div class="tab-num">1</div> Cliente</div> <div class="tab-item active" onclick="goTab(0)" id="tab-0"><div class="tab-num">1</div> Licenza</div>
<div class="tab-item" onclick="goTab(1)" id="tab-1"><div class="tab-num">2</div> Rete & SSL</div> <div class="tab-item" onclick="goTab(1)" id="tab-1"><div class="tab-num">2</div> Cliente</div>
<div class="tab-item" onclick="goTab(2)" id="tab-2"><div class="tab-num">3</div> SIEM</div> <div class="tab-item" onclick="goTab(2)" id="tab-2"><div class="tab-num">3</div> Rete & SSL</div>
<div class="tab-item" onclick="goTab(3)" id="tab-3"><div class="tab-num">4</div> Utente admin</div> <div class="tab-item" onclick="goTab(3)" id="tab-3"><div class="tab-num">4</div> SIEM</div>
<div class="tab-item" onclick="goTab(4)" id="tab-4"><div class="tab-num">5</div> Installa</div> <div class="tab-item" onclick="goTab(4)" id="tab-4"><div class="tab-num">5</div> Utente admin</div>
<div class="tab-item" onclick="goTab(5)" id="tab-5"><div class="tab-num">6</div> Installa</div>
</aside> </aside>
<div class="content"> <div class="content">
<div class="progress"> <div class="progress">
@ -108,10 +109,62 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div class="prog-step" id="ps-2"></div> <div class="prog-step" id="ps-2"></div>
<div class="prog-step" id="ps-3"></div> <div class="prog-step" id="ps-3"></div>
<div class="prog-step" id="ps-4"></div> <div class="prog-step" id="ps-4"></div>
<div class="prog-step" id="ps-5"></div>
</div> </div>
<!-- Tab 1: Cliente --> <!-- Tab 1: Licenza ARGOS -->
<div class="tab-panel active" id="panel-0"> <div class="tab-panel active" id="panel-0">
<div class="panel-title">🔑 Licenza ARGOS</div>
<div class="panel-sub">ARGOS SOC richiede una licenza valida emessa da Tecnotel Servizi SRL,
vincolata all'identificativo hardware di questo server.</div>
<div class="form-grid">
<div class="sec-div" style="grid-column:1/-1">1. Identificativo hardware del server</div>
<div class="form-full">
<label class="form-label">Machine ID (SHA256 hex)</label>
<div style="display:flex;gap:8px;align-items:stretch">
<input class="form-input mono-input" id="machine-id-display" value="Calcolo in corso..." readonly style="flex:1">
<button class="btn btn-ghost" onclick="copyMachineId()" id="btn-copy-mid" style="padding:10px 16px">📋 Copia</button>
</div>
<div class="form-hint">Invia questo ID a <strong>Tecnotel Servizi SRL</strong> per ricevere la licenza.
Email: <strong>info@tecnotelsrl.com</strong></div>
</div>
<div class="sec-div" style="grid-column:1/-1">2. Carica license.json ricevuta</div>
<div class="upload-area form-full" id="lic-drop"
ondragover="event.preventDefault();this.classList.add('drag')"
ondragleave="this.classList.remove('drag')"
ondrop="handleLicenseDrop(event)"
onclick="document.getElementById('lic-input').click()">
<input type="file" id="lic-input" accept=".json,application/json" style="display:none" onchange="handleLicenseFile(this)">
<div style="font-size:24px;margin-bottom:6px">📜</div>
<div style="font-size:13px;color:var(--text2)">Trascina license.json o clicca per selezionare</div>
<div style="font-size:11px;color:var(--text3);margin-top:3px">File JSON firmato Ed25519 — max 10KB</div>
</div>
<div id="lic-error" class="form-full" style="display:none;background:var(--err-dim);border:1px solid var(--err);border-radius:var(--r);padding:12px 16px;color:var(--err);font-size:13px"></div>
<div id="lic-summary" class="form-full" style="display:none;background:var(--ok-dim);border:1px solid var(--ok);border-radius:var(--r);padding:16px 20px">
<div style="font-size:14px;font-weight:700;color:var(--ok);margin-bottom:10px">✅ Licenza valida</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:12px;font-family:var(--mono)">
<div style="color:var(--text3)">Cliente:</div> <div id="lic-customer"></div>
<div style="color:var(--text3)">Tier:</div> <div id="lic-tier"></div>
<div style="color:var(--text3)">Hostname:</div> <div id="lic-hostname"></div>
<div style="color:var(--text3)">Emessa:</div> <div id="lic-issued"></div>
<div style="color:var(--text3)">Scadenza:</div> <div id="lic-expires"></div>
<div style="color:var(--text3)">Token Gitea:</div><div id="lic-gitea"></div>
</div>
</div>
</div>
<div class="nav-row">
<div></div>
<button class="btn btn-brand" id="btn-after-license" onclick="goTab(1)" disabled style="opacity:.4;cursor:not-allowed">Avanti →</button>
</div>
</div>
<!-- Tab 2: Cliente -->
<div class="tab-panel" id="panel-1">
<div class="panel-title">👤 Informazioni cliente</div> <div class="panel-title">👤 Informazioni cliente</div>
<div class="panel-sub">Dati dell'organizzazione che utilizzerà ARGOS SOC.</div> <div class="panel-sub">Dati dell'organizzazione che utilizzerà ARGOS SOC.</div>
<div class="form-grid"> <div class="form-grid">
@ -129,11 +182,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div class="upload-preview" id="logo-preview"><img id="logo-img" src=""><div style="font-size:11px;color:var(--ok);font-family:var(--mono)" id="logo-fname"></div></div> <div class="upload-preview" id="logo-preview"><img id="logo-img" src=""><div style="font-size:11px;color:var(--ok);font-family:var(--mono)" id="logo-fname"></div></div>
</div> </div>
</div> </div>
<div class="nav-row"><div></div><button class="btn btn-brand" onclick="goTab(1)">Avanti →</button></div> <div class="nav-row"><div></div><button class="btn btn-brand" onclick="goTab(2)">Avanti →</button></div>
</div> </div>
<!-- Tab 2: Rete & SSL --> <!-- Tab 3: Rete & SSL -->
<div class="tab-panel" id="panel-1"> <div class="tab-panel" id="panel-2">
<div class="panel-title">🌐 Rete & SSL</div> <div class="panel-title">🌐 Rete & SSL</div>
<div class="panel-sub">Configurazione dominio e certificato SSL. Il DNS deve già puntare a questo server.</div> <div class="panel-sub">Configurazione dominio e certificato SSL. Il DNS deve già puntare a questo server.</div>
<div class="form-grid"> <div class="form-grid">
@ -152,11 +205,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
</div> </div>
</div> </div>
</div> </div>
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(0)">← Indietro</button><button class="btn btn-brand" onclick="goTab(2)">Avanti →</button></div> <div class="nav-row"><button class="btn btn-ghost" onclick="goTab(4)">← Indietro</button><button class="btn btn-brand" onclick="goTab(4)">Avanti →</button></div>
</div> </div>
<!-- Tab 3: SIEM --> <!-- Tab 4: SIEM -->
<div class="tab-panel" id="panel-2"> <div class="tab-panel" id="panel-3">
<div class="panel-title">🔍 SIEM — OpenSearch</div> <div class="panel-title">🔍 SIEM — OpenSearch</div>
<div class="panel-sub">Connessione al backend SIEM. Le altre integrazioni si configurano dall'interfaccia ARGOS dopo l'installazione.</div> <div class="panel-sub">Connessione al backend SIEM. Le altre integrazioni si configurano dall'interfaccia ARGOS dopo l'installazione.</div>
<div class="form-grid"> <div class="form-grid">
@ -169,7 +222,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
</div> </div>
<!-- Tab 4: Admin --> <!-- Tab 4: Admin -->
<div class="tab-panel" id="panel-3"> <div class="tab-panel" id="panel-4">
<div class="panel-title">🔑 Utente amministratore</div> <div class="panel-title">🔑 Utente amministratore</div>
<div class="panel-sub">Crea il primo account admin per accedere a ARGOS SOC.</div> <div class="panel-sub">Crea il primo account admin per accedere a ARGOS SOC.</div>
<div class="form-grid"> <div class="form-grid">
@ -183,8 +236,8 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(2)">← Indietro</button><button class="btn btn-brand" onclick="goToInstall()">Avanti →</button></div> <div class="nav-row"><button class="btn btn-ghost" onclick="goTab(2)">← Indietro</button><button class="btn btn-brand" onclick="goToInstall()">Avanti →</button></div>
</div> </div>
<!-- Tab 5: Installa --> <!-- Tab 6: Installa -->
<div class="tab-panel" id="panel-4"> <div class="tab-panel" id="panel-5">
<div class="panel-title">🚀 Riepilogo & Installazione</div> <div class="panel-title">🚀 Riepilogo & Installazione</div>
<div class="panel-sub">Verifica i dati e avvia l'installazione.</div> <div class="panel-sub">Verifica i dati e avvia l'installazione.</div>
@ -235,6 +288,91 @@ let keyUploaded = false;
let installing = false; let installing = false;
let installDomain = ''; let installDomain = '';
// ── Licenza ARGOS: fetch machine_id e gestione upload ────────────────────────
let licenseValid = false;
async function loadMachineId() {
try {
const r = await fetch('/api/machine-id');
const d = await r.json();
const el = document.getElementById('machine-id-display');
if (d && d.machine_id) {
el.value = d.machine_id;
} else {
el.value = 'Errore caricamento';
}
} catch (e) {
document.getElementById('machine-id-display').value = 'Errore: ' + e.message;
}
}
function copyMachineId() {
const el = document.getElementById('machine-id-display');
el.select();
navigator.clipboard.writeText(el.value).then(() => {
const btn = document.getElementById('btn-copy-mid');
const orig = btn.textContent;
btn.textContent = '✅ Copiato';
setTimeout(() => btn.textContent = orig, 1500);
}).catch(() => {});
}
function handleLicenseDrop(ev) {
ev.preventDefault();
ev.currentTarget.classList.remove('drag');
if (ev.dataTransfer.files.length) uploadLicense(ev.dataTransfer.files[0]);
}
function handleLicenseFile(inp) {
if (inp.files.length) uploadLicense(inp.files[0]);
}
async function uploadLicense(file) {
const errEl = document.getElementById('lic-error');
const sumEl = document.getElementById('lic-summary');
const btnNext = document.getElementById('btn-after-license');
errEl.style.display = 'none';
sumEl.style.display = 'none';
try {
const text = await file.text();
const r = await fetch('/api/license/upload', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: text,
});
const d = await r.json();
if (!d.ok) {
errEl.textContent = 'Licenza non valida: ' + (d.error || 'errore sconosciuto');
errEl.style.display = 'block';
licenseValid = false;
btnNext.disabled = true;
btnNext.style.opacity = '.4';
btnNext.style.cursor = 'not-allowed';
return;
}
const s = d.summary || {};
document.getElementById('lic-customer').textContent = s.customer || '—';
document.getElementById('lic-tier').textContent = (s.tier || '—').toUpperCase();
document.getElementById('lic-hostname').textContent = s.issued_to || '—';
document.getElementById('lic-issued').textContent = s.issued_at || '—';
document.getElementById('lic-expires').textContent = s.expires_at || '—';
document.getElementById('lic-gitea').textContent = s.has_gitea ? '✓ Incluso' : '— Non incluso';
sumEl.style.display = 'block';
licenseValid = true;
btnNext.disabled = false;
btnNext.style.opacity = '1';
btnNext.style.cursor = 'pointer';
} catch (e) {
errEl.textContent = 'Errore upload: ' + e.message;
errEl.style.display = 'block';
}
}
// Al caricamento pagina: fetch machine_id
window.addEventListener('DOMContentLoaded', loadMachineId);
function goTab(n) { function goTab(n) {
if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done'); if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
@ -254,7 +392,7 @@ function goToInstall() {
const err = document.getElementById('pw-error'); const err = document.getElementById('pw-error');
if (pw1 !== pw2) { err.style.display = 'block'; return; } if (pw1 !== pw2) { err.style.display = 'block'; return; }
err.style.display = 'none'; err.style.display = 'none';
goTab(4); goTab(5);
buildSummary(); buildSummary();
} }

View File

@ -26,11 +26,121 @@ SETUP_DIR = Path("/opt/argos/setup")
APP_USER = "argos" APP_USER = "argos"
PORT = 8888 PORT = 8888
# ── Licenza ARGOS — pubkey per verifica firma Ed25519 ─────────────────────────
# Stessa pubkey hardcoded in backend/core.py. Raw Ed25519 (32 byte) base64.
# Il setup_server la usa per verificare licenze ricevute dal cliente PRIMA
# di procedere con l'installazione.
_LICENSE_PUBLIC_KEY_B64 = "GMRsZMoxOlCBiJU66EsQcj0ZO0gVd0GHB5LelEo/hns="
# ── Clone argos: username Basic Auth bot Gitea (costante del sistema) ─────────
GITEA_BOT_USER = "argos-portal-bot"
GITEA_REPO_PATH = "/tecnotel/argos.git"
install_log = [] install_log = []
install_done = False install_done = False
install_error = False install_error = False
def get_machine_id() -> str:
"""Fingerprint univoco del server (stesso algoritmo di core.py).
SHA256 hex di: /etc/machine-id | hostname | MAC prima interfaccia fisica.
"""
import socket as _sock
import subprocess as _sp
parts = []
try:
with open("/etc/machine-id") as f:
parts.append(f.read().strip())
except Exception:
parts.append("")
try:
parts.append(_sock.gethostname())
except Exception:
parts.append("")
try:
r = _sp.run(["cat", "/sys/class/net/eth0/address"],
capture_output=True, text=True, timeout=2)
mac = r.stdout.strip()
if not mac or mac == "00:00:00:00:00:00":
r = _sp.run(["ip", "-o", "link", "show"],
capture_output=True, text=True, timeout=2)
for line in r.stdout.splitlines():
if "link/ether" in line and "00:00:00:00:00:00" not in line:
if "docker" in line or "br-" in line or "veth" in line:
continue
mac = line.split("link/ether")[1].split()[0].strip()
break
parts.append(mac or "")
except Exception:
parts.append("")
combined = "|".join(parts)
return hashlib.sha256(combined.encode()).hexdigest()
def verify_license(raw_bytes):
"""Verifica firma Ed25519 + machine_id match + scadenza.
Ritorna tupla (ok, license_dict, error_message).
Se ok=True, license_dict contiene il payload completo (inclusi gitea_url,
gitea_token se presenti). Se ok=False, error_message e' umano-friendly.
"""
try:
raw = json.loads(raw_bytes)
except Exception as e:
return (False, None, f"File non e' JSON valido: {e}")
if not isinstance(raw, dict):
return (False, None, "Formato licenza non riconosciuto")
# Verifica firma
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
import base64
sig = raw.pop("signature", "")
if not sig:
return (False, None, "Licenza senza firma (campo 'signature' mancante)")
payload = json.dumps(raw, sort_keys=True, separators=(",", ":"))
raw["signature"] = sig # ripristina
pub_bytes = base64.b64decode(_LICENSE_PUBLIC_KEY_B64)
pub = Ed25519PublicKey.from_public_bytes(pub_bytes)
try:
pub.verify(base64.b64decode(sig), payload.encode())
except InvalidSignature:
return (False, None, "Firma non valida. La licenza potrebbe essere manipolata o emessa da altro vendor.")
except ImportError:
return (False, None, "Libreria 'cryptography' non disponibile. Installa: apt install python3-cryptography")
except Exception as e:
return (False, None, f"Errore verifica firma: {e}")
# Verifica machine_id
lic_machine = raw.get("machine_id", "")
if not lic_machine:
return (False, None, "Licenza senza machine_id — formato non supportato")
cur_machine = get_machine_id()
if lic_machine != cur_machine:
return (False, None,
f"Machine ID non corrisponde: licenza per {lic_machine[:12]}..., "
f"questo server e' {cur_machine[:12]}... "
"La licenza non e' valida per questa macchina.")
# Verifica scadenza
expires = raw.get("expires_at", "")
if expires:
today = datetime.now().strftime("%Y-%m-%d")
if expires < today:
return (False, None, f"Licenza scaduta il {expires}")
# Verifica presenza credenziali Gitea (la licenza deve averle per permettere clone)
if not raw.get("gitea_url") or not raw.get("gitea_token"):
return (False, None,
"Licenza priva di credenziali Gitea. "
"Contatta Tecnotel per riemetterla con il nuovo formato.")
return (True, raw, "")
def log(msg): def log(msg):
ts = datetime.now().strftime("%H:%M:%S") ts = datetime.now().strftime("%H:%M:%S")
line = f"[{ts}] {msg}" line = f"[{ts}] {msg}"
@ -166,7 +276,51 @@ def install(data):
try: try:
log("=== AVVIO INSTALLAZIONE ARGOS SOC ===") log("=== AVVIO INSTALLAZIONE ARGOS SOC ===")
# 1. argos.json # 0. Carica licenza (gia' validata da /api/license/upload)
log("── Verifica licenza ARGOS ──")
lic_path = SETUP_DIR / "license.json"
if not lic_path.exists():
raise RuntimeError(
"License.json non trovata in /opt/argos/setup/. "
"Caricare una licenza valida prima di avviare l'installazione."
)
ok, lic, err = verify_license(lic_path.read_bytes())
if not ok:
raise RuntimeError(f"Licenza non valida: {err}")
gitea_url = lic.get("gitea_url", "").rstrip("/")
gitea_token = lic.get("gitea_token", "")
# Deriva host dal gitea_url (es: https://host/api/v1 -> https://host)
gitea_host = gitea_url[:-len("/api/v1")] if gitea_url.endswith("/api/v1") else gitea_url
log(f"Licenza OK: {lic.get('customer')} / {lic.get('tier')} / exp {lic.get('expires_at')}")
# 1. Clone repository argos (URL autenticato temporaneo: token NON in .git/config)
log("── Clone repository ARGOS ──")
if APP_DIR.exists() and (APP_DIR / ".git").exists():
log("Repository gia' presente — skip clone")
else:
auth_url = f"https://{GITEA_BOT_USER}:{gitea_token}@{gitea_host[len('https://'):]}{GITEA_REPO_PATH}"
APP_DIR.parent.mkdir(parents=True, exist_ok=True)
# Clona con URL autenticato
run(f"git clone {auth_url} {APP_DIR}")
# Dopo clone, ripulisci .git/config rimuovendo il token
clean_url = f"{gitea_host}{GITEA_REPO_PATH}"
run(f"git -C {APP_DIR} remote set-url origin {clean_url}")
run(f"chown -R {APP_USER}:{APP_USER} {APP_DIR}")
log("Repository ARGOS clonato")
# 2. Python virtualenv
log("── Creazione virtualenv Python ──")
venv_dir = APP_DIR / "backend/venv"
if not venv_dir.exists():
run(f"python3 -m venv {venv_dir}")
run(f"{venv_dir}/bin/pip install --upgrade pip -q")
req_file = APP_DIR / "backend/requirements.txt"
if req_file.exists():
run(f"{venv_dir}/bin/pip install -r {req_file} -q")
run(f"chown -R {APP_USER}:{APP_USER} {venv_dir}")
log("Virtualenv Python pronto")
# 3. argos.json
log("── Generazione argos.json ──") log("── Generazione argos.json ──")
CONFIG_DIR.mkdir(parents=True, exist_ok=True) CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config = generate_argos_json(data) config = generate_argos_json(data)
@ -177,7 +331,7 @@ def install(data):
chown(CONFIG_DIR) chown(CONFIG_DIR)
log("argos.json creato") log("argos.json creato")
# 2. integrations.json # 4. integrations.json
log("── Generazione integrations.json ──") log("── Generazione integrations.json ──")
integ = generate_integrations_json() integ = generate_integrations_json()
# Aggiorna pdf con nome organizzazione # Aggiorna pdf con nome organizzazione
@ -191,14 +345,14 @@ def install(data):
chown(integ_path) chown(integ_path)
log("integrations.json creato") log("integrations.json creato")
# 3. modules.json # 5. modules.json
mods = APP_DIR / "config/modules.json.example" mods = APP_DIR / "config/modules.json.example"
if mods.exists(): if mods.exists():
shutil.copy(mods, CONFIG_DIR / "modules.json") shutil.copy(mods, CONFIG_DIR / "modules.json")
chown(CONFIG_DIR / "modules.json") chown(CONFIG_DIR / "modules.json")
log("modules.json copiato") log("modules.json copiato")
# 4. Logo cliente # 6. Logo cliente
logo_src = SETUP_DIR / "logo_cliente.png" logo_src = SETUP_DIR / "logo_cliente.png"
if logo_src.exists(): if logo_src.exists():
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
@ -206,7 +360,7 @@ def install(data):
chown(CONFIG_DIR / "assets" / "logo_cliente.png") chown(CONFIG_DIR / "assets" / "logo_cliente.png")
log("Logo cliente copiato") log("Logo cliente copiato")
# 5. Build frontend # 7. Build frontend
log("── Build frontend React ──") log("── Build frontend React ──")
run(f"cd {APP_DIR}/frontend && npm install --silent") run(f"cd {APP_DIR}/frontend && npm install --silent")
run(f"cd {APP_DIR}/frontend && npm run build") run(f"cd {APP_DIR}/frontend && npm run build")
@ -216,7 +370,7 @@ def install(data):
run(f"chmod -R 755 {APP_DIR}/frontend/dist/") run(f"chmod -R 755 {APP_DIR}/frontend/dist/")
log("Frontend buildato") log("Frontend buildato")
# 6. Dipendenze Python # 8. Dipendenze Python (re-run in caso di aggiornamenti)
log("── Dipendenze Python ──") log("── Dipendenze Python ──")
venv_pip = APP_DIR / "backend/venv/bin/pip" venv_pip = APP_DIR / "backend/venv/bin/pip"
req = APP_DIR / "backend/requirements.txt" req = APP_DIR / "backend/requirements.txt"
@ -224,11 +378,11 @@ def install(data):
run(f"{venv_pip} install -r {req} -q") run(f"{venv_pip} install -r {req} -q")
log("Dipendenze Python installate") log("Dipendenze Python installate")
# 7. Utente admin # 9. Utente admin
log("── Creazione utente admin ──") log("── Creazione utente admin ──")
create_admin_user(data) create_admin_user(data)
# 8. SSL # 10. SSL
log("── Configurazione SSL ──") log("── Configurazione SSL ──")
domain = data.get("domain", "").strip() domain = data.get("domain", "").strip()
aliases = data.get("aliases", "").strip() aliases = data.get("aliases", "").strip()
@ -257,13 +411,13 @@ def install(data):
ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem" ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem"
log("Certificato Let's Encrypt ottenuto") log("Certificato Let's Encrypt ottenuto")
# 9. Nginx finale # 11. Nginx finale
log("── Nginx configurazione finale ──") log("── Nginx configurazione finale ──")
_write_nginx_final(all_names, ssl_crt, ssl_key) _write_nginx_final(all_names, ssl_crt, ssl_key)
run("nginx -t && systemctl restart nginx") run("nginx -t && systemctl restart nginx")
log("Nginx configurato") log("Nginx configurato")
# 10. Systemd services # 12. Systemd services
log("── Creazione e avvio servizi ──") log("── Creazione e avvio servizi ──")
_write_services() _write_services()
run("systemctl daemon-reload") run("systemctl daemon-reload")
@ -271,7 +425,15 @@ def install(data):
run(f"systemctl enable --now {svc}") run(f"systemctl enable --now {svc}")
log(f"{svc} avviato") log(f"{svc} avviato")
# 11. Chiudi web installer # 13. Copia licenza + chiudi web installer
log("── Copia licenza in posizione finale ──")
DATA_DIR.mkdir(parents=True, exist_ok=True)
final_lic = DATA_DIR / "license.json"
shutil.copy(lic_path, final_lic)
os.chmod(final_lic, 0o600)
chown(final_lic)
log(f"Licenza copiata in {final_lic}")
log("── Chiusura web installer ──") log("── Chiusura web installer ──")
run("systemctl disable --now argos-setup", check=False) run("systemctl disable --now argos-setup", check=False)
run("ufw delete allow 8888/tcp", check=False) run("ufw delete allow 8888/tcp", check=False)
@ -435,6 +597,8 @@ class SetupHandler(BaseHTTPRequestHandler):
self.wfile.write(html) self.wfile.write(html)
elif path == "/api/status": elif path == "/api/status":
self._json({"done": install_done, "error": install_error, "log": install_log[-60:]}) self._json({"done": install_done, "error": install_error, "log": install_log[-60:]})
elif path == "/api/machine-id":
self._json({"machine_id": get_machine_id()})
else: else:
self.send_response(404); self.end_headers() self.send_response(404); self.end_headers()
@ -462,6 +626,28 @@ class SetupHandler(BaseHTTPRequestHandler):
SETUP_DIR.mkdir(parents=True, exist_ok=True) SETUP_DIR.mkdir(parents=True, exist_ok=True)
(SETUP_DIR / "logo_cliente.png").write_bytes(body) (SETUP_DIR / "logo_cliente.png").write_bytes(body)
self._json({"ok": True}) self._json({"ok": True})
elif path == "/api/license/upload":
ok, lic, err = verify_license(body)
if not ok:
self._json({"ok": False, "error": err}, 400)
return
# Salva licenza valida nel SETUP_DIR per essere usata durante install()
SETUP_DIR.mkdir(parents=True, exist_ok=True)
lic_path = SETUP_DIR / "license.json"
lic_path.write_bytes(body)
os.chmod(lic_path, 0o600)
# Risposta: solo dati sommari (non rinvia token in plain)
self._json({
"ok": True,
"summary": {
"customer": lic.get("customer", ""),
"tier": lic.get("tier", ""),
"issued_to": lic.get("issued_to", ""),
"issued_at": lic.get("issued_at", ""),
"expires_at": lic.get("expires_at", ""),
"has_gitea": bool(lic.get("gitea_token")),
}
})
else: else:
self.send_response(404); self.end_headers() self.send_response(404); self.end_headers()