UNPKG

n8n-nodes-pytenable

Version:

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

188 lines (169 loc) 5.77 kB
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, NodeApiError, NodeConnectionType, } from 'n8n-workflow'; import Docker from 'dockerode'; export class Pytenable implements INodeType { description: INodeTypeDescription = { 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' as unknown as NodeConnectionType], outputs: ['main' as unknown as NodeConnectionType], 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(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const node = this.getNode(); const docker = new Docker({ socketPath: '/var/run/docker.sock' }); const imageName = 'pytenable-runner:latest'; const userCode = this.getNodeParameter('pythonCode', 0, '') as string; const credentials = await this.getCredentials('tenableApi'); const runInContainer = async (inputData: object): Promise<INodeExecutionData[]> => { 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', '-c', pythonWrapper], 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<INodeExecutionData[]>((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: Buffer) => { 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 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 as Error).message}`; return reject(new NodeApiError(node, { message: parseErrorMsg })); } }); stream.on('error', (err: Error) => { return reject(new NodeApiError(node, { message: `Error de stream con Docker: ${err.message}` })); }); container.start((err: Error) => { if (err) return reject(new 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: INodeExecutionData[] = []; 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]; } } }