diff --git a/tools/ns8-candados/README.md b/tools/ns8-candados/README.md index 970104f6..afdcca72 100644 --- a/tools/ns8-candados/README.md +++ b/tools/ns8-candados/README.md @@ -26,6 +26,15 @@ - `ruby ns8-candados.rb list`: Lista las claves guardadas (requiere autorización). - `ruby ns8-candados.rb get `: Recupera el valor descifrado (requiere autorización). +## 🛡️ Auditoría (logging) +La herramienta mantiene un archivo `access.log` en su directorio local que registra de forma inmutable: +- **Timestamp**: Fecha y hora exacta de la acción. +- **Usuario**: El usuario del sistema operativo que ejecutó el comando. +- **Acción**: `AUTHORIZE`, `GET`, `LOCK`, `DENIED` o `EXPIRED`. +- **Detalle**: Nombre de la clave solicitada (solo en comandos `get`). + +El archivo tiene permisos `600` para asegurar que solo el propietario pueda auditar los accesos. + ## 🧠 Contexto para IA - **Flujo Obligatorio**: Los agentes de IA DEBEN ejecutar `authorize` antes de intentar recuperar cualquier clave. - **Captura Segura**: Capturar siempre la salida en variables silenciadas: diff --git a/tools/ns8-candados/ns8-candados.rb b/tools/ns8-candados/ns8-candados.rb index 04ea46e4..d2954f69 100755 --- a/tools/ns8-candados/ns8-candados.rb +++ b/tools/ns8-candados/ns8-candados.rb @@ -35,6 +35,7 @@ MASTER_KEY = File.join(TOOL_DIR, '.master.key') OLD_KEY = File.expand_path('../../.master.key', TOOL_DIR) BOVEDA = File.join(TOOL_DIR, '.boveda.json') SESION = File.join(TOOL_DIR, '.session') +ACCESS_LOG = File.join(TOOL_DIR, 'access.log') # ─── Transición de Seguridad ───────────────────────────────────────── if File.exist?(OLD_KEY) && !File.exist?(MASTER_KEY) @@ -122,26 +123,44 @@ def cargar_master_key File.read(MASTER_KEY) end +# ─── Auditoría ─────────────────────────────────────────────────────── +def registrar_acceso(accion, detalle = '') + usuario = ENV['USER'] || ENV['USERNAME'] || 'unknown' + timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S') + log_entry = "[#{timestamp}] - user:#{usuario} - action:#{accion} - #{detalle}\n" + + File.open(ACCESS_LOG, 'a') do |f| + f.write(log_entry) + end + FileUtils.chmod(0600, ACCESS_LOG) if File.exist?(ACCESS_LOG) +end + # ─── Gestión de Sesión (MFA) ───────────────────────────────────────── def autorizar! File.write(SESION, Time.now.to_i.to_s) FileUtils.chmod(0600, SESION) + registrar_acceso('AUTHORIZE', 'Candado abierto (Sesión iniciada)') $stderr.puts "#{C::GREEN}✓ Autorización concedida (Válida por 5 minutos).#{C::RESET}" end def cerrar_sesion! - FileUtils.rm_f(SESION) - $stderr.puts "#{C::YELLOW}🔒 Sesión cerrada. Candado puesto.#{C::RESET}" + if File.exist?(SESION) + FileUtils.rm_f(SESION) + registrar_acceso('LOCK', 'Candado cerrado (Sesión finalizada)') + $stderr.puts "#{C::YELLOW}🔒 Sesión cerrada. Candado puesto.#{C::RESET}" + end end def verificar_sesion! unless File.exist?(SESION) + registrar_acceso('DENIED', 'Intento de acceso sin autorización') abort "#{C::RED}✗ Error: El candado está puesto. Ejecutá 'ruby #{File.basename($0)} authorize' para abrirlo.#{C::RESET}" end inicio = File.read(SESION).to_i if Time.now.to_i - inicio > 300 # 5 minutos cerrar_sesion! + registrar_acceso('EXPIRED', 'Sesión expirada automáticamente') abort "#{C::RED}✗ Sesión expirada. Por seguridad, volvé a autorizar.#{C::RESET}" end end @@ -179,6 +198,8 @@ def cmd_get(cripto, boveda, clave) verificar_sesion! token = boveda.get(clave) abort "#{C::RED}✗ Clave '#{clave}' no encontrada.#{C::RESET}" if token.nil? + + registrar_acceso('GET', "Clave: #{clave}") puts cripto.decrypt(token) end