{ "name": "LogWhisperer_Ingest", "nodes": [ { "parameters": {}, "id": "trigger-node", "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [ 250, 300 ], "webhookId": "logwhisperer-ingest", "path": "logwhisperer/ingest", "responseMode": "responseNode", "options": {} }, { "parameters": { "jsCode": "// HMAC Validation Node\n// Verifica la firma HMAC-SHA256 secondo il Metodo Sacchi: Safety First\n\nconst crypto = require('crypto');\n\n// Recupera headers\nconst signatureHeader = $headers['x-logwhisperer-signature'];\nconst timestampHeader = $headers['x-logwhisperer-timestamp'];\n\n// Recupera secret da variabile ambiente\nconst secret = process.env.LOGWHISPERER_SECRET;\n\nif (!secret) {\n throw new Error('LOGWHISPERER_SECRET not configured');\n}\n\nif (!signatureHeader || !timestampHeader) {\n return [{\n json: {\n valid: false,\n error: 'Missing authentication headers',\n statusCode: 401\n }\n }];\n}\n\n// Estrai timestamp e signature dal formato: timestamp:signature\nconst parts = signatureHeader.split(':');\nif (parts.length !== 2) {\n return [{\n json: {\n valid: false,\n error: 'Invalid signature format',\n statusCode: 401\n }\n }];\n}\n\nconst receivedTimestamp = parts[0];\nconst receivedSignature = parts[1];\n\n// Verifica timestamp (anti-replay: max 5 minuti di differenza)\nconst now = Math.floor(Date.now() / 1000);\nconst requestTime = parseInt(timestampHeader, 10);\nconst timeDiff = Math.abs(now - requestTime);\n\nif (timeDiff > 300) {\n return [{\n json: {\n valid: false,\n error: 'Request timestamp too old',\n statusCode: 401\n }\n }];\n}\n\n// Ottieni il payload raw\nconst payload = JSON.stringify($input.first().json);\n\n// Calcola HMAC atteso: HMAC-SHA256(timestamp:payload)\nconst expectedSignature = crypto\n .createHmac('sha256', secret)\n .update(`${timestampHeader}:${payload}`)\n .digest('hex');\n\n// Comparazione timing-safe\nconst isValid = crypto.timingSafeEqual(\n Buffer.from(receivedSignature, 'hex'),\n Buffer.from(expectedSignature, 'hex')\n);\n\nif (!isValid) {\n return [{\n json: {\n valid: false,\n error: 'Invalid signature',\n statusCode: 401\n }\n }];\n}\n\n// Validazione HMAC passata - restituisci payload per il prossimo nodo\nreturn [{\n json: {\n valid: true,\n data: $input.first().json\n }\n}];" }, "id": "hmac-validation-node", "name": "HMAC Validation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 450, 300 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "condition-1", "leftValue": "={{ $json.valid }}", "rightValue": "true", "operator": { "type": "boolean", "operation": "equals" } } ], "combinator": "and" } }, "id": "hmac-check-node", "name": "HMAC Valid?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 650, 300 ] }, { "parameters": { "jsCode": "// Data Validation Node\n// Validazione campi obbligatori secondo il Metodo Sacchi: Double Check\n\nconst data = $input.first().json.data;\nconst errors = [];\n\n// Validazione UUID per client_id\nfunction isValidUUID(uuid) {\n const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n return uuidRegex.test(uuid);\n}\n\n// Validazione campi obbligatori\nif (!data.client_id) {\n errors.push('Missing client_id');\n} else if (!isValidUUID(data.client_id)) {\n errors.push('Invalid client_id format (must be UUID)');\n}\n\nif (!data.raw_log || data.raw_log.trim() === '') {\n errors.push('Missing or empty raw_log');\n}\n\nconst validSeverities = ['low', 'medium', 'critical'];\nif (!data.severity) {\n errors.push('Missing severity');\n} else if (!validSeverities.includes(data.severity.toLowerCase())) {\n errors.push(`Invalid severity: ${data.severity} (must be one of: ${validSeverities.join(', ')})`);\n}\n\nif (errors.length > 0) {\n return [{\n json: {\n valid: false,\n errors: errors,\n received: data\n }\n }];\n}\n\n// Normalizza severity a lowercase\nconst normalizedData = {\n ...data,\n severity: data.severity.toLowerCase()\n};\n\nreturn [{\n json: {\n valid: true,\n data: normalizedData\n }\n}];" }, "id": "data-validation-node", "name": "Data Validation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 850, 200 ] }, { "parameters": { "operation": "executeQuery", "query": "INSERT INTO logs (client_id, hostname, source, severity, timestamp, raw_log, matched_pattern)\nVALUES ($1, $2, $3, $4, $5, $6, $7)\nRETURNING id;", "options": { "queryParams": "={{ JSON.stringify([$json.data.client_id, $json.data.hostname, $json.data.source, $json.data.severity, $json.data.timestamp, $json.data.raw_log, $json.data.matched_pattern]) }}" } }, "id": "postgres-insert-node", "name": "Store Log", "type": "n8n-nodes-base.postgres", "typeVersion": 2.2, "position": [ 1050, 200 ], "credentials": { "postgres": { "id": "postgres-credentials", "name": "PostgreSQL LogWhisperer" } } }, { "parameters": { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "condition-1", "leftValue": "={{ $json.data.severity }}", "rightValue": "critical", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "or" } }, "id": "severity-check-node", "name": "Critical Severity?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1250, 100 ] }, { "parameters": { "jsCode": "// AI Processing Placeholder\n// Per Sprint 2 Feature 2: prepara i dati per l'elaborazione AI\n// Il nodo vero e proprio sarà implementato nello Sprint 3\n\nconst logData = $input.first().json;\n\n// Log di sicurezza: non esporre raw_log completo nei log\nconsole.log('AI Processing requested for log ID:', logData.id);\nconsole.log('Client:', logData.data.client_id);\nconsole.log('Severity:', logData.data.severity);\nconsole.log('Pattern:', logData.data.matched_pattern);\n\nreturn [{\n json: {\n status: 'queued_for_ai_processing',\n log_id: logData.id,\n client_id: logData.data.client_id,\n severity: logData.data.severity,\n pattern: logData.data.matched_pattern,\n // raw_log escluso per sicurezza\n timestamp: new Date().toISOString()\n }\n}];" }, "id": "ai-processing-node", "name": "AI Processing (Placeholder)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1450, 50 ] }, { "parameters": { "respondWith": "json", "options": {} }, "id": "success-response-node", "name": "Success Response", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 1450, 250 ] }, { "parameters": { "respondWith": "json", "responseBody": "={\"error\": \"Unauthorized\", \"message\": \"Invalid HMAC signature\"}", "options": { "responseCode": 401 } }, "id": "unauthorized-response-node", "name": "401 Unauthorized", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 850, 400 ] }, { "parameters": { "respondWith": "json", "responseBody": "={\"error\": \"Bad Request\", \"message\": $json.errors.join(\", \")}", "options": { "responseCode": 400 } }, "id": "validation-error-node", "name": "400 Validation Error", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 1050, 400 ] }, { "parameters": { "jsCode": "// Ensure Table Exists\n// Crea la tabella logs se non esiste già\n\nreturn [{\n json: {\n sql: `\n CREATE TABLE IF NOT EXISTS logs (\n id SERIAL PRIMARY KEY,\n client_id VARCHAR(36) NOT NULL,\n hostname VARCHAR(255),\n source VARCHAR(500),\n severity VARCHAR(20) CHECK (severity IN ('low', 'medium', 'critical')),\n timestamp TIMESTAMP WITH TIME ZONE,\n raw_log TEXT,\n matched_pattern VARCHAR(100),\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n );\n \n CREATE INDEX IF NOT EXISTS idx_logs_client_id ON logs(client_id);\n CREATE INDEX IF NOT EXISTS idx_logs_severity ON logs(severity);\n CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);\n `\n }\n}];" }, "id": "ensure-table-node", "name": "Ensure Table SQL", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 650, 100 ] }, { "parameters": { "operation": "executeQuery", "query": "={{ $json.sql }}", "options": {} }, "id": "create-table-node", "name": "Create Table", "type": "n8n-nodes-base.postgres", "typeVersion": 2.2, "position": [ 850, 100 ], "credentials": { "postgres": { "id": "postgres-credentials", "name": "PostgreSQL LogWhisperer" } } } ], "connections": { "Webhook Trigger": { "main": [ [ { "node": "HMAC Validation", "type": "main", "index": 0 }, { "node": "Ensure Table SQL", "type": "main", "index": 0 } ] ] }, "HMAC Validation": { "main": [ [ { "node": "HMAC Valid?", "type": "main", "index": 0 } ] ] }, "HMAC Valid?": { "main": [ [ { "node": "Data Validation", "type": "main", "index": 0 } ], [ { "node": "401 Unauthorized", "type": "main", "index": 0 } ] ] }, "Data Validation": { "main": [ [ { "node": "Store Log", "type": "main", "index": 0 } ], [ { "node": "400 Validation Error", "type": "main", "index": 0 } ] ] }, "Store Log": { "main": [ [ { "node": "Critical Severity?", "type": "main", "index": 0 }, { "node": "Success Response", "type": "main", "index": 0 } ] ] }, "Critical Severity?": { "main": [ [ { "node": "AI Processing (Placeholder)", "type": "main", "index": 0 } ], [ { "node": "Success Response", "type": "main", "index": 0 } ] ] }, "Ensure Table SQL": { "main": [ [ { "node": "Create Table", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" }, "staticData": null, "tags": [ { "name": "logwhisperer", "id": "tag-logwhisperer", "createdAt": "2026-04-02T00:00:00.000Z", "updatedAt": "2026-04-02T00:00:00.000Z" }, { "name": "security", "id": "tag-security", "createdAt": "2026-04-02T00:00:00.000Z", "updatedAt": "2026-04-02T00:00:00.000Z" }, "ingestion" ] }