diff --git a/.gitignore b/.gitignore index e4851281..6ad48659 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ logs/ tmp/ .temp/ .cache/ +scripts/.agent_token +scripts/.agent_token diff --git a/adn/00_bitacora.md b/adn/00_bitacora.md index 4a2088e0..67d761a3 100644 --- a/adn/00_bitacora.md +++ b/adn/00_bitacora.md @@ -4,6 +4,16 @@ Este documento registra la evolución, cambios y eventos significativos en el se > **Referencias:** ✅ Completado | ⏳ En Proceso | 🧪 Verificado | ⚠️ Alerta | 📍 Definido +## 2026-02-18 + +| Hora | Nodo | Detalle | +| :--- | :--- | :--- | +| 00:55 | Grid | ✅ **Documentación ONTO**: Se actualiza `01_ontologia.md` y se crean fichas para `srv-xen01` y `cam-hikvision-24`. | +| 00:45 | cam-hikvision-24 | ⚠️ **Sonda de Campo**: **Detección HTTP**. IP `10.0.10.24`. Puerto 80 abierto (Hikvision Login), SSH cerrado. Rol: Cámara/DVR. | +| 00:35 | srv-xen01 | ✅ **Sonda de Campo**: **Corrección de Rol**. IP `10.0.10.23`. Identificado como **Citrix XenServer 7.0** (previamente catalogado como Debian). Evidencia forense de procesos Xen y Open vSwitch. | +| 00:10 | Grid | 🔒 **Protocolo de Seguridad**: Implementación de **Candado JIT** (`scripts/candado.rb`). Autorización temporal (5 min) basada en desafío de passphrase SSH. El Agente requiere token activo para operar sondas. | +| 00:00 | Grid | 🔧 **Automatización**: Despliegue de `scripts/alta_nodo.rb` (Ruby). Sonda de relevamiento autónoma con detección de roles y soporte SSH legacy. | + ## 2026-02-16 | Hora | Nodo | Detalle | diff --git a/adn/01_ontologia.md b/adn/01_ontologia.md index d5400c1e..8d48ee10 100644 --- a/adn/01_ontologia.md +++ b/adn/01_ontologia.md @@ -23,6 +23,8 @@ Este documento define la identidad, propósito y componentes principales del ser - **[srvv-UPTIME](nodos/srvv-UPTIME.md)** (`10.0.10.9`): LXC Debian 12 / Monitorización (⚠️ Disk 77%). - **[srvv-DATA](nodos/srvv-DATA.md)** (`10.0.10.13`): LXC Debian 12 / Datos. - **[srvv-SITIO2](nodos/srvv-SITIO2.md)** (`10.0.10.19`): LXC Debian 12 / Web Secundario (Acceso `root`). +- **[srv-xen01](nodos/srv-xen01.md)** (`10.0.10.23`): **Citrix XenServer 7.0** / Hypervisor (4 Cores Xeon). +- **[cam-hikvision-24](nodos/cam-hikvision-24.md)** (`10.0.10.24`): Disp. Hikvision / Cámara o DVR (Detectado vía HTTP). - **[srvv-DNS](nodos/srvv-DNS.md)** (`10.0.10.2`): VM Debian 12 / Servidor DNS. diff --git a/adn/nodos/cam-hikvision-24.md b/adn/nodos/cam-hikvision-24.md new file mode 100644 index 00000000..fd4813e1 --- /dev/null +++ b/adn/nodos/cam-hikvision-24.md @@ -0,0 +1,18 @@ +# Nodo: cam-hikvision-24 (Detectado) + +## Información General +- **Hostname**: `cam-hikvision-24` (Asignado provisionalmente) +- **IP**: `10.0.10.24` +- **Rol**: **Cámara IP / DVR** (Hipótesis Alta) +- **Sistema Operativo**: Firmware Propietario (Hikvision Embedded) +- **Estado**: 🟢 Online (HTTP Accesible) + +## Evidencia de Sondeo +- **SSH**: 🔴 Cerrado / Rechazado. +- **HTTP (Puerto 80)**: 🟢 Abierto. + - **Firma Web**: Redirección a `/doc/page/login.asp`. + - **Fabricante Probable**: Hikvision. + +## Observaciones +- Este nodo no admite gestión SSH estándar. +- Se recomienda aislar en VLAN de videovigilancia si es posible. diff --git a/adn/nodos/srv-xen01.md b/adn/nodos/srv-xen01.md new file mode 100644 index 00000000..e4f6f124 --- /dev/null +++ b/adn/nodos/srv-xen01.md @@ -0,0 +1,30 @@ +# Nodo: srv-xen01 + +## Información General +- **Hostname**: `srv-xen01` +- **IP**: `10.0.10.23` +- **Rol**: **Citrix XenServer Hypervisor** (Detectado) +- **Sistema Operativo**: XenServer 7.0.0-125380c +- **Kernel**: Linux (Dom0) +- **Estado**: 🟢 Online (Relevado el 2026-02-18) + +## Hardware (Dom0) +- **CPU**: Intel(R) Xeon(R) CPU E31230 @ 3.20GHz (4 Cores) +- **RAM**: ~658 MB (Asignada a Dom0) / Total Físico Desconocido (Probable 16GB+ dado el rol) +- **Almacenamiento**: + - `sda`: 298,1G (WDC WD3200AAKS-0) + - `sdb`: 931,5G (WDC WD10EZEX-00W) - Posible Storage Repository (SR). + - `sdc`: 931,5G (WDC WD10EZEX-00W) - Posible Storage Repository (SR). + - `tda`: 120G + - `tdb`: 64G + +## Evidencia Forense (Roles Detectados) +- **Hypervisor**: ✅ **Citrix XenServer** (Marcadores: `/etc/xen`, `/opt/xensource/bin/xe`). +- **Gestión**: `xapi` (XenAPI), `xenopsd`. +- **Red**: `ovs-vswitchd` (Open vSwitch). +- **Puertos**: 80, 443 (Gestión Web/API), 22 (SSH), 5900-5902 (VNC Consoles para VMs). + +## Bitácora de Alta +- **Fecha**: 2026-02-18 +- **Acción**: Re-Sondeo con Script `alta_nodo.rb`. +- **Descubrimiento**: El nodo fue identificado previamente como Debian 12 (falso positivo o re-instalación), pero la evidencia actual confirma **Citrix XenServer 7.0**. diff --git a/scripts/alta_nodo.rb b/scripts/alta_nodo.rb new file mode 100755 index 00000000..21af0f47 --- /dev/null +++ b/scripts/alta_nodo.rb @@ -0,0 +1,231 @@ +#!/usr/bin/env ruby +# script: alta_nodo.rb +# Uso: ruby scripts/alta_nodo.rb [USUARIO] +# Descripción: Automatiza la configuración SSH y la documentación del ADN para un nuevo nodo. + +require 'fileutils' +require 'date' +require 'json' +require 'time' + +# --- Configuración --- +ADN_DIR = File.expand_path('../adn', __dir__) +NODOS_DIR = File.join(ADN_DIR, 'nodos') +ONTOLOGIA_FILE = File.join(ADN_DIR, '01_ontologia.md') +SSH_CONFIG_FILE = File.expand_path('~/.ssh/config') +SSH_PUB_KEY = File.expand_path('~/.ssh/id_rsa.pub') + +# --- Argumentos --- +ip = ARGV[0] +user = ARGV[1] || 'rmonla' + +unless ip + puts "❌ Error: Falta la IP." + puts "Uso correcto: ruby scripts/alta_nodo.rb [USUARIO]" + puts "Ejemplo: ruby scripts/alta_nodo.rb 10.0.10.205 rmonla" + exit 1 +end + +puts "--- 🚀 Iniciando Alta de Nodo: #{ip} con Usuario: #{user} ---" + +# --- 🚨 PROTOCOLO DE SEGURIDAD (CANDADO JIT) --- +TOKEN_FILE = File.expand_path('../.agent_token', __dir__) + +def verificar_autorizacion! + unless File.exist?(TOKEN_FILE) + puts "\n⛔ ACCESO DENEGADO: No se encontró token de autorización." + puts "🔒 Protocolo de seguridad activo. El Agente requiere permiso explícito." + puts "👉 Ejecuta: ruby scripts/candado.rb" + exit 1 + end + + begin + token = JSON.parse(File.read(TOKEN_FILE)) + expires_at = Time.parse(token['expires_at']) + + if Time.now > expires_at + puts "\n⛔ ACCESO DENEGADO: El token de autorización ha EXPIRADO." + puts "💀 Expiró a las: #{expires_at.strftime('%H:%M:%S')}" + puts "👉 Ejecuta nuevamente: ruby scripts/candado.rb" + exit 1 + end + + puts "🔓 Autorización Verificada (Válida hasta #{expires_at.strftime('%H:%M:%S')})" + rescue => e + puts "\n❌ Error al verificar token: #{e.message}" + exit 1 + end +end + +verificar_autorizacion! +# ----------------------------------------------- + +# Variable para trackear si tenemos acceso sin password/interactivo +ssh_access_ok = false + +# Detectamos si necesitamos flags legacy +# Intentamos primero con flags modernos, si falla, probamos con legacy +ssh_flags = "-o ConnectTimeout=5 -o StrictHostKeyChecking=no" +ssh_legacy_flags = "#{ssh_flags} -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa" + +# --- 0. Detección de Hostname y Verificación de Acceso --- +puts "\n🔍 0. Detectando Hostname y Verificando Acceso..." +begin + # Probamos conexión con BatchMode para ver si ya entramos sin password + check_cmd = "ssh -o BatchMode=yes #{ssh_legacy_flags} #{user}@#{ip} hostname 2>/dev/null" + hostname = `#{check_cmd}`.strip + + if $?.success? && !hostname.empty? + puts "✅ Acceso SSH verificado (Sin contraseña)." + puts "✅ Hostname detectado: #{hostname}" + ssh_access_ok = true + else + puts "⚠️ Acceso directo no disponible. Intentando conexión interactiva (pedirá password)..." + # Usamos flags legacy aquí también por si acaso + hostname = `ssh #{ssh_legacy_flags} #{user}@#{ip} hostname`.strip + + if $?.success? && !hostname.empty? + puts "✅ Hostname detectado: #{hostname}" + else + puts "❌ No se pudo obtener el hostname. Verifica la IP y credenciales." + exit 1 + end + end +rescue => e + puts "❌ Error al intentar conectar: #{e.message}" + exit 1 +end + +# --- 1. Configuración SSH Local --- +puts "\n🔧 1. Configurando SSH Local..." + +if File.read(SSH_CONFIG_FILE).include?("Host #{hostname}") + puts "ℹ️ Host '#{hostname}' ya existe en #{SSH_CONFIG_FILE}." +else + puts "📝 Agregando '#{hostname}' a #{SSH_CONFIG_FILE}..." + File.open(SSH_CONFIG_FILE, 'a') do |f| + f.puts "\nHost #{hostname}" + f.puts " HostName #{ip}" + f.puts " User #{user}" + f.puts " StrictHostKeyChecking no" + f.puts " PubkeyAcceptedAlgorithms +ssh-rsa" + f.puts " HostKeyAlgorithms +ssh-rsa" + end + puts "✅ Configuración SSH actualizada (con soporte legacy)." +end + +# --- 2. Copia de Claves SSH --- +if ssh_access_ok + puts "\n🔑 2. Copia de Claves SSH: OMITIDO (Ya tienes acceso)." +else + puts "\n🔑 2. Instalando Clave SSH en #{user}@#{ip}..." + if File.exist?(SSH_PUB_KEY) + # ssh-copy-id pedirá password una única vez aquí + # Pasamos opciones legacy explícitamente + system("ssh-copy-id #{ssh_legacy_flags} -i #{SSH_PUB_KEY} #{user}@#{ip}") + + if $?.success? + puts "✅ Clave instalada exitosamente." + ssh_access_ok = true + else + puts "⚠️ Falló ssh-copy-id. Quizás debas hacerlo manualmente o verificar password." + end + else + puts "❌ No se encontró clave pública en #{SSH_PUB_KEY}." + end +end + +require 'json' + +# --- 3. Relevamiento de Hardware (Extracción Cruda & Procesamiento Local) --- +puts "\n🔍 3. Extrayendo Datos Crudos del Nodo (Procesamiento Local)..." + +# Script remoto minimalista: Solo "cat" de archivos clave y lsblk +# Usamos delimitadores únicos para separar bloques +# Agregamos recolección de evidencia para detección de roles +remote_script = <<~SHELL + echo "---START_HOSTNAME---"; cat /etc/hostname + echo "---START_CPUINFO---"; cat /proc/cpuinfo + echo "---START_MEMINFO---"; cat /proc/meminfo + echo "---START_OSRELEASE---"; cat /etc/os-release + echo "---START_LSBLK---"; lsblk -d -o NAME,SIZE,MODEL,TYPE -J 2>/dev/null || lsblk -d -o NAME,SIZE,MODEL,TYPE + echo "---START_PROCESSES---"; ps -ef --sort=-%mem | head -n 20 + echo "---START_PORTS---"; ss -tuln 2>/dev/null || netstat -tuln 2>/dev/null + echo "---START_MARKERS---" + [ -d "/etc/xen" ] && echo "xen_config_dir_exists" + [ -d "/etc/pve" ] && echo "pve_config_dir_exists" + [ -d "/var/lib/docker" ] && echo "docker_lib_dir_exists" + [ -f "/usr/bin/dockerd" ] && echo "dockerd_bin_exists" + [ -f "/opt/xensource/bin/xe" ] && echo "citrix_xe_bin_exists" +SHELL + +safe_script = remote_script.gsub('"', '\\"') +# Usamos flags legacy para la sonda también +raw_output = `ssh #{ssh_legacy_flags} #{hostname} "#{safe_script}"` + +# Parsers Locales (Host-Side Logic) +def parse_block(output, key) + return "" unless output.include?("---START_#{key}---") + output.split("---START_#{key}---")[1].split("---START_")[0].strip +rescue + "" +end + +hostname_raw = parse_block(raw_output, 'HOSTNAME') +cpuinfo_raw = parse_block(raw_output, 'CPUINFO') +meminfo_raw = parse_block(raw_output, 'MEMINFO') +osrelease_raw = parse_block(raw_output, 'OSRELEASE') +lsblk_raw = parse_block(raw_output, 'LSBLK') +processes_raw = parse_block(raw_output, 'PROCESSES') +ports_raw = parse_block(raw_output, 'PORTS') +markers_raw = parse_block(raw_output, 'MARKERS') + +# Analizador de CPU (Local) +cpu_model = cpuinfo_raw.scan(/model name\s+:\s+(.*)/).flatten.first || "Unknown" +cpu_cores = cpuinfo_raw.scan(/^processor\s+:/).count +cpu_arch = "x86_64" # Default (podría sacarse de flags en cpuinfo 'lm' = long mode) + +# Analizador de RAM (Local) +mem_total_kb = meminfo_raw.scan(/MemTotal:\s+(\d+)\s+kB/).flatten.first.to_i +mem_total_human = "#{(mem_total_kb / 1024.0 / 1024.0).round(2)} GiB" + +# Analizador de OS (Local) +os_distro = osrelease_raw.scan(/PRETTY_NAME="(.*)"/).flatten.first || "Unknown Linux" +os_kernel_ver = "Linux (Kernel ver. unknown)" # Kernel version requiring 'uname -r', keeping simple for now or parsing from /proc/version if needed later + +params = {} +params[:timestamp] = Time.now.to_s +params[:connector] = { ip: ip, user: user, hostname_detected: hostname_raw.empty? ? hostname : hostname_raw } + +params[:cpu] = { model: cpu_model, cores: cpu_cores, arch: cpu_arch } +params[:ram] = { total: mem_total_human, raw: mem_total_kb * 1024 } +params[:os] = { distro: os_distro, kernel: os_kernel_ver } # Simplified + +# Procesamiento de Discos (Local) +if lsblk_raw.strip.start_with?("{") + begin + params[:disks] = JSON.parse(lsblk_raw) + rescue + params[:disks] = { format: 'raw', content: lsblk_raw } + end +else + params[:disks] = { format: 'text', content: lsblk_raw } +end + +# Evidencia para Inteligencia de Agente +params[:evidence] = { + processes: processes_raw.lines.map(&:strip), + ports: ports_raw.lines.map(&:strip), + file_markers: markers_raw.lines.map(&:strip) +} + +puts " ✅ Datos crudos extraídos y procesados localmente." + +# --- 4. Salida para el Agente (STDOUT) --- +puts "\n💾 4. Telemetría Final (Para consumo del Agente)..." +puts "---START_PAYLOAD---" +puts JSON.pretty_generate(params) +puts "---END_PAYLOAD---" + +puts "\n✅ Proceso finalizado. Copia el bloque JSON anterior si necesitas procesarlo manualmente, o deja que el Agente lo lea." + diff --git a/scripts/candado.rb b/scripts/candado.rb new file mode 100755 index 00000000..3d2552e3 --- /dev/null +++ b/scripts/candado.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby +# script: candado.rb +# Uso: ruby scripts/candado.rb [minutos] +# Descripción: Genera un token de autorización temporal para que el Agente pueda ejecutar tareas críticas. +# Requiere que el usuario tenga claves SSH cargadas (prueba de identidad). + +require 'json' +require 'fileutils' + +TOKEN_FILE = File.expand_path('../.agent_token', __dir__) +DEFAULT_DURATION_MINS = 5 + +# 1. Validación de Identidad (Proof of User Presence) +puts "🔐 Requerimiento de Protocolo: Autorización FRESCA." +puts "🧹 Limpiando identidades previas en el agente SSH para exigir validación..." +system("ssh-add -D > /dev/null 2>&1") # Borra claves cacheadas para forzar el prompt + +puts "🔑 Por favor, ingresa tu passphrase para autorizar al Agente:" +# Intentamos cargar las claves interactivamente (ahora sí pedirá pass porque borramos las anteriores) +system("ssh-add") + +# Verificamos que la carga haya sido exitosa +system("ssh-add -l > /dev/null 2>&1") +unless $?.success? + puts "❌ ERROR: No se detectaron identidades SSH." + puts " Debes completar el desafío de identidad (passphrase) para continuar." + exit 1 +end + +# 2. Generación del Token +duration = (ARGV[0] || DEFAULT_DURATION_MINS).to_i +expires_at = Time.now + (duration * 60) + +token_data = { + created_at: Time.now.to_s, + expires_at: expires_at.to_s, + duration_mins: duration, + authorized_by: ENV['USER'] || 'unknown', + signature: rand(36**8).to_s(36) # Firma aleatoria simple +} + +File.write(TOKEN_FILE, JSON.pretty_generate(token_data)) +FileUtils.chmod(0600, TOKEN_FILE) # Solo lectura para el usuario + +puts "\n🔓 VACANCIA DE SEGURIDAD ACTIVADA" +puts "--------------------------------" +puts "✅ Agente AUTORIZADO para operaciones críticas." +puts "⏳ Ventana de tiempo: #{duration} minutos." +puts "💀 Expira: #{expires_at.strftime('%H:%M:%S')}" +puts "🔑 Token: #{TOKEN_FILE}" +puts "\n>> Ahora puedes pedirle al Agente que ejecute sus tareas."