- 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>
99 lines
3 KiB
TypeScript
99 lines
3 KiB
TypeScript
/**
|
|
* LogLift evidence webhook.
|
|
*
|
|
* The Datto RMM collector component uploads gzipped event-log JSON to B2,
|
|
* then POSTs the metadata here. Pulse downloads the gzip, slims it, persists
|
|
* it as RMM evidence, and (when the hostname matches a single IT Glue
|
|
* Configuration) fires an asset-first audit on the side.
|
|
*
|
|
* Auth: `x-openclaw-key` header — same shared secret the collector already
|
|
* carries for OpenClaw API calls.
|
|
*
|
|
* Public route per `middleware.ts` (`/api/rmm/loglift` exclusion).
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { z } from 'zod';
|
|
|
|
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
|
import { OBJECT_KEY_REGEX } from '@/lib/services/b2/client';
|
|
import { processLogliftWebhook } from '@/lib/services/rmm/loglift-receiver';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
export const maxDuration = 60;
|
|
|
|
const WebhookSchema = z.object({
|
|
runId: z.string().min(1).max(200),
|
|
clientId: z.string().min(1).max(200),
|
|
computerName: z.string().min(1).max(200),
|
|
deviceUid: z.string().max(200).nullable().optional(),
|
|
summary: z.object({
|
|
totalEvents: z.number().int().nonnegative().nullable().optional(),
|
|
criticalEvents: z.number().int().nonnegative().nullable().optional(),
|
|
errorCount: z.number().int().nonnegative().nullable().optional(),
|
|
warningCount: z.number().int().nonnegative().nullable().optional(),
|
|
timeRange: z.string().max(200).nullable().optional(),
|
|
}),
|
|
objectKey: z
|
|
.string()
|
|
.min(1)
|
|
.max(500)
|
|
.refine((s) => OBJECT_KEY_REGEX.test(s), {
|
|
message: 'objectKey does not match the required eventlogs path shape',
|
|
}),
|
|
collectedAt: z.string().min(1).max(64),
|
|
rmmContext: z
|
|
.object({
|
|
siteName: z.string().max(200).nullable().optional(),
|
|
siteUid: z.string().max(200).nullable().optional(),
|
|
accountUid: z.string().max(200).nullable().optional(),
|
|
})
|
|
.partial()
|
|
.nullable()
|
|
.optional(),
|
|
issueDescription: z.string().max(4000).nullable().optional(),
|
|
ticketNumber: z.string().max(64).nullable().optional(),
|
|
});
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const auth = validateOpenClawKey(req);
|
|
if (auth) return auth;
|
|
|
|
let raw: unknown;
|
|
try {
|
|
raw = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
}
|
|
|
|
const parsed = WebhookSchema.safeParse(raw);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Invalid payload',
|
|
details: parsed.error.flatten(),
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const result = await processLogliftWebhook(parsed.data);
|
|
return NextResponse.json(
|
|
{
|
|
executionId: result.executionId,
|
|
matched: result.matched,
|
|
parsed: result.parsed,
|
|
auditId: result.audit_id,
|
|
},
|
|
{ status: 200 }
|
|
);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
console.error('[LOGLIFT-WEBHOOK]', message, err);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to process LogLift webhook', message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|