UNPKG

n8n-nodes-pytenable

Version:

Un nodo de n8n para interactuar con la API de Tenable usando Pytenable en un sandbox de Docker.

180 lines (172 loc) 7.46 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Pytenable = void 0; const n8n_workflow_1 = require("n8n-workflow"); const dockerode_1 = __importDefault(require("dockerode")); class Pytenable { constructor() { this.description = { displayName: 'Pytenable', name: 'pytenable', icon: 'file:pytenable.svg', group: ['transform'], version: 1, description: 'Ejecuta código Python con Pytenable en un entorno Docker seguro', defaults: { name: 'Pytenable', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [ { name: 'tenableApi', required: true, }, ], properties: [ { displayName: 'Modo de Operación', name: 'mode', type: 'options', noDataExpression: true, options: [ { name: 'Ejecutar una vez para todos los items', value: 'runOnceForAllItems', description: 'Ejecuta este código una sola vez para todos los datos de entrada.', }, { name: 'Ejecutar una vez por cada item', value: 'runOnceForEachItem', description: 'Ejecuta este código una vez por cada item de entrada.', }, ], default: 'runOnceForAllItems', }, { displayName: 'Código Python', name: 'pythonCode', type: 'string', typeOptions: { editor: 'codeNodeEditor', editorLanguage: 'python', }, default: '# El resultado debe ser una lista de diccionarios\n# return [{"resultado": "ok"}]', description: 'El código Python a ejecutar. Usa las variables _items, _item y _credentials.', noDataExpression: true, }, ], }; } async execute() { const node = this.getNode(); const docker = new dockerode_1.default({ socketPath: '/var/run/docker.sock' }); const imageName = 'pytenable-runner:latest'; const userCode = this.getNodeParameter('pythonCode', 0, ''); const credentials = await this.getCredentials('tenableApi'); const runInContainer = async (inputData) => { const pythonWrapper = ` import sys import json import traceback import asyncio async def main(): try: input_data_str = sys.stdin.read() input_data = json.loads(input_data_str) global _items, _item, _credentials _item = input_data.get('item', {}) _items = input_data.get('items', []) _credentials = input_data.get('credentials', {}) user_code = """ ${userCode} """ exec_scope = {} exec(f"async def user_main():\\n" + "\\n".join(f" {line}" for line in user_code.split("\\n")), globals(), exec_scope) result = await exec_scope['user_main']() print(json.dumps(result)) except Exception as e: print(f"Error en el script de Python: {e}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) if __name__ == "__main__": asyncio.run(main()) `; let outputData = ''; let errorData = ''; const container = await docker.createContainer({ Image: imageName, Cmd: ['python', '-u'], Tty: false, AttachStdin: true, AttachStdout: true, AttachStderr: true, OpenStdin: true, StdinOnce: true, HostConfig: { AutoRemove: true, }, }); const stream = await container.attach({ stream: true, stdin: true, stdout: true, stderr: true }); return new Promise((resolve, reject) => { // Docker multiplexa stdout y stderr en un solo stream. // El primer byte del chunk indica el canal (1 = stdout, 2 = stderr). // Los siguientes 3 bytes son padding. // Los 4 bytes finales son el tamaño del payload. // Nos saltamos los 8 bytes de la cabecera para leer el mensaje. stream.on('data', (chunk) => { if (chunk[0] === 1) { outputData += chunk.slice(8).toString('utf-8'); } else if (chunk[0] === 2) { errorData += chunk.slice(8).toString('utf-8'); } }); stream.on('end', () => { if (errorData) { return reject(new n8n_workflow_1.NodeApiError(node, { message: `Error en el contenedor de Python: ${errorData}` })); } try { const result = JSON.parse(outputData); const returnItems = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(result), { itemData: [] }); return resolve(returnItems); } catch (error) { const parseErrorMsg = `Error al parsear la salida de Python. Salida recibida: "${outputData}". Error: ${error.message}`; return reject(new n8n_workflow_1.NodeApiError(node, { message: parseErrorMsg })); } }); stream.on('error', (err) => { return reject(new n8n_workflow_1.NodeApiError(node, { message: `Error de stream con Docker: ${err.message}` })); }); container.start((err) => { if (err) return reject(new n8n_workflow_1.NodeApiError(node, { message: `Error al iniciar el contenedor: ${err.message}` })); stream.write(JSON.stringify(inputData)); stream.end(); }); }); }; const mode = this.getNodeParameter('mode', 0, 'runOnceForAllItems'); const inputItems = this.getInputData(); if (mode === 'runOnceForAllItems') { const result = await runInContainer({ items: inputItems.map(item => item.json), credentials }); return [result]; } else { // runOnceForEachItem const returnData = []; for (let i = 0; i < inputItems.length; i++) { const resultItems = await runInContainer({ item: inputItems[i].json, credentials }); for (const item of resultItems) { item.pairedItem = { item: i }; returnData.push(item); } } return [returnData]; } } } exports.Pytenable = Pytenable;