{
  "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"
    }
  ]
}