431 lines
16 KiB
TypeScript
431 lines
16 KiB
TypeScript
// src/pages/api/ai/query.ts
|
|
import type { APIRoute } from 'astro';
|
|
import { withAPIAuth } from '../../../utils/auth.js';
|
|
import { getCompressedToolsDataForAI } from '../../../utils/dataService.js';
|
|
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js';
|
|
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
|
|
|
|
export const prerender = false;
|
|
|
|
function getEnv(key: string): string {
|
|
const value = process.env[key];
|
|
if (!value) {
|
|
throw new Error(`Missing environment variable: ${key}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const AI_MODEL = getEnv('AI_MODEL');
|
|
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
|
const RATE_LIMIT_WINDOW = 60 * 1000;
|
|
const RATE_LIMIT_MAX = 10;
|
|
|
|
function sanitizeInput(input: string): string {
|
|
let sanitized = input
|
|
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks
|
|
.replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags
|
|
.replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
|
|
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
|
|
.trim();
|
|
|
|
sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
function stripMarkdownJson(content: string): string {
|
|
return content
|
|
.replace(/^```json\s*/i, '')
|
|
.replace(/\s*```\s*$/, '')
|
|
.trim();
|
|
}
|
|
|
|
function checkRateLimit(userId: string): boolean {
|
|
const now = Date.now();
|
|
const userLimit = rateLimitStore.get(userId);
|
|
|
|
if (!userLimit || now > userLimit.resetTime) {
|
|
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
|
|
return true;
|
|
}
|
|
|
|
if (userLimit.count >= RATE_LIMIT_MAX) {
|
|
return false;
|
|
}
|
|
|
|
userLimit.count++;
|
|
return true;
|
|
}
|
|
|
|
function cleanupExpiredRateLimits() {
|
|
const now = Date.now();
|
|
for (const [userId, limit] of rateLimitStore.entries()) {
|
|
if (now > limit.resetTime) {
|
|
rateLimitStore.delete(userId);
|
|
}
|
|
}
|
|
}
|
|
|
|
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
|
|
|
|
async function loadToolsDatabase() {
|
|
try {
|
|
return await getCompressedToolsDataForAI();
|
|
} catch (error) {
|
|
console.error('Failed to load tools database:', error);
|
|
throw new Error('Database unavailable');
|
|
}
|
|
}
|
|
|
|
function createWorkflowSystemPrompt(toolsData: any): string {
|
|
const toolsList = toolsData.tools.map((tool: any) => ({
|
|
name: tool.name,
|
|
description: tool.description,
|
|
domains: tool.domains,
|
|
phases: tool.phases,
|
|
domainAgnostic: tool['domain-agnostic-software'],
|
|
platforms: tool.platforms,
|
|
skillLevel: tool.skillLevel,
|
|
license: tool.license,
|
|
tags: tool.tags,
|
|
related_concepts: tool.related_concepts || []
|
|
}));
|
|
|
|
const conceptsList = toolsData.concepts.map((concept: any) => ({
|
|
name: concept.name,
|
|
description: concept.description,
|
|
domains: concept.domains,
|
|
phases: concept.phases,
|
|
skillLevel: concept.skillLevel,
|
|
tags: concept.tags
|
|
}));
|
|
|
|
const regularPhases = toolsData.phases || [];
|
|
|
|
const domainAgnosticSoftware = toolsData['domain-agnostic-software'] || [];
|
|
|
|
const allPhaseItems = [
|
|
...regularPhases,
|
|
...domainAgnosticSoftware
|
|
];
|
|
|
|
const phasesDescription = allPhaseItems.map((phase: any) =>
|
|
`- ${phase.id}: ${phase.name}`
|
|
).join('\n');
|
|
|
|
const domainsDescription = toolsData.domains.map((domain: any) =>
|
|
`- ${domain.id}: ${domain.name}`
|
|
).join('\n');
|
|
|
|
const phaseDescriptions = regularPhases.map((phase: any) =>
|
|
`- ${phase.name}: ${phase.description || 'Tools/Methods for this phase'}`
|
|
).join('\n');
|
|
|
|
const domainAgnosticDescriptions = domainAgnosticSoftware.map((section: any) =>
|
|
`- ${section.name}: ${section.description || 'Cross-cutting software and platforms'}`
|
|
).join('\n');
|
|
|
|
const validPhases = [
|
|
...regularPhases.map((p: any) => p.id),
|
|
...domainAgnosticSoftware.map((s: any) => s.id)
|
|
].join('|');
|
|
|
|
return `Du bist ein DFIR (Digital Forensics and Incident Response) Experte, der Ermittlern bei der Auswahl von Software und Methoden hilft.
|
|
|
|
VERFÜGBARE TOOLS/METHODEN:
|
|
${JSON.stringify(toolsList, null, 2)}
|
|
|
|
VERFÜGBARE HINTERGRUNDWISSEN-KONZEPTE:
|
|
${JSON.stringify(conceptsList, null, 2)}
|
|
|
|
UNTERSUCHUNGSPHASEN (NIST Framework):
|
|
${phasesDescription}
|
|
|
|
FORENSISCHE DOMÄNEN:
|
|
${domainsDescription}
|
|
|
|
WICHTIGE REGELN:
|
|
1. Pro Phase 2-3 Tools/Methoden empfehlen (immer mindestens 2 wenn verfügbar)
|
|
2. Tools/Methoden können in MEHREREN Phasen empfohlen werden wenn sinnvoll - versuche ein Tool/Methode für jede Phase zu empfehlen, selbst wenn die Priorität "low" ist.
|
|
3. Für Reporting-Phase: Visualisierungs- und Dokumentationssoftware einschließen
|
|
4. Gib stets dem spezieller für den Fall geeigneten Werkzeug den Vorzug.
|
|
5. Deutsche Antworten für deutsche Anfragen, English for English queries
|
|
6. Methoden haben, sofern für das SZENARIO passend, IMMER Vorrang vor Software.
|
|
7. Bevorzuge alles, was nicht proprietär ist (license != "Proprietary"), aber erkenne an, wenn proprietäre Software besser geeignet ist.
|
|
8. WICHTIG: Erwähne relevante Hintergrundwissen-Konzepte wenn Tools verwendet werden, die related_concepts haben
|
|
9. Konzepte sind NICHT Tools - empfehle sie nicht als actionable Schritte, sondern als Wissensbasis
|
|
|
|
ENHANCED CONTEXTUAL ANALYSIS:
|
|
10. Analysiere das Szenario detailliert und identifiziere Schlüsselelemente, Bedrohungen und forensische Herausforderungen
|
|
11. Entwickle einen strategischen Untersuchungsansatz basierend auf dem spezifischen Szenario
|
|
12. Identifiziere zeitkritische oder besonders wichtige Faktoren für diesen Fall
|
|
|
|
SOFTWARE/METHODEN-AUSWAHL NACH PHASE:
|
|
${phaseDescriptions}
|
|
|
|
DOMÄNENAGNOSTISCHE SOFTWARE/METHODEN:
|
|
${domainAgnosticDescriptions}
|
|
|
|
ANTWORT-FORMAT (strict JSON):
|
|
{
|
|
"scenario_analysis": "Detaillierte Analyse des Szenarios: Erkannte Schlüsselelemente, Art des Vorfalls, betroffene Systeme, potentielle Bedrohungen und forensische Herausforderungen",
|
|
"investigation_approach": "Strategischer Untersuchungsansatz für dieses spezifische Szenario: Prioritäten, Reihenfolge der Phasen, besondere Überlegungen",
|
|
"critical_considerations": "Zeitkritische Faktoren, wichtige Sicherheitsaspekte oder besondere Vorsichtsmaßnahmen für diesen Fall",
|
|
"recommended_tools": [
|
|
{
|
|
"name": "EXAKTER Name aus der Tools-Database",
|
|
"priority": "high|medium|low",
|
|
"phase": "${validPhases}",
|
|
"justification": "Warum diese Methode für diese Phase und dieses spezifische Szenario geeignet ist - mit Bezug zu den erkannten Schlüsselelementen"
|
|
}
|
|
],
|
|
"workflow_suggestion": "Vorgeschlagener Untersuchungsablauf mit konkreten Schritten für dieses Szenario",
|
|
"background_knowledge": [
|
|
{
|
|
"concept_name": "EXAKTER Name aus der Konzepte-Database",
|
|
"relevance": "Warum dieses Konzept für das Szenario relevant ist, und bei welchen der empfohlenen Methoden/Tools."
|
|
}
|
|
],
|
|
"additional_notes": "Wichtige Überlegungen und Hinweise"
|
|
}
|
|
|
|
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
|
|
}
|
|
|
|
function createToolSystemPrompt(toolsData: any): string {
|
|
const toolsList = toolsData.tools.map((tool: any) => ({
|
|
name: tool.name,
|
|
description: tool.description,
|
|
domains: tool.domains,
|
|
phases: tool.phases,
|
|
platforms: tool.platforms,
|
|
skillLevel: tool.skillLevel,
|
|
license: tool.license,
|
|
tags: tool.tags,
|
|
url: tool.url,
|
|
projectUrl: tool.projectUrl,
|
|
related_concepts: tool.related_concepts || []
|
|
}));
|
|
|
|
const conceptsList = toolsData.concepts.map((concept: any) => ({
|
|
name: concept.name,
|
|
description: concept.description,
|
|
domains: concept.domains,
|
|
phases: concept.phases,
|
|
skillLevel: concept.skillLevel,
|
|
tags: concept.tags
|
|
}));
|
|
|
|
return `Du bist ein DFIR (Digital Forensics and Incident Response) Experte, der bei der Auswahl spezifischer Software/Methoden für konkrete Probleme hilft.
|
|
|
|
VERFÜGBARE TOOLS/METHODEN:
|
|
${JSON.stringify(toolsList, null, 2)}
|
|
|
|
VERFÜGBARE HINTERGRUNDWISSEN-KONZEPTE:
|
|
${JSON.stringify(conceptsList, null, 2)}
|
|
|
|
WICHTIGE REGELN:
|
|
1. Analysiere das spezifische Problem/die Anforderung sorgfältig
|
|
2. Empfehle 1-3 Methoden/Tools, sortiert nach Eignung (beste Empfehlung zuerst)
|
|
3. Gib detaillierte Erklärungen, WARUM und WIE jede Methode/Tool das Problem löst
|
|
4. Berücksichtige praktische Aspekte: Skill Level, Plattformen, Verfügbarkeit
|
|
5. Deutsche Antworten für deutsche Anfragen, English for English queries
|
|
6. Gib konkrete Anwendungshinweise, nicht nur allgemeine Beschreibungen - Methoden haben, sofern für das SZENARIO passend, IMMER Vorrang vor Software.
|
|
7. Erwähne sowohl Stärken als auch Schwächen/Limitationen
|
|
8. Schlage alternative Ansätze vor, wenn sinnvoll
|
|
9. Gib grundsätzliche Hinweise, WIE die Methode/Tool konkret eingesetzt wird
|
|
10. WICHTIG: Erwähne relevante Hintergrundwissen-Konzepte wenn Tools verwendet werden, die related_concepts haben
|
|
11. Konzepte sind NICHT Tools - empfehle sie nicht als actionable Schritte, sondern als Wissensbasis
|
|
|
|
ENHANCED CONTEXTUAL ANALYSIS:
|
|
12. Analysiere das Problem detailliert und identifiziere technische Anforderungen, Herausforderungen und Erfolgsfaktoren
|
|
13. Entwickle einen strategischen Lösungsansatz basierend auf dem spezifischen Problem
|
|
14. Identifiziere wichtige Voraussetzungen oder Warnungen für die Anwendung
|
|
|
|
ANTWORT-FORMAT (strict JSON):
|
|
{
|
|
"problem_analysis": "Detaillierte Analyse des Problems: Erkannte technische Anforderungen, Herausforderungen, benötigte Fähigkeiten und Erfolgsfaktoren",
|
|
"investigation_approach": "Strategischer Lösungsansatz für dieses spezifische Problem: Herangehensweise, Prioritäten, optimale Anwendungsreihenfolge",
|
|
"critical_considerations": "Wichtige Voraussetzungen, potentielle Fallstricke oder Warnungen für die Anwendung der empfohlenen Lösungen",
|
|
"recommended_tools": [
|
|
{
|
|
"name": "EXAKTER Name aus der Tools-Database",
|
|
"rank": 1,
|
|
"suitability_score": "high|medium|low",
|
|
"detailed_explanation": "Detaillierte Erklärung, warum dieses Tool/diese Methode das spezifische Problem löst - mit Bezug zu den erkannten Anforderungen",
|
|
"implementation_approach": "Konkrete Schritte/Ansatz zur Anwendung für dieses spezifische Problem",
|
|
"pros": ["Spezifische Vorteile für diesen Anwendungsfall", "Weitere Vorteile"],
|
|
"cons": ["Potentielle Nachteile oder Limitationen", "Weitere Einschränkungen"],
|
|
"alternatives": "Alternative Ansätze oder ergänzende Tools/Methoden, falls relevant"
|
|
}
|
|
],
|
|
"background_knowledge": [
|
|
{
|
|
"concept_name": "EXAKTER Name aus der Konzepte-Database",
|
|
"relevance": "Warum dieses Konzept für die empfohlenen Tools/das Problem relevant ist, und für welche der empfohlenen Methoden/Tools."
|
|
}
|
|
],
|
|
"additional_considerations": "Wichtige Überlegungen, Voraussetzungen oder Warnungen"
|
|
}
|
|
|
|
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
|
|
}
|
|
|
|
export const POST: APIRoute = async ({ request }) => {
|
|
try {
|
|
const authResult = await withAPIAuth(request, 'ai');
|
|
if (!authResult.authenticated) {
|
|
return createAuthErrorResponse();
|
|
}
|
|
|
|
const userId = authResult.userId;
|
|
|
|
if (!checkRateLimit(userId)) {
|
|
return apiError.rateLimit('Rate limit exceeded');
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { query, mode = 'workflow', taskId: clientTaskId } = body;
|
|
|
|
if (!query || typeof query !== 'string') {
|
|
return apiError.badRequest('Query required');
|
|
}
|
|
|
|
if (!['workflow', 'tool'].includes(mode)) {
|
|
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
|
|
}
|
|
|
|
const sanitizedQuery = sanitizeInput(query);
|
|
if (sanitizedQuery.includes('[FILTERED]')) {
|
|
return apiError.badRequest('Invalid input detected');
|
|
}
|
|
|
|
const toolsData = await loadToolsDatabase();
|
|
|
|
const systemPrompt = mode === 'workflow'
|
|
? createWorkflowSystemPrompt(toolsData)
|
|
: createToolSystemPrompt(toolsData);
|
|
|
|
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
|
|
const aiResponse = await enqueueApiCall(() =>
|
|
fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${process.env.AI_API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
model: AI_MODEL,
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: systemPrompt
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: sanitizedQuery
|
|
}
|
|
],
|
|
max_tokens: 2000,
|
|
temperature: 0.3
|
|
})
|
|
})
|
|
, taskId);
|
|
|
|
if (!aiResponse.ok) {
|
|
console.error('AI API error:', await aiResponse.text());
|
|
return apiServerError.unavailable('AI service unavailable');
|
|
}
|
|
|
|
const aiData = await aiResponse.json();
|
|
const aiContent = aiData.choices?.[0]?.message?.content;
|
|
|
|
if (!aiContent) {
|
|
return apiServerError.unavailable('No response from AI');
|
|
}
|
|
|
|
let recommendation;
|
|
try {
|
|
const cleanedContent = stripMarkdownJson(aiContent);
|
|
recommendation = JSON.parse(cleanedContent);
|
|
} catch (error) {
|
|
console.error('Failed to parse AI response:', aiContent);
|
|
return apiServerError.unavailable('Invalid AI response format');
|
|
}
|
|
|
|
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
|
|
const validConceptNames = new Set(toolsData.concepts.map((c: any) => c.name));
|
|
|
|
let validatedRecommendation;
|
|
|
|
if (mode === 'workflow') {
|
|
validatedRecommendation = {
|
|
...recommendation,
|
|
// Ensure all new fields are included with fallbacks
|
|
scenario_analysis: recommendation.scenario_analysis || recommendation.problem_analysis || '',
|
|
investigation_approach: recommendation.investigation_approach || '',
|
|
critical_considerations: recommendation.critical_considerations || '',
|
|
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
|
|
if (!validToolNames.has(tool.name)) {
|
|
console.warn(`AI recommended unknown tool: ${tool.name}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}) || [],
|
|
background_knowledge: recommendation.background_knowledge?.filter((concept: any) => {
|
|
if (!validConceptNames.has(concept.concept_name)) {
|
|
console.warn(`AI referenced unknown concept: ${concept.concept_name}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}) || []
|
|
};
|
|
} else {
|
|
validatedRecommendation = {
|
|
...recommendation,
|
|
// Ensure all new fields are included with fallbacks
|
|
problem_analysis: recommendation.problem_analysis || recommendation.scenario_analysis || '',
|
|
investigation_approach: recommendation.investigation_approach || '',
|
|
critical_considerations: recommendation.critical_considerations || '',
|
|
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
|
|
if (!validToolNames.has(tool.name)) {
|
|
console.warn(`AI recommended unknown tool: ${tool.name}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}).map((tool: any, index: number) => ({
|
|
...tool,
|
|
rank: tool.rank || (index + 1),
|
|
suitability_score: tool.suitability_score || 'medium',
|
|
pros: Array.isArray(tool.pros) ? tool.pros : [],
|
|
cons: Array.isArray(tool.cons) ? tool.cons : []
|
|
})) || [],
|
|
background_knowledge: recommendation.background_knowledge?.filter((concept: any) => {
|
|
if (!validConceptNames.has(concept.concept_name)) {
|
|
console.warn(`AI referenced unknown concept: ${concept.concept_name}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}) || []
|
|
};
|
|
}
|
|
|
|
console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}, Concepts: ${validatedRecommendation.background_knowledge?.length || 0}`);
|
|
|
|
return new Response(JSON.stringify({
|
|
success: true,
|
|
mode,
|
|
taskId,
|
|
recommendation: validatedRecommendation,
|
|
query: sanitizedQuery
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('AI query error:', error);
|
|
return apiServerError.internal('Internal server error');
|
|
}
|
|
}; |