wulf-pulse/lib/services/analyzer/asset-audit/runner.ts
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

136 lines
4.1 KiB
TypeScript

/**
* Runs one audit against an IT Glue record.
*
* Phase 4 supported only Application (flexible_asset) audits, all-time
* scoped. Phase 4.1 generalizes:
* - assetType: 'flexible_asset' | 'configuration'
* - ticketScopeAnalysisId: when set, ticket evidence is the single source
* analysis only ("ticket-first" mode).
*
* Flow:
* 1. buildAssetAuditContext() — pulls everything; redacted.
* 2. callLLMStage() — single shot using the deep-analysis tier model for
* the requested provider (Sonnet for Anthropic, V4 Pro for OpenRouter).
* 3. insertAssetAudit() — persists the row, including ticket linkage.
*/
import { callLLMStage } from '@/lib/services/llm/call';
import {
type Provider,
stageModelsFor,
} from '@/lib/services/llm/models';
import { AssetAuditResponse } from '@/lib/types/analyzer';
import {
buildAssetAuditContext,
type AuditAssetType,
} from './data-builder';
import { getSystemPrompt, buildAssetAuditUserPayload } from './prompt';
import {
insertAssetAudit,
insertFailedAssetAudit,
} from './persistence';
const STAGE_MAX_TOKENS = 8_000;
export interface RunAssetAuditInput {
assetType: AuditAssetType;
assetId: number | string;
generatedByUserId: string | null;
provider?: Provider;
/** Phase 4.1: when set, ticket evidence narrows to just this analysis. */
ticketScopeAnalysisId?: string;
}
export interface RunAssetAuditResult {
auditId: string;
status: 'complete' | 'failed';
ticketCount: number;
errorMessage?: string;
}
export async function runAssetAudit(
input: RunAssetAuditInput
): Promise<RunAssetAuditResult> {
const provider: Provider = input.provider ?? 'anthropic';
const model = stageModelsFor(provider).deep_analysis;
const ctx = await buildAssetAuditContext({
assetType: input.assetType,
assetId: input.assetId,
ticketScopeAnalysisId: input.ticketScopeAnalysisId,
});
const ticketCount = ctx.ticket_evidence.length;
const assetSnapshot = {
id: ctx.asset.id,
name: ctx.asset.name,
organization_id: ctx.asset.organization_id,
organization_name: ctx.asset.organization_name,
asset_type: ctx.asset_type,
type_id: ctx.type_id,
type_name: ctx.type_name,
fields: ctx.asset.traits,
};
const triggeredByTicketNumber = ctx.ticket_scope?.ticket_number ?? null;
const triggeredByAnalysisId = ctx.ticket_scope?.analysis_id ?? null;
const { payload } = buildAssetAuditUserPayload(ctx);
const systemPrompt = getSystemPrompt(ctx);
try {
const result = await callLLMStage({
model,
system: systemPrompt,
user: payload,
schema: AssetAuditResponse,
maxTokens: STAGE_MAX_TOKENS,
});
const inserted = await insertAssetAudit({
asset_type: ctx.asset_type,
asset_id: ctx.asset.id,
asset_type_id: ctx.type_id,
organization_id: ctx.asset.organization_id,
generated_by_user_id: input.generatedByUserId,
provider,
model_used: model,
asset_snapshot: assetSnapshot,
ticket_count: ticketCount,
response: result.data,
estimated_cost_usd: result.estimated_cost_usd,
total_input_tokens: result.usage.input_tokens,
total_output_tokens: result.usage.output_tokens,
triggered_by_ticket_number: triggeredByTicketNumber,
triggered_by_analysis_id: triggeredByAnalysisId,
});
return {
auditId: inserted.id,
status: 'complete',
ticketCount,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const failed = await insertFailedAssetAudit({
asset_type: ctx.asset_type,
asset_id: ctx.asset.id,
asset_type_id: ctx.type_id,
organization_id: ctx.asset.organization_id,
generated_by_user_id: input.generatedByUserId,
provider,
model_used: model,
asset_snapshot: assetSnapshot,
ticket_count: ticketCount,
error_message: message,
triggered_by_ticket_number: triggeredByTicketNumber,
triggered_by_analysis_id: triggeredByAnalysisId,
});
return {
auditId: failed.id,
status: 'failed',
ticketCount,
errorMessage: message,
};
}
}