- LocalAI inference takes 30-60s on ARM64 hardware - Changed RPCD chat handler to async pattern: - Returns poll_id immediately - Background process runs AI query (120s timeout) - Saves result to /var/lib/threat-analyst/chat_*.json - Client polls with poll_id to get result - Updated api.js with chatAsync() that polls automatically - Changed default LocalAI port from 8081 to 8091 - Frontend shows "Thinking..." message with spinner during inference - Uses curl instead of wget (BusyBox wget doesn't support --post-data=-) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
171 lines
3.8 KiB
JavaScript
171 lines
3.8 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require rpc';
|
|
|
|
/**
|
|
* Threat Analyst API
|
|
* Package: luci-app-threat-analyst
|
|
* RPCD object: luci.threat-analyst
|
|
* Version: 0.1.0
|
|
*
|
|
* Generative AI-powered threat filtering for:
|
|
* - CrowdSec autoban scenarios
|
|
* - mitmproxy filter rules
|
|
* - WAF rules
|
|
*/
|
|
|
|
var callStatus = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'status',
|
|
expect: { }
|
|
});
|
|
|
|
var callGetThreats = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'get_threats',
|
|
params: ['limit'],
|
|
expect: { }
|
|
});
|
|
|
|
var callGetPending = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'get_pending',
|
|
expect: { }
|
|
});
|
|
|
|
var callChat = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'chat',
|
|
params: ['message', 'poll_id'],
|
|
expect: { }
|
|
});
|
|
|
|
// Async chat with polling
|
|
function chatAsync(message, maxPolls, pollInterval) {
|
|
maxPolls = maxPolls || 20;
|
|
pollInterval = pollInterval || 3000;
|
|
|
|
return callChat(message, null).then(function(result) {
|
|
if (!result.pending) {
|
|
return result;
|
|
}
|
|
|
|
var pollId = result.poll_id;
|
|
var polls = 0;
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
function poll() {
|
|
polls++;
|
|
callChat('poll', pollId).then(function(res) {
|
|
if (res.response || res.error) {
|
|
resolve(res);
|
|
} else if (polls < maxPolls) {
|
|
setTimeout(poll, pollInterval);
|
|
} else {
|
|
resolve({ error: 'AI response timeout (max polls reached)' });
|
|
}
|
|
}).catch(reject);
|
|
}
|
|
setTimeout(poll, pollInterval);
|
|
});
|
|
});
|
|
}
|
|
|
|
var callGenerateRules = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'generate_rules',
|
|
params: ['target'],
|
|
expect: { }
|
|
});
|
|
|
|
var callApproveRule = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'approve_rule',
|
|
params: ['id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callRejectRule = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'reject_rule',
|
|
params: ['id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callRunCycle = rpc.declare({
|
|
object: 'luci.threat-analyst',
|
|
method: 'run_cycle',
|
|
expect: { }
|
|
});
|
|
|
|
function formatRelativeTime(dateStr) {
|
|
if (!dateStr) return 'N/A';
|
|
try {
|
|
var date = new Date(dateStr);
|
|
var now = new Date();
|
|
var seconds = Math.floor((now - date) / 1000);
|
|
if (seconds < 60) return seconds + 's ago';
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
|
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
|
return Math.floor(seconds / 86400) + 'd ago';
|
|
} catch(e) {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
function parseScenario(scenario) {
|
|
if (!scenario) return 'Unknown';
|
|
var parts = scenario.split('/');
|
|
var name = parts[parts.length - 1];
|
|
return name.split('-').map(function(word) {
|
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
}).join(' ');
|
|
}
|
|
|
|
function getSeverityClass(scenario) {
|
|
if (!scenario) return 'medium';
|
|
var s = scenario.toLowerCase();
|
|
if (s.includes('malware') || s.includes('exploit') || s.includes('cve')) return 'critical';
|
|
if (s.includes('bruteforce') || s.includes('scan')) return 'high';
|
|
if (s.includes('crawl') || s.includes('http')) return 'low';
|
|
return 'medium';
|
|
}
|
|
|
|
function extractCVE(scenario) {
|
|
if (!scenario) return null;
|
|
// Match CVE patterns: CVE-YYYY-NNNNN
|
|
var match = scenario.match(/CVE-\d{4}-\d{4,}/i);
|
|
return match ? match[0].toUpperCase() : null;
|
|
}
|
|
|
|
return baseclass.extend({
|
|
getStatus: callStatus,
|
|
getThreats: callGetThreats,
|
|
getPending: callGetPending,
|
|
chat: chatAsync,
|
|
chatSync: callChat,
|
|
generateRules: callGenerateRules,
|
|
approveRule: callApproveRule,
|
|
rejectRule: callRejectRule,
|
|
runCycle: callRunCycle,
|
|
|
|
formatRelativeTime: formatRelativeTime,
|
|
parseScenario: parseScenario,
|
|
getSeverityClass: getSeverityClass,
|
|
extractCVE: extractCVE,
|
|
|
|
getOverview: function() {
|
|
return Promise.all([
|
|
callStatus(),
|
|
callGetThreats(20),
|
|
callGetPending()
|
|
]).then(function(results) {
|
|
return {
|
|
status: results[0] || {},
|
|
threats: (results[1] || {}).threats || [],
|
|
pending: (results[2] || {}).pending || []
|
|
};
|
|
});
|
|
}
|
|
});
|