{ "name": "LogLift Review", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "dd9e1afc-a494-4688-9542-010d388c7bd9", "responseMode": "lastNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ 0, 0 ], "id": "2c05a136-8106-4f38-82c5-6bc4e0574618", "name": "Webhook", "webhookId": "dd9e1afc-a494-4688-9542-010d388c7bd9" }, { "parameters": { "jsCode": "const crypto = require('crypto');\n\nconst bucket = \"wulf-audits\";\nconst region = \"us-west-002\"; \nconst endpoint = \"s3.us-west-002.backblazeb2.com\";\n\nconst keyId = $env.B2_KEY_ID;\nconst secret = $env.B2_APP_KEY;\n\n// Get objectKey from webhook payload\nconst body = $json.body || $json;\nconst objectKey = body.objectKey;\n\nif (!objectKey) {\n throw new Error('objectKey not provided in webhook payload');\n}\n\nconst expiresIn = 600;\n\nconst method = \"GET\";\nconst host = endpoint;\nconst canonicalUri = \"/\" + bucket + \"/\" + objectKey;\nconst algorithm = \"AWS4-HMAC-SHA256\";\n\nconst now = new Date();\nconst amzDate = now.toISOString().replace(/[-:]/g, \"\").split('.')[0] + 'Z';\nconst dateStamp = amzDate.slice(0, 8);\n\nconst credentialScope = `${dateStamp}/${region}/s3/aws4_request`;\nconst canonicalHeaders = `host:${host}\\n`;\nconst signedHeaders = \"host\";\n\nconst qs = {\n \"X-Amz-Algorithm\": algorithm,\n \"X-Amz-Credential\": encodeURIComponent(`${keyId}/${credentialScope}`),\n \"X-Amz-Date\": amzDate,\n \"X-Amz-Expires\": expiresIn.toString(),\n \"X-Amz-SignedHeaders\": signedHeaders,\n};\n\nconst canonicalQueryString = Object.keys(qs)\n .sort()\n .map((k) => `${k}=${qs[k]}`)\n .join(\"&\");\n\nconst payloadHash = \"UNSIGNED-PAYLOAD\";\n\nconst canonicalRequest = [\n method,\n canonicalUri,\n canonicalQueryString,\n canonicalHeaders,\n signedHeaders,\n payloadHash,\n].join(\"\\n\");\n\nconst stringToSign = [\n algorithm,\n amzDate,\n credentialScope,\n crypto.createHash(\"sha256\").update(canonicalRequest).digest(\"hex\"),\n].join(\"\\n\");\n\n// Derive signing key\nfunction sign(key, msg) {\n return crypto.createHmac(\"sha256\", key).update(msg).digest();\n}\n\nconst kDate = sign(\"AWS4\" + secret, dateStamp);\nconst kRegion = sign(kDate, region);\nconst kService = sign(kRegion, \"s3\");\nconst kSigning = sign(kService, \"aws4_request\");\n\nconst signature = crypto\n .createHmac(\"sha256\", kSigning)\n .update(stringToSign)\n .digest(\"hex\");\n\nconst downloadUrl =\n `https://${host}${canonicalUri}?${canonicalQueryString}` +\n `&X-Amz-Signature=${signature}`;\n\n// Pass through original payload data\nreturn [\n {\n json: {\n downloadUrl,\n objectKey,\n clientId: body.clientId,\n deviceUid: body.deviceUid,\n computerName: body.computerName,\n runId: body.runId,\n collectedAt: body.collectedAt,\n summary: body.summary,\n rmmContext: body.rmmContext,\n issueDescription: body.issueDescription,\n ticketNumber: body.ticketNumber\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 304, 0 ], "id": "473abf9f-d19f-4e16-868b-fe8bc4b0245a", "name": "Generate Download URL" }, { "parameters": { "url": "={{ $json.downloadUrl }}", "options": { "response": { "response": { "responseFormat": "file" } } } }, "id": "a8bef6ea-7273-4643-a2b3-595b8bcaab41", "name": "Download from B2", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 560, 0 ] }, { "parameters": { "jsCode": "const zlib = require('zlib');\n\n// Get the binary data from the previous node\nconst binaryData = await this.helpers.getBinaryDataBuffer(0, 'data');\n\n// Decompress gzip\nconst decompressed = zlib.gunzipSync(binaryData);\n\n// Parse JSON\nconst payload = JSON.parse(decompressed.toString('utf8'));\n\n// Get context from previous nodes\nconst context = $('Generate Download URL').first().json;\n\nreturn [\n {\n json: {\n payload,\n context: {\n clientId: context.clientId,\n deviceUid: context.deviceUid,\n computerName: context.computerName,\n runId: context.runId,\n collectedAt: context.collectedAt,\n summary: context.summary,\n rmmContext: context.rmmContext,\n issueDescription: context.issueDescription,\n ticketNumber: context.ticketNumber\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 848, 0 ], "id": "5f037bde-fb17-46e2-b627-f0e4b4b65395", "name": "Decompress & Parse" }, { "parameters": { "jsCode": "const payload = $json.payload;\nconst context = $json.context;\n\n// Build a focused prompt for the LLM\nconst systemPrompt = `You are an expert Windows systems administrator and IT security analyst. You are analyzing Windows Event Logs collected from a managed device to identify issues, anomalies, and potential problems.\n\nProvide a structured analysis with:\n1. **Executive Summary** - 2-3 sentence overview of system health\n2. **Critical Issues** - Any immediate problems requiring attention\n3. **Warnings & Concerns** - Issues that should be monitored or addressed soon\n4. **Notable Patterns** - Any interesting patterns or correlations in the events\n5. **Recommendations** - Specific actionable steps to resolve or investigate issues\n\nBe concise but thorough. Focus on actionable insights. If there are no significant issues, say so clearly.`;\n\n// Build context about the device\nlet deviceContext = `## Device Information\n- **Computer Name:** ${payload.metadata?.computerName || context.computerName}\n- **Device UID:** ${payload.metadata?.deviceUid || context.deviceUid}\n- **OS:** ${payload.systemContext?.OS?.Caption || 'Unknown'} (Build ${payload.systemContext?.OS?.Build || 'Unknown'})\n- **Manufacturer:** ${payload.systemContext?.Hardware?.Manufacturer || 'Unknown'} ${payload.systemContext?.Hardware?.Model || ''}\n- **Uptime:** ${payload.systemContext?.Uptime?.Total || 'Unknown'}\n- **Last Boot:** ${payload.systemContext?.LastBoot || 'Unknown'}\n- **Pending Reboot:** ${payload.systemContext?.PendingReboot ? 'Yes - ' + (payload.systemContext?.RebootReasons || []).join(', ') : 'No'}\n`;\n\n// Add memory/disk info\nif (payload.systemContext?.Memory) {\n deviceContext += `- **Memory:** ${payload.systemContext.Memory.UsedPercent}% used (${payload.systemContext.Memory.AvailableGB} GB free of ${payload.systemContext.Memory.TotalGB} GB)\\n`;\n}\n\nif (payload.systemContext?.Disks && Array.isArray(payload.systemContext.Disks)) {\n payload.systemContext.Disks.forEach(disk => {\n deviceContext += `- **Disk ${disk.Drive}:** ${disk.PercentFree}% free (${disk.FreeGB} GB of ${disk.SizeGB} GB)\\\\n`;\n });\n}\n\n// Add issue context if provided\nif (context.issueDescription) {\n deviceContext += `\\n## Reported Issue\\n${context.issueDescription}\\n`;\n}\nif (context.ticketNumber) {\n deviceContext += `**Ticket:** ${context.ticketNumber}\\n`;\n}\n\n// Add RMM context\nif (context.rmmContext?.siteName) {\n deviceContext += `\\n## Site Information\\n- **Site:** ${context.rmmContext.siteName}\\n`;\n}\n\n// Build event summary\nlet eventSummary = `\\n## Event Summary\\n`;\neventSummary += `- **Total Events:** ${payload.summary?.TotalEvents || 0}\\n`;\neventSummary += `- **Critical Events:** ${payload.summary?.CriticalEvents || 0}\\n`;\n\nif (payload.summary?.ByLevel) {\n eventSummary += `- **By Level:** `;\n const levels = [];\n for (const [level, count] of Object.entries(payload.summary.ByLevel)) {\n if (count > 0) levels.push(`${level}: ${count}`);\n }\n eventSummary += levels.join(', ') + '\\n';\n}\n\nif (payload.summary?.TimeRange) {\n eventSummary += `- **Time Range:** ${payload.summary.TimeRange.Earliest} to ${payload.summary.TimeRange.Latest}\\n`;\n}\n\n// Add top event types\nif (payload.summary?.TopEventIds && payload.summary.TopEventIds.length > 0) {\n eventSummary += `\\n### Top Event Types\\n`;\n payload.summary.TopEventIds.slice(0, 10).forEach(evt => {\n eventSummary += `- **[${evt.LogName}] Event ID ${evt.EventId}** (${evt.Level}): ${evt.Count} occurrences - ${evt.Source}\\n`;\n if (evt.Sample) {\n eventSummary += ` Sample: ${evt.Sample.substring(0, 200)}...\\n`;\n }\n });\n}\n\n// Add recent Windows updates if present\nif (payload.systemContext?.RecentUpdates && payload.systemContext.RecentUpdates.length > 0) {\n eventSummary += `\\n### Recent Windows Updates (Last 7 Days)\\n`;\n payload.systemContext.RecentUpdates.forEach(update => {\n eventSummary += `- ${update.Date}: ${update.Title} (${update.Result})\\n`;\n });\n}\n\n// Build the events section - include all events but truncate if too many\nlet eventsSection = `\\n## Event Log Entries\\n`;\nconst events = payload.events || [];\nconst maxEvents = 100; // Limit to avoid token overflow\n\nif (events.length > maxEvents) {\n eventsSection += `*Showing ${maxEvents} of ${events.length} events (prioritizing errors and critical)*\\n\\n`;\n}\n\n// Sort by severity and take top events\nconst severityOrder = { 'Critical': 0, 'Error': 1, 'Warning': 2, 'Information': 3 };\nconst sortedEvents = [...events].sort((a, b) => {\n const aOrder = severityOrder[a.Level] ?? 4;\n const bOrder = severityOrder[b.Level] ?? 4;\n if (aOrder !== bOrder) return aOrder - bOrder;\n return new Date(b.TimeCreated) - new Date(a.TimeCreated);\n});\n\nsortedEvents.slice(0, maxEvents).forEach(evt => {\n const marker = evt.IsCritical ? '🔴' : (evt.Level === 'Error' ? '🔴' : (evt.Level === 'Warning' ? '🟡' : '⚪'));\n eventsSection += `${marker} **${evt.TimeCreated}** [${evt.LogName}] Event ${evt.EventId} (${evt.Level})\\n`;\n eventsSection += ` Source: ${evt.Source}\\n`;\n if (evt.Message) {\n const msg = evt.Message.length > 500 ? evt.Message.substring(0, 500) + '...' : evt.Message;\n eventsSection += ` ${msg}\\n`;\n }\n eventsSection += `\\n`;\n});\n\n// Combine into user prompt\nconst userPrompt = deviceContext + eventSummary + eventsSection;\n\nreturn [\n {\n json: {\n systemPrompt,\n userPrompt,\n context\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1104, 0 ], "id": "f58ebb0f-e1b8-4622-ba72-e8508c8cca38", "name": "Build LLM Prompt" }, { "parameters": { "jsCode": "const analysis = $json.output?.[0]?.content?.[0]?.text || $json.message?.content || $json.choices?.[0]?.message?.content || 'No analysis generated';\nconst context = $('Build LLM Prompt').first().json.context;\n\nconst result = {\n status: 'success',\n computerName: context.computerName,\n deviceUid: context.deviceUid,\n clientId: context.clientId,\n siteName: context.rmmContext?.siteName || 'Unknown',\n analyzedAt: new Date().toISOString(),\n analysis\n};\n\n// Log the full analysis\nconsole.log('\\n' + '='.repeat(60));\nconsole.log('LOGLIFT ANALYSIS RESULTS');\nconsole.log('='.repeat(60));\nconsole.log(`Computer: ${result.computerName}`);\nconsole.log(`Device UID: ${result.deviceUid}`);\nconsole.log(`Site: ${result.siteName}`);\nconsole.log(`Analyzed: ${result.analyzedAt}`);\nconsole.log('-'.repeat(60));\nconsole.log(analysis);\nconsole.log('='.repeat(60) + '\\n');\n\nreturn [{ json: result }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1744, 0 ], "id": "761ac85f-ed62-4482-b463-5d906c9d4b72", "name": "Log Results" }, { "parameters": { "modelId": { "__rl": true, "value": "gpt-5.1", "mode": "list", "cachedResultName": "GPT-5.1" }, "responses": { "values": [ { "content": "={{ $json.userPrompt }}" }, { "role": "system", "content": "={{ $json.systemPrompt }}" } ] }, "builtInTools": {}, "options": {} }, "type": "@n8n/n8n-nodes-langchain.openAi", "typeVersion": 2.1, "position": [ 1328, 0 ], "id": "4bc852d9-52fb-4e59-ba56-ebc25953a3d9", "name": "Message a model", "credentials": { "openAiApi": { "id": "38HvRPpoNqRs3WND", "name": "OpenAi Dan" } } }, { "parameters": { "chatId": "7870607184", "text": "={{ $('Log Results').item.json.siteName }}\n{{ $('Log Results').item.json.deviceUid }}\n{{ $json.output[0].content[0].text }}", "additionalFields": {} }, "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ 2320, 0 ], "id": "1a872d78-73bc-44d9-9570-9362c945edb4", "name": "Send a text message", "webhookId": "c1eb4c83-c91c-4056-9442-404b6d70b736", "credentials": { "telegramApi": { "id": "csNHSd4rHXYD87nR", "name": "Telegram account" } } }, { "parameters": { "modelId": { "__rl": true, "value": "gpt-5.1", "mode": "list", "cachedResultName": "GPT-5.1" }, "responses": { "values": [ { "content": "={{ $json.analysis }}" }, { "role": "system", "content": "You are a senior support person helping diagnose a problematic workstation and prepareing a quick summary of the analysis for a telegram message to a superior. Make it pretty as well as informative. Less than 600 characters. for a non-technical person" } ] }, "builtInTools": {}, "options": {} }, "type": "@n8n/n8n-nodes-langchain.openAi", "typeVersion": 2.1, "position": [ 1952, 0 ], "id": "75a46aa3-2f12-48d1-90cd-11ce9f2315b0", "name": "Message a model1", "credentials": { "openAiApi": { "id": "38HvRPpoNqRs3WND", "name": "OpenAi Dan" } } } ], "pinData": { "Webhook": [ { "json": { "headers": { "connection": "keep-alive", "host": "n8n.wulfnetwork.com", "x-forwarded-scheme": "https", "x-forwarded-proto": "https", "x-forwarded-for": "24.49.85.211, 10.215.0.29", "x-real-ip": "10.215.0.29", "content-length": "1031", "user-agent": "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.6456", "accept-encoding": "gzip, br", "cdn-loop": "cloudflare; loops=1", "cf-connecting-ip": "24.49.85.211", "cf-ipcountry": "US", "cf-ray": "9a7e45ed085eee15-ATL", "cf-visitor": "{\"scheme\":\"https\"}", "cf-warp-tag-id": "72c7cfe4-accc-4cdf-85f1-87c142c80262", "content-type": "application/json" }, "params": {}, "query": {}, "body": { "runId": "eventlogs_20251202_173301", "clientId": "ba03268b-5528-4dde-ad76-867523446ecd", "computerName": "DT031", "summary": { "warningCount": null, "totalEvents": 74, "criticalEvents": 32, "timeRange": { "Latest": "2025-12-02 13:41:29", "Earliest": "2025-12-02 07:43:06" }, "errorCount": null }, "deviceUid": "DT031", "ticketNumber": "", "rmmContext": { "siteName": "Blake Dentistry", "siteUid": "ba03268b-5528-4dde-ad76-867523446ecd", "accountUid": "1bdb0002" }, "issueDescription": "", "objectKey": "ba03268b-5528-4dde-ad76-867523446ecd/unknown-server/eventlogs_20251202_173301.json.gz", "collectedAt": "2025-12-02T22:33:12Z" }, "webhookUrl": "https://n8n.wulfnetwork.com/webhook/dd9e1afc-a494-4688-9542-010d388c7bd9", "executionMode": "production" } } ] }, "connections": { "Generate Download URL": { "main": [ [ { "node": "Download from B2", "type": "main", "index": 0 } ] ] }, "Download from B2": { "main": [ [ { "node": "Decompress & Parse", "type": "main", "index": 0 } ] ] }, "Decompress & Parse": { "main": [ [ { "node": "Build LLM Prompt", "type": "main", "index": 0 } ] ] }, "Build LLM Prompt": { "main": [ [ { "node": "Message a model", "type": "main", "index": 0 } ] ] }, "Log Results": { "main": [ [ { "node": "Message a model1", "type": "main", "index": 0 } ] ] }, "Webhook": { "main": [ [ { "node": "Generate Download URL", "type": "main", "index": 0 } ] ] }, "Message a model": { "main": [ [ { "node": "Log Results", "type": "main", "index": 0 } ] ] }, "Message a model1": { "main": [ [ { "node": "Send a text message", "type": "main", "index": 0 } ] ] } }, "active": true, "settings": { "executionOrder": "v1", "binaryMode": "separate", "callerPolicy": "workflowsFromSameOwner", "availableInMCP": true }, "versionId": "1ba2cafe-4157-4899-8e01-72eb07dc3e99", "meta": { "templateCredsSetupCompleted": true, "instanceId": "ad3e8921b8f3ec5eb0f5de9993c5671acfe1653760ce28a0ae49df7ec32eb653" }, "id": "Kj3mC2c1nsDvDh3j", "tags": [ { "updatedAt": "2025-12-02T22:16:11.334Z", "createdAt": "2025-12-02T22:16:11.334Z", "id": "9rR7wgyIMan3vx0W", "name": "LogLift" }, { "updatedAt": "2025-11-20T13:03:47.079Z", "createdAt": "2025-11-20T13:03:47.079Z", "id": "PiB6zoFNWIgRxg3a", "name": "Audit" } ] }