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.
This commit is contained in:
Tecnotel 2026-04-20 10:28:40 +02:00
commit 7d2c1d8809
5 changed files with 1240 additions and 0 deletions

29
README.md Normal file
View File

@ -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`).

199
first-setup.sh Normal file
View File

@ -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 ""

110
gen_config.py Normal file
View File

@ -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}")

419
setup.html Normal file
View File

@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARGOS SOC — Installazione</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Barlow:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#080a0e;--bg2:#0e1117;--bg3:#161b24;--bg4:#1c2333;
--border:#1e2840;--border2:#2a3a58;
--text:#e2e8f4;--text2:#8a9ab8;--text3:#445068;
--brand:#F09000;--brand2:#cc7a00;--brand-dim:#F0900018;
--ok:#10b981;--ok-dim:#10b98118;--err:#ef4444;--err-dim:#ef444418;--warn:#f59e0b;
--mono:'JetBrains Mono',monospace;--sans:'Barlow',sans-serif;--r:10px;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.6}
.shell{min-height:100vh;display:grid;grid-template-rows:auto 1fr auto}
.hdr{background:var(--bg2);border-bottom:1px solid var(--border);padding:16px 32px;display:flex;align-items:center;gap:16px}
.hdr-logo{font-size:22px;font-weight:800;color:var(--brand);letter-spacing:3px}
.hdr-sep{width:1px;height:24px;background:var(--border2)}
.hdr-sub{font-size:12px;color:var(--text3);font-family:var(--mono);text-transform:uppercase;letter-spacing:1.5px}
.hdr-badge{margin-left:auto;font-size:10px;font-family:var(--mono);background:var(--brand-dim);color:var(--brand);padding:4px 10px;border-radius:20px;border:1px solid #F0900033}
.main{display:flex;max-width:1000px;margin:0 auto;width:100%;padding:32px 24px;gap:28px}
.sidebar{width:200px;flex-shrink:0}
.sidebar-title{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;padding:0 12px 12px;font-family:var(--mono)}
.tab-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--r);cursor:pointer;transition:all .15s;color:var(--text2);font-size:13px;font-weight:500;margin-bottom:2px;border:1px solid transparent}
.tab-item:hover{background:var(--bg3);color:var(--text)}
.tab-item.active{background:var(--bg3);color:var(--brand);border-color:var(--border2)}
.tab-item.done .tab-num{background:var(--ok);color:#000}
.tab-item.active .tab-num{background:var(--brand);color:#000}
.tab-num{width:22px;height:22px;border-radius:50%;background:var(--bg4);color:var(--text3);font-size:10px;font-weight:700;font-family:var(--mono);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s}
.content{flex:1;min-width:0}
.tab-panel{display:none}
.tab-panel.active{display:block}
.panel-title{font-size:22px;font-weight:700;margin-bottom:4px}
.panel-sub{font-size:13px;color:var(--text2);margin-bottom:24px}
.progress{display:flex;gap:6px;margin-bottom:28px}
.prog-step{flex:1;height:3px;border-radius:2px;background:var(--border2);transition:background .3s}
.prog-step.done{background:var(--ok)}
.prog-step.active{background:var(--brand)}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.form-full{grid-column:1/-1}
.form-label{display:block;font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px}
.form-label span{color:var(--err);margin-left:2px}
.form-input,.form-select{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);padding:10px 14px;color:var(--text);font-family:var(--sans);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}
.form-input:focus,.form-select:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-dim)}
.form-input::placeholder{color:var(--text3)}
.mono-input{font-family:var(--mono);font-size:12px}
.form-hint{font-size:11px;color:var(--text3);margin-top:5px}
.sec-div{grid-column:1/-1;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;font-family:var(--mono);border-top:1px solid var(--border);padding-top:18px;margin-top:4px}
.sec-div:first-child{border-top:none;padding-top:0;margin-top:0}
.ssl-toggle{grid-column:1/-1;display:flex;gap:8px}
.ssl-btn{flex:1;padding:12px 16px;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);cursor:pointer;text-align:center;font-size:13px;font-weight:600;color:var(--text2);transition:all .15s}
.ssl-btn.active{background:var(--brand-dim);border-color:var(--brand);color:var(--brand)}
.ssl-btn small{display:block;font-weight:400;font-size:11px;opacity:.7;margin-top:2px}
.upload-area{grid-column:1/-1;border:2px dashed var(--border2);border-radius:var(--r);padding:20px;text-align:center;cursor:pointer;transition:all .15s}
.upload-area:hover,.upload-area.drag{border-color:var(--brand);background:var(--brand-dim)}
.upload-preview{display:none;align-items:center;justify-content:center;gap:10px;margin-top:10px}
.upload-preview img{max-height:50px;max-width:180px;border-radius:6px}
.alert{padding:12px 16px;border-radius:var(--r);font-size:13px;margin-bottom:0}
.alert-err{background:var(--err-dim);border:1px solid #ef444433;color:var(--err)}
.alert-ok{background:var(--ok-dim);border:1px solid #10b98133;color:var(--ok)}
.alert-warn{background:#f59e0b15;border:1px solid #f59e0b33;color:var(--warn)}
.nav-row{display:flex;justify-content:space-between;align-items:center;margin-top:28px;padding-top:20px;border-top:1px solid var(--border)}
.btn{padding:10px 22px;border-radius:var(--r);border:none;font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:8px}
.btn-brand{background:var(--brand);color:#000}
.btn-brand:hover{background:var(--brand2)}
.btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border2)}
.btn-ghost:hover{background:var(--bg3);color:var(--text)}
.btn-ok{background:var(--ok);color:#000;font-size:15px;padding:14px 36px}
.btn-ok:hover{filter:brightness(1.1)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.summary-table{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;margin-bottom:16px}
.summary-section{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1px;padding:10px 14px;font-family:var(--mono);background:var(--bg4);border-bottom:1px solid var(--border)}
.summary-row{display:flex;border-bottom:1px solid #1e284044}
.summary-row:last-child{border-bottom:none}
.summary-label{width:160px;padding:8px 14px;font-size:12px;color:var(--text3);flex-shrink:0}
.summary-value{padding:8px 14px;font-size:12px;color:var(--text);font-family:var(--mono);flex:1}
.summary-value.empty{color:var(--text3);font-style:italic;font-family:var(--sans)}
.footer{background:var(--bg2);border-top:1px solid var(--border);padding:12px 32px;text-align:center;font-size:11px;color:var(--text3);font-family:var(--mono)}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:.3;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
</style>
</head>
<body>
<div class="shell">
<header class="hdr">
<div class="hdr-logo">ARGOS</div>
<div class="hdr-sep"></div>
<div class="hdr-sub">Setup Wizard — Tecnotel Servizi SRL</div>
<div class="hdr-badge">v1.0.0</div>
</header>
<div class="main">
<aside class="sidebar">
<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" onclick="goTab(1)" id="tab-1"><div class="tab-num">2</div> Rete & SSL</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(3)" id="tab-3"><div class="tab-num">4</div> Utente admin</div>
<div class="tab-item" onclick="goTab(4)" id="tab-4"><div class="tab-num">5</div> Installa</div>
</aside>
<div class="content">
<div class="progress">
<div class="prog-step active" id="ps-0"></div>
<div class="prog-step" id="ps-1"></div>
<div class="prog-step" id="ps-2"></div>
<div class="prog-step" id="ps-3"></div>
<div class="prog-step" id="ps-4"></div>
</div>
<!-- Tab 1: Cliente -->
<div class="tab-panel active" id="panel-0">
<div class="panel-title">👤 Informazioni cliente</div>
<div class="panel-sub">Dati dell'organizzazione che utilizzerà ARGOS SOC.</div>
<div class="form-grid">
<div><label class="form-label">Nome breve <span>*</span></label><input class="form-input" id="cliente_name" placeholder="es. NomeAzienda"><div class="form-hint">Usato nei titoli e intestazioni</div></div>
<div><label class="form-label">Tipo organizzazione</label><select class="form-select" id="cliente_type"><option value="enterprise">Azienda / Enterprise</option><option value="healthcare">Sanità / Healthcare</option><option value="pa">Pubblica Amministrazione</option><option value="education">Istruzione</option></select></div>
<div class="form-full"><label class="form-label">Nome completo</label><input class="form-input" id="cliente_full" placeholder="es. Nome Azienda Srl"></div>
<div><label class="form-label">Dominio email <span>*</span></label><input class="form-input" id="cliente_domain" placeholder="es. azienda.it"><div class="form-hint">Dominio utenti (es. nome@azienda.it)</div></div>
<div><label class="form-label">SharePoint tenant</label><input class="form-input" id="sp_tenant" placeholder="es. nomeazienda"><div class="form-hint">Solo se Microsoft 365. Vuoto se non usato.</div></div>
<div class="sec-div">Logo cliente (per i PDF report)</div>
<div class="upload-area form-full" id="logo-drop" ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')" ondrop="handleLogoDrop(event)" onclick="document.getElementById('logo-input').click()">
<input type="file" id="logo-input" accept="image/*" style="display:none" onchange="handleLogoFile(this)">
<div style="font-size:24px;margin-bottom:6px">🖼️</div>
<div style="font-size:13px;color:var(--text2)">Trascina il logo o clicca per selezionare</div>
<div style="font-size:11px;color:var(--text3);margin-top:3px">PNG, JPG — max 2MB — opzionale</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 class="nav-row"><div></div><button class="btn btn-brand" onclick="goTab(1)">Avanti →</button></div>
</div>
<!-- Tab 2: Rete & SSL -->
<div class="tab-panel" id="panel-1">
<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="form-grid">
<div class="form-full"><label class="form-label">Hostname principale <span>*</span></label><input class="form-input mono-input" id="domain" placeholder="es. soc.azienda.it"></div>
<div class="form-full"><label class="form-label">Nomi alternativi</label><input class="form-input mono-input" id="aliases" placeholder="es. argos.azienda.it soc2.azienda.it"><div class="form-hint">Separati da spazio — opzionale</div></div>
<div class="sec-div">Certificato SSL</div>
<div class="ssl-toggle">
<div class="ssl-btn active" id="ssl-le" onclick="setSsl('letsencrypt')">🔒 Let's Encrypt<small>Automatico e gratuito</small></div>
<div class="ssl-btn" id="ssl-manual" onclick="setSsl('manual')">📄 Certificato esistente<small>Carica .crt e .key</small></div>
</div>
<div id="le-block" class="form-full"><label class="form-label">Email amministratore <span>*</span></label><input class="form-input" id="admin_email" placeholder="admin@tecnotelsrl.com"><div class="form-hint">Per notifiche di scadenza certificato</div></div>
<div id="manual-block" class="form-full" style="display:none">
<div class="form-grid">
<div><label class="form-label">File .crt <span>*</span></label><input type="file" class="form-input" accept=".crt,.pem,.cer" onchange="uploadSsl(this,'cert')"><div class="form-hint" id="crt-status">Nessun file selezionato</div></div>
<div><label class="form-label">File .key <span>*</span></label><input type="file" class="form-input" accept=".key,.pem" onchange="uploadSsl(this,'key')"><div class="form-hint" id="key-status">Nessun file selezionato</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>
<!-- Tab 3: SIEM -->
<div class="tab-panel" id="panel-2">
<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="form-grid">
<div class="form-full"><label class="form-label">URL OpenSearch <span>*</span></label><input class="form-input mono-input" id="os_url" placeholder="https://10.0.0.30:9200"></div>
<div><label class="form-label">Username</label><input class="form-input" id="os_user" placeholder="admin" value="admin"></div>
<div><label class="form-label">Password <span>*</span></label><input class="form-input" type="password" id="os_pass" placeholder="••••••••"></div>
<div class="form-full"><div class="alert alert-warn"> Le integrazioni (ESET, FortiGate, Entra ID, ecc.) si configurano dopo il primo accesso dalla sezione <strong>Integrazioni</strong> di ARGOS.</div></div>
</div>
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(1)">← Indietro</button><button class="btn btn-brand" onclick="goTab(3)">Avanti →</button></div>
</div>
<!-- Tab 4: Admin -->
<div class="tab-panel" id="panel-3">
<div class="panel-title">🔑 Utente amministratore</div>
<div class="panel-sub">Crea il primo account admin per accedere a ARGOS SOC.</div>
<div class="form-grid">
<div><label class="form-label">Username <span>*</span></label><input class="form-input" id="admin_username" placeholder="admin" value="admin"></div>
<div><label class="form-label">Email</label><input class="form-input" id="admin_email_user" placeholder="admin@azienda.it"></div>
<div><label class="form-label">Password <span>*</span></label><input class="form-input" type="password" id="admin_password" placeholder="Min. 8 caratteri"></div>
<div><label class="form-label">Conferma password <span>*</span></label><input class="form-input" type="password" id="admin_password2" placeholder="Ripeti la password"></div>
<div class="form-full" id="pw-error" style="display:none"><div class="alert alert-err">Le password non corrispondono</div></div>
<div class="form-full"><div class="alert alert-ok">✓ Potrai creare altri utenti con ruoli diversi dall'interfaccia ARGOS dopo l'installazione.</div></div>
</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>
<!-- Tab 5: Installa -->
<div class="tab-panel" id="panel-4">
<div class="panel-title">🚀 Riepilogo & Installazione</div>
<div class="panel-sub">Verifica i dati e avvia l'installazione.</div>
<!-- Riepilogo -->
<div id="summary-box"></div>
<!-- Errori validazione -->
<div id="errors-box" style="display:none"></div>
<!-- Spinner installazione -->
<div id="spinner-box" style="display:none;text-align:center;padding:40px 20px">
<div style="font-size:40px;margin-bottom:16px">⚙️</div>
<div style="font-size:18px;font-weight:700;margin-bottom:8px">Installazione in corso...</div>
<div style="font-size:13px;color:var(--text2);margin-bottom:24px">Attendere il completamento. Richiede circa 5-10 minuti.</div>
<div style="display:flex;justify-content:center;gap:8px;margin-bottom:20px">
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0s infinite"></div>
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0.4s infinite"></div>
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0.8s infinite"></div>
</div>
<div style="font-size:12px;color:var(--text3);font-family:var(--mono)" id="spinner-status">Avvio installazione...</div>
</div>
<!-- Successo con tasto -->
<div id="success-box" style="display:none;text-align:center;padding:40px 20px">
<div style="font-size:56px;margin-bottom:16px"></div>
<div style="font-size:24px;font-weight:800;color:var(--ok);margin-bottom:8px">Installazione completata!</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:24px">ARGOS SOC è operativo e pronto all'uso.</div>
<button class="btn btn-ok" id="btn-open" onclick="openArgos()">Apri ARGOS SOC →</button>
</div>
<!-- Nav -->
<div class="nav-row" id="install-nav">
<button class="btn btn-ghost" onclick="goTab(3)">← Indietro</button>
<button class="btn btn-ok" id="btn-install" onclick="avviaInstallazione()">🚀 Avvia installazione</button>
</div>
</div>
</div>
</div>
<footer class="footer">ARGOS SOC — Tecnotel Servizi SRL &nbsp;·&nbsp; Web Installer v1.0.0 &nbsp;·&nbsp; La porta 8888 verrà chiusa al termine</footer>
</div>
<script>
let currentTab = 0;
let sslMode = 'letsencrypt';
let certUploaded = false;
let keyUploaded = false;
let installing = false;
let installDomain = '';
function goTab(n) {
if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.prog-step').forEach((s,i) => {
s.classList.remove('active','done');
if (i < n) s.classList.add('done');
else if (i === n) s.classList.add('active');
});
document.getElementById('panel-' + n).classList.add('active');
document.getElementById('tab-' + n).classList.add('active');
currentTab = n;
}
function goToInstall() {
const pw1 = g('admin_password'), pw2 = g('admin_password2');
const err = document.getElementById('pw-error');
if (pw1 !== pw2) { err.style.display = 'block'; return; }
err.style.display = 'none';
goTab(4);
buildSummary();
}
function setSsl(mode) {
sslMode = mode;
document.getElementById('ssl-le').classList.toggle('active', mode === 'letsencrypt');
document.getElementById('ssl-manual').classList.toggle('active', mode === 'manual');
document.getElementById('le-block').style.display = mode === 'letsencrypt' ? 'block' : 'none';
document.getElementById('manual-block').style.display = mode === 'manual' ? 'block' : 'none';
}
async function uploadSsl(input, type) {
const file = input.files[0]; if (!file) return;
const sid = type === 'cert' ? 'crt-status' : 'key-status';
try {
const r = await fetch('/api/upload/' + type, { method: 'POST', body: await file.arrayBuffer() });
const d = await r.json();
if (d.ok) {
document.getElementById(sid).textContent = '✓ ' + file.name;
document.getElementById(sid).style.color = 'var(--ok)';
if (type === 'cert') certUploaded = true; else keyUploaded = true;
}
} catch(e) { document.getElementById(sid).textContent = 'Errore: ' + e.message; }
}
function handleLogoDrop(e) { e.preventDefault(); document.getElementById('logo-drop').classList.remove('drag'); processLogo(e.dataTransfer.files[0]); }
function handleLogoFile(input) { processLogo(input.files[0]); }
async function processLogo(file) {
if (!file || file.size > 2*1024*1024) return;
const reader = new FileReader();
reader.onload = e => {
document.getElementById('logo-img').src = e.target.result;
document.getElementById('logo-fname').textContent = file.name;
document.getElementById('logo-preview').style.display = 'flex';
};
reader.readAsDataURL(file);
fetch('/api/upload/logo', { method: 'POST', body: await file.arrayBuffer() });
}
function g(id) { return (document.getElementById(id)?.value || '').trim(); }
function collectData() {
return {
cliente_name: g('cliente_name'), cliente_full: g('cliente_full'),
cliente_domain: g('cliente_domain'), cliente_type: g('cliente_type'),
sp_tenant: g('sp_tenant'), domain: g('domain'), aliases: g('aliases'),
ssl_mode: sslMode, admin_email: g('admin_email'),
os_url: g('os_url'), os_user: g('os_user') || 'admin', os_pass: g('os_pass'),
admin_username: g('admin_username') || 'admin',
admin_email_user: g('admin_email_user'), admin_password: g('admin_password'),
};
}
function buildSummary() {
const d = collectData();
const errors = [];
if (!d.cliente_name) errors.push('Nome cliente obbligatorio');
if (!d.cliente_domain) errors.push('Dominio email obbligatorio');
if (!d.domain) errors.push('Hostname obbligatorio');
if (!d.os_url) errors.push('URL OpenSearch obbligatorio');
if (!d.os_pass) errors.push('Password OpenSearch obbligatoria');
if (!d.admin_password) errors.push('Password admin obbligatoria');
if (sslMode === 'letsencrypt' && !d.admin_email) errors.push("Email per Let's Encrypt obbligatoria");
if (sslMode === 'manual' && (!certUploaded || !keyUploaded)) errors.push('Certificato SSL (.crt e .key) non caricato');
const row = (l, v) => '<div class="summary-row"><div class="summary-label">' + l + '</div><div class="summary-value ' + (v ? '' : 'empty') + '">' + (v || 'non configurato') + '</div></div>';
document.getElementById('summary-box').innerHTML = '<div class="summary-table">' +
'<div class="summary-section">Cliente</div>' +
row('Nome', d.cliente_name) + row('Nome completo', d.cliente_full) +
row('Dominio email', d.cliente_domain) + row('Tipo', d.cliente_type) +
'<div class="summary-section">Rete & SSL</div>' +
row('Hostname', d.domain) + row('Alias', d.aliases) +
row('SSL', sslMode === 'letsencrypt' ? "Let's Encrypt" : 'Certificato manuale') +
'<div class="summary-section">SIEM</div>' +
row('OpenSearch URL', d.os_url) + row('Username', d.os_user) +
'<div class="summary-section">Utente admin</div>' +
row('Username', d.admin_username) + row('Email', d.admin_email_user) +
'</div>';
const errBox = document.getElementById('errors-box');
if (errors.length) {
errBox.style.display = 'block';
errBox.innerHTML = '<div class="alert alert-err" style="margin-bottom:16px">⚠️ <strong>Correggere prima di procedere:</strong><ul style="margin-top:8px;padding-left:16px">' + errors.map(e => '<li style="margin-top:4px">' + e + '</li>').join('') + '</ul></div>';
document.getElementById('btn-install').disabled = true;
} else {
errBox.style.display = 'none';
document.getElementById('btn-install').disabled = false;
}
}
async function avviaInstallazione() {
if (installing) return;
installing = true;
const data = collectData();
installDomain = data.domain;
document.getElementById('summary-box').style.display = 'none';
document.getElementById('errors-box').style.display = 'none';
document.getElementById('install-nav').style.display = 'none';
document.getElementById('spinner-box').style.display = 'block';
try {
const r = await fetch('/api/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const res = await r.json();
if (!res.ok) throw new Error(res.error || 'Errore sconosciuto');
pollStatus();
} catch(e) {
document.getElementById('spinner-status').textContent = 'ERRORE: ' + e.message;
document.getElementById('spinner-status').style.color = 'var(--err)';
installing = false;
}
}
function pollStatus() {
let errors = 0;
const statusEl = document.getElementById('spinner-status');
const steps = ['Generazione configurazione...','Build frontend React...','Installazione dipendenze...','Creazione utente admin...','Configurazione SSL...','Configurazione nginx...','Avvio servizi...','Finalizzazione...'];
let step = 0;
statusEl.textContent = steps[0];
const stepIv = setInterval(() => { if (step < steps.length-1) statusEl.textContent = steps[++step]; }, 20000);
const iv = setInterval(async () => {
try {
const r = await fetch('/api/status');
const d = await r.json();
errors = 0;
if (d.log && d.log.length) {
const last = d.log[d.log.length-1];
if (last.includes('frontend')) statusEl.textContent = 'Build frontend React...';
else if (last.includes('Python')) statusEl.textContent = 'Installazione dipendenze...';
else if (last.includes('admin')) statusEl.textContent = 'Creazione utente admin...';
else if (last.includes('SSL') || last.includes('certbot')) statusEl.textContent = 'Configurazione SSL...';
else if (last.includes('nginx')) statusEl.textContent = 'Configurazione nginx...';
else if (last.includes('avviato')) statusEl.textContent = 'Avvio servizi...';
else if (last.includes('COMPLETATA')) statusEl.textContent = 'Completato!';
}
if (d.done) { clearInterval(iv); clearInterval(stepIv); showSuccess(); }
if (d.error) { clearInterval(iv); clearInterval(stepIv); statusEl.textContent = 'Errore — controlla i log del server'; statusEl.style.color = 'var(--err)'; }
} catch(e) {
errors++;
if (errors >= 6) { clearInterval(iv); clearInterval(stepIv); showSuccess(); }
}
}, 2000);
}
function showSuccess() {
document.getElementById('spinner-box').style.display = 'none';
document.getElementById('success-box').style.display = 'block';
}
function openArgos() {
window.location.href = 'https://' + installDomain;
}
</script>
</body>
</html>

483
setup_server.py Normal file
View File

@ -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://<IP_SERVER>:{PORT}")
print(f"{'='*55}\n")
HTTPServer(("0.0.0.0", PORT), SetupHandler).serve_forever()