wulf-pulse/docs/LogLift Review.json
lorentz 1112a06afe feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
  resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
  receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
  xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
  link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
  panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
  admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00

362 lines
No EOL
18 KiB
JSON

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