cleanup, prompt centralization
This commit is contained in:
parent
b17458d153
commit
dc9f52fb7c
@ -1130,16 +1130,12 @@ class AIQueryInterface {
|
|||||||
const lowConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) < 60).length;
|
const lowConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) < 60).length;
|
||||||
const mediumConfidenceSteps = auditTrail.length - highConfidenceSteps - lowConfidenceSteps;
|
const mediumConfidenceSteps = auditTrail.length - highConfidenceSteps - lowConfidenceSteps;
|
||||||
|
|
||||||
// FIX 1: Count actual AI decision actions only
|
|
||||||
const aiDecisionCount = auditTrail.filter(entry => entry.action === 'ai-decision').length;
|
const aiDecisionCount = auditTrail.filter(entry => entry.action === 'ai-decision').length;
|
||||||
|
|
||||||
// FIX 2: Count actual similarity search actions, not metadata flags
|
|
||||||
const embeddingsUsageCount = auditTrail.filter(entry => entry.action === 'similarity-search').length;
|
const embeddingsUsageCount = auditTrail.filter(entry => entry.action === 'similarity-search').length;
|
||||||
|
|
||||||
// FIX 3: Maintain tool selection count (this was correct)
|
|
||||||
const toolSelectionCount = auditTrail.filter(entry => entry.action === 'selection-decision').length;
|
const toolSelectionCount = auditTrail.filter(entry => entry.action === 'selection-decision').length;
|
||||||
|
|
||||||
// Additional diagnostic counts for debugging
|
|
||||||
const microTaskCount = auditTrail.filter(entry =>
|
const microTaskCount = auditTrail.filter(entry =>
|
||||||
entry.action === 'ai-decision' && entry.metadata?.microTaskType
|
entry.action === 'ai-decision' && entry.metadata?.microTaskType
|
||||||
).length;
|
).length;
|
||||||
@ -1152,7 +1148,6 @@ class AIQueryInterface {
|
|||||||
entry.action === 'phase-enhancement'
|
entry.action === 'phase-enhancement'
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Enhanced insights with diagnostic information
|
|
||||||
const keyInsights = [];
|
const keyInsights = [];
|
||||||
const potentialIssues = [];
|
const potentialIssues = [];
|
||||||
|
|
||||||
@ -1172,7 +1167,6 @@ class AIQueryInterface {
|
|||||||
keyInsights.push(`${microTaskCount} spezialisierte Micro-Task-Analysen durchgeführt`);
|
keyInsights.push(`${microTaskCount} spezialisierte Micro-Task-Analysen durchgeführt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect mode-specific patterns for validation
|
|
||||||
if (phaseToolSelectionCount > 0 || phaseEnhancementCount > 0) {
|
if (phaseToolSelectionCount > 0 || phaseEnhancementCount > 0) {
|
||||||
keyInsights.push('Workflow-Modus: Phasenspezifische Analyse durchgeführt');
|
keyInsights.push('Workflow-Modus: Phasenspezifische Analyse durchgeführt');
|
||||||
} else if (microTaskCount >= 3) {
|
} else if (microTaskCount >= 3) {
|
||||||
@ -1216,10 +1210,9 @@ class AIQueryInterface {
|
|||||||
keyInsights.push('Mehrheit der Analyseschritte mit hoher Sicherheit');
|
keyInsights.push('Mehrheit der Analyseschritte mit hoher Sicherheit');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate expected counts based on mode detection
|
|
||||||
const isWorkflowMode = phaseToolSelectionCount > 0 || phaseEnhancementCount > 0;
|
const isWorkflowMode = phaseToolSelectionCount > 0 || phaseEnhancementCount > 0;
|
||||||
const expectedMinAI = isWorkflowMode ? 11 : 8; // Workflow: 5 common + 6 phase selections, Tool: 5 common + 3 evaluations
|
const expectedMinAI = isWorkflowMode ? 11 : 8;
|
||||||
const expectedMinEmbeddings = 1; // Both modes should have initial search
|
const expectedMinEmbeddings = 1;
|
||||||
|
|
||||||
if (aiDecisionCount < expectedMinAI) {
|
if (aiDecisionCount < expectedMinAI) {
|
||||||
potentialIssues.push(`${expectedMinAI - aiDecisionCount} fehlende KI-Entscheidungen für ${isWorkflowMode ? 'Workflow' : 'Tool'}-Modus`);
|
potentialIssues.push(`${expectedMinAI - aiDecisionCount} fehlende KI-Entscheidungen für ${isWorkflowMode ? 'Workflow' : 'Tool'}-Modus`);
|
||||||
@ -1250,7 +1243,6 @@ class AIQueryInterface {
|
|||||||
analysisQuality,
|
analysisQuality,
|
||||||
keyInsights,
|
keyInsights,
|
||||||
potentialIssues,
|
potentialIssues,
|
||||||
// Debug information
|
|
||||||
debugCounts: {
|
debugCounts: {
|
||||||
microTaskCount,
|
microTaskCount,
|
||||||
phaseToolSelectionCount,
|
phaseToolSelectionCount,
|
||||||
|
@ -16,9 +16,41 @@ STRICTNESS:
|
|||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
export const AI_PROMPTS = {
|
export const AI_PROMPTS = {
|
||||||
// ---------------------------------------------------------------------------
|
enhancementQuestions: (input: string) => {
|
||||||
// Tool/Concept selection (AI pre-pick from embedding-curated set)
|
return `Sie sind DFIR-Experte. Ein Nutzer beschreibt unten ein Szenario/Problem.
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
ZIEL:
|
||||||
|
- Stellen Sie NUR dann 1–3 präzise Rückfragen, wenn entscheidende forensische Lücken die weitere Analyse/Toolauswahl PHASENREIHENFOLGE oder EVIDENCE-STRATEGIE wesentlich beeinflussen würden.
|
||||||
|
- Wenn ausreichend abgedeckt: Geben Sie eine leere Liste [] zurück.
|
||||||
|
|
||||||
|
PRIORITÄT DER THEMEN (in dieser Reihenfolge prüfen):
|
||||||
|
1) Available Evidence & Artefakte (z.B. RAM-Dump, Disk-Image, Logs, PCAP, Registry, Cloud/Audit-Logs)
|
||||||
|
2) Scope/Systems (konkrete Plattformen/Assets/Identitäten/Netzsegmente)
|
||||||
|
3) Investigation Objectives (Ziele: IOC-Extraktion, Timeline, Impact, Attribution)
|
||||||
|
4) Timeline/Timeframe (kritische Zeitfenster, Erhalt flüchtiger Daten)
|
||||||
|
5) Legal & Compliance (Chain of Custody, Aufbewahrung, DSGVO/Branchenvorgaben)
|
||||||
|
6) Technical Constraints (Ressourcen, Zugriffsrechte, Tooling/EDR)
|
||||||
|
|
||||||
|
FRAGEN-QUALITÄT:
|
||||||
|
- Forensisch spezifisch und entscheidungsrelevant (keine Allgemeinplätze).
|
||||||
|
- Eine Frage pro Thema, keine Dopplungen.
|
||||||
|
- Antwortbar vom Nutzer (keine Spekulation, keine “Beweise senden”-Aufforderungen).
|
||||||
|
- Maximal 18 Wörter, endet mit "?".
|
||||||
|
|
||||||
|
VALIDIERUNG:
|
||||||
|
- Stellen Sie NUR Fragen zu Themen, die im Nutzertext NICHT hinreichend konkret beantwortet sind (keine Wiederholung bereits gegebener Details).
|
||||||
|
- Wenn alle priorisierten Themen ausreichend sind → [].
|
||||||
|
|
||||||
|
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
|
||||||
|
[
|
||||||
|
"präzise Frage 1?",
|
||||||
|
"präzise Frage 2?",
|
||||||
|
"präzise Frage 3?"
|
||||||
|
]
|
||||||
|
|
||||||
|
NUTZER-EINGABE:
|
||||||
|
${input}`.trim();
|
||||||
|
},
|
||||||
toolSelection: (mode: string, userQuery: string, maxSelectedItems: number) => {
|
toolSelection: (mode: string, userQuery: string, maxSelectedItems: number) => {
|
||||||
const modeInstruction =
|
const modeInstruction =
|
||||||
mode === 'workflow'
|
mode === 'workflow'
|
||||||
@ -80,9 +112,6 @@ ${RELEVANCE_RUBRIC}
|
|||||||
${STRICTNESS}`;
|
${STRICTNESS}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Contextual analyses (keine JSON-Ausgabe nötig; kurzer Fließtext)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
|
scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
|
||||||
const analysisType = isWorkflow ? 'Szenario' : 'Problem';
|
const analysisType = isWorkflow ? 'Szenario' : 'Problem';
|
||||||
const focus = isWorkflow
|
const focus = isWorkflow
|
||||||
@ -124,9 +153,6 @@ Fokus: ${focus}
|
|||||||
Antwort: Fließtext, max 100 Wörter.`;
|
Antwort: Fließtext, max 100 Wörter.`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Phase-specific selection (workflow mode substep)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
|
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
|
||||||
const methods = phaseTools.filter(t => t.type === 'method');
|
const methods = phaseTools.filter(t => t.type === 'method');
|
||||||
const tools = phaseTools.filter(t => t.type === 'software');
|
const tools = phaseTools.filter(t => t.type === 'software');
|
||||||
@ -183,9 +209,6 @@ ANTWORT (NUR JSON):
|
|||||||
]`;
|
]`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Per-item evaluation (used in tool mode & elsewhere)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
toolEvaluation: (userQuery: string, tool: any, rank: number) => {
|
toolEvaluation: (userQuery: string, tool: any, rank: number) => {
|
||||||
const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
|
const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
|
||||||
|
|
||||||
@ -215,9 +238,6 @@ ANTWORT (NUR JSON):
|
|||||||
}`;
|
}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Background knowledge (concepts)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
|
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
|
||||||
return `Wähle 2–4 Konzepte, die das Verständnis/den Einsatz der ausgewählten Tools verbessern.
|
return `Wähle 2–4 Konzepte, die das Verständnis/den Einsatz der ausgewählten Tools verbessern.
|
||||||
|
|
||||||
@ -242,9 +262,6 @@ ANTWORT (NUR JSON):
|
|||||||
]`;
|
]`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Phase completion (underrepresented phase fix)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
phaseCompletionReasoning: (
|
phaseCompletionReasoning: (
|
||||||
originalQuery: string,
|
originalQuery: string,
|
||||||
phase: any,
|
phase: any,
|
||||||
@ -306,9 +323,6 @@ ANTWORT (NUR JSON):
|
|||||||
}`;
|
}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Final synthesis (short prose, no JSON needed)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
|
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
|
||||||
const focus = isWorkflow
|
const focus = isWorkflow
|
||||||
? 'Knappe Workflow-Schritte & Best Practices; neutral formulieren'
|
? 'Knappe Workflow-Schritte & Best Practices; neutral formulieren'
|
||||||
@ -324,7 +338,7 @@ Antwort: Fließtext, max ${isWorkflow ? '100' : '80'} Wörter. Keine Liste.`;
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Overloads
|
export function getPrompt(key: 'enhancementQuestions', input: string): string;
|
||||||
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, maxSelectedItems: number): string;
|
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, maxSelectedItems: number): string;
|
||||||
export function getPrompt(key: 'toolSelectionWithData', basePrompt: string, toolsToSend: any[], conceptsToSend: any[]): string;
|
export function getPrompt(key: 'toolSelectionWithData', basePrompt: string, toolsToSend: any[], conceptsToSend: any[]): string;
|
||||||
export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
|
export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
|
||||||
@ -337,7 +351,6 @@ export function getPrompt(key: 'phaseCompletionReasoning', originalQuery: string
|
|||||||
export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
|
export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
|
||||||
export function getPrompt(key: 'generatePhaseCompletionPrompt', originalQuery: string, phase: any, candidateTools: any[], candidateConcepts: any[]): string;
|
export function getPrompt(key: 'generatePhaseCompletionPrompt', originalQuery: string, phase: any, candidateTools: any[], candidateConcepts: any[]): string;
|
||||||
|
|
||||||
// Dispatcher
|
|
||||||
export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
|
export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
|
||||||
try {
|
try {
|
||||||
const f = AI_PROMPTS[promptKey];
|
const f = AI_PROMPTS[promptKey];
|
||||||
|
@ -273,15 +273,13 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Load simple-boost from local node_modules instead of CDN
|
// TODO: cleanup
|
||||||
import('simple-boost').then(() => {
|
import('simple-boost').then(() => {
|
||||||
console.log('Simple-boost loaded successfully from local dependencies');
|
console.log('Simple-boost loaded successfully from local dependencies');
|
||||||
|
|
||||||
// Setup dynamic amount updating
|
|
||||||
setupDynamicAmounts();
|
setupDynamicAmounts();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Failed to load simple-boost:', error);
|
console.error('Failed to load simple-boost:', error);
|
||||||
// Fallback: try to load from node_modules path
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = 'module';
|
script.type = 'module';
|
||||||
script.src = '/node_modules/simple-boost/dist/simple-boost.js';
|
script.src = '/node_modules/simple-boost/dist/simple-boost.js';
|
||||||
@ -294,7 +292,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setupDynamicAmounts() {
|
function setupDynamicAmounts() {
|
||||||
// EUR boost button
|
|
||||||
const eurBoost = document.getElementById('eur-boost');
|
const eurBoost = document.getElementById('eur-boost');
|
||||||
const eurInput = document.getElementById('eur-amount') as HTMLInputElement;
|
const eurInput = document.getElementById('eur-amount') as HTMLInputElement;
|
||||||
|
|
||||||
@ -305,7 +302,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
console.log('EUR amount set to:', amount);
|
console.log('EUR amount set to:', amount);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update on input change for better UX
|
|
||||||
eurInput.addEventListener('input', () => {
|
eurInput.addEventListener('input', () => {
|
||||||
const amount = parseFloat(eurInput.value) || 0.5;
|
const amount = parseFloat(eurInput.value) || 0.5;
|
||||||
eurBoost.setAttribute('amount', amount.toString());
|
eurBoost.setAttribute('amount', amount.toString());
|
||||||
@ -315,7 +311,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Custom styling for simple-boost buttons to match your theme */
|
|
||||||
simple-boost {
|
simple-boost {
|
||||||
--simple-boost-primary: var(--color-warning);
|
--simple-boost-primary: var(--color-warning);
|
||||||
--simple-boost-primary-hover: var(--color-accent);
|
--simple-boost-primary-hover: var(--color-accent);
|
||||||
|
@ -5,13 +5,53 @@ import { apiError, apiServerError, createAuthErrorResponse } from '../../../util
|
|||||||
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
|
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
|
||||||
import { aiService } from '../../../utils/aiService.js';
|
import { aiService } from '../../../utils/aiService.js';
|
||||||
import { JSONParser } from '../../../utils/jsonUtils.js';
|
import { JSONParser } from '../../../utils/jsonUtils.js';
|
||||||
|
import { getPrompt } from '../../../config/prompts.js';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
const RATE_LIMIT_WINDOW_MS =
|
||||||
const RATE_LIMIT_WINDOW = 60 * 1000;
|
Number.isFinite(parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '', 10))
|
||||||
const RATE_LIMIT_MAX = 5;
|
? parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10)
|
||||||
|
: 60_000;
|
||||||
|
|
||||||
|
const RATE_LIMIT_MAX =
|
||||||
|
Number.isFinite(parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS ?? '', 10))
|
||||||
|
? parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS!, 10)
|
||||||
|
: 5;
|
||||||
|
|
||||||
|
const INPUT_MIN_CHARS = 40;
|
||||||
|
const INPUT_MAX_CHARS = 1000;
|
||||||
|
const Q_MIN_LEN = 15;
|
||||||
|
const Q_MAX_LEN = 160;
|
||||||
|
const Q_MAX_COUNT = 3;
|
||||||
|
const AI_TEMPERATURE = 0.3;
|
||||||
|
const CLEANER_TEMPERATURE = 0.0;
|
||||||
|
|
||||||
|
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||||
|
|
||||||
|
function checkRateLimit(userId: string): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = rateLimitStore.get(userId);
|
||||||
|
if (!entry || now > entry.resetTime) {
|
||||||
|
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entry.count >= RATE_LIMIT_MAX) return false;
|
||||||
|
entry.count++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupExpiredRateLimits(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [userId, entry] of rateLimitStore.entries()) {
|
||||||
|
if (now > entry.resetTime) rateLimitStore.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers
|
||||||
|
*/
|
||||||
function sanitizeInput(input: string): string {
|
function sanitizeInput(input: string): string {
|
||||||
return input
|
return input
|
||||||
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
|
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
|
||||||
@ -19,79 +59,24 @@ function sanitizeInput(input: string): string {
|
|||||||
.replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
|
.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]')
|
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, 1000);
|
.slice(0, INPUT_MAX_CHARS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRateLimit(userId: string): boolean {
|
function stripJsonFences(s: string): string {
|
||||||
const now = Date.now();
|
return s.replace(/^```json\s*/i, '')
|
||||||
const userLimit = rateLimitStore.get(userId);
|
.replace(/^```\s*/i, '')
|
||||||
|
.replace(/\s*```\s*$/, '')
|
||||||
if (!userLimit || now > userLimit.resetTime) {
|
.trim();
|
||||||
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(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
for (const [userId, limit] of rateLimitStore.entries()) {
|
|
||||||
if (now > limit.resetTime) {
|
|
||||||
rateLimitStore.delete(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
|
|
||||||
|
|
||||||
function createEnhancementPrompt(input: string): string {
|
|
||||||
return `Sie sind ein DFIR-Experte mit Spezialisierung auf forensische Methodik. Ein Nutzer beschreibt ein Szenario oder Problem. Analysieren Sie die Eingabe auf Vollständigkeit für eine wissenschaftlich fundierte Untersuchung.
|
|
||||||
|
|
||||||
ANALYSIEREN SIE DIESE FORENSISCHEN KATEGORIEN:
|
|
||||||
1. **Incident Context**: Was ist passiert? Welche Angriffsvektoren oder technischen Probleme liegen vor?
|
|
||||||
2. **Affected Systems**: Welche spezifischen Technologien/Plattformen sind betroffen? (Windows/Linux/ICS/SCADA/Mobile/Cloud/Network Infrastructure)
|
|
||||||
3. **Available Evidence**: Welche forensischen Datenquellen stehen zur Verfügung? (RAM-Dumps, Disk-Images, Log-Files, Network-Captures, Registry-Hives)
|
|
||||||
4. **Investigation Objectives**: Was soll erreicht werden? (IOC-Extraktion, Timeline-Rekonstruktion, Attribution, Impact-Assessment)
|
|
||||||
5. **Timeline Constraints**: Wie zeitkritisch ist die Untersuchung?
|
|
||||||
6. **Legal & Compliance**: Rechtliche Anforderungen, Chain of Custody, Compliance-Rahmen (DSGVO, sector-specific regulations)
|
|
||||||
7. **Technical Constraints**: Verfügbare Ressourcen, Skills, Infrastrukturbeschränkungen
|
|
||||||
|
|
||||||
WENN die Beschreibung alle kritischen forensischen Aspekte abdeckt: Geben Sie eine leere Liste [] zurück.
|
|
||||||
|
|
||||||
WENN wichtige Details fehlen: Formulieren Sie 2-3 präzise Fragen, die die kritischsten Lücken für eine wissenschaftlich fundierte Analyse schließen.
|
|
||||||
|
|
||||||
QUALITÄTSKRITERIEN FÜR FRAGEN:
|
|
||||||
- Forensisch spezifisch, nicht allgemein (NICHT: "Mehr Details?")
|
|
||||||
- Methodisch relevant (NICHT: "Wann passierte das?")
|
|
||||||
- Priorisiert nach Auswirkung auf die Untersuchungsqualität
|
|
||||||
- Die Frage soll maximal 20 Wörter umfassen
|
|
||||||
|
|
||||||
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
|
|
||||||
[
|
|
||||||
"spezifische Frage 1?",
|
|
||||||
"spezifische Frage 2?",
|
|
||||||
"spezifische Frage 3?"
|
|
||||||
]
|
|
||||||
|
|
||||||
NUTZER-EINGABE:
|
|
||||||
${input}
|
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler
|
||||||
|
*/
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const authResult = await withAPIAuth(request, 'ai');
|
const auth = await withAPIAuth(request, 'ai');
|
||||||
if (!authResult.authenticated) {
|
if (!auth.authenticated) return createAuthErrorResponse();
|
||||||
return createAuthErrorResponse();
|
const userId = auth.userId;
|
||||||
}
|
|
||||||
|
|
||||||
const userId = authResult.userId;
|
|
||||||
|
|
||||||
if (!checkRateLimit(userId)) {
|
if (!checkRateLimit(userId)) {
|
||||||
return apiError.rateLimit('Enhancement rate limit exceeded');
|
return apiError.rateLimit('Enhancement rate limit exceeded');
|
||||||
@ -100,61 +85,38 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { input } = body;
|
const { input } = body;
|
||||||
|
|
||||||
if (!input || typeof input !== 'string' || input.length < 40) {
|
if (!input || typeof input !== 'string' || input.length < INPUT_MIN_CHARS) {
|
||||||
return apiError.badRequest('Input too short for enhancement (minimum 40 characters)');
|
return apiError.badRequest(`Input too short for enhancement (minimum ${INPUT_MIN_CHARS} characters)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedInput = sanitizeInput(input);
|
const sanitizedInput = sanitizeInput(input);
|
||||||
if (sanitizedInput.length < 40) {
|
if (sanitizedInput.length < INPUT_MIN_CHARS) {
|
||||||
return apiError.badRequest('Input too short after sanitization');
|
return apiError.badRequest('Input too short after sanitization');
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = createEnhancementPrompt(sanitizedInput);
|
const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
|
const questionsPrompt = getPrompt('enhancementQuestions', sanitizedInput);
|
||||||
|
|
||||||
console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`);
|
console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`);
|
||||||
|
|
||||||
const aiResponse = await enqueueApiCall(() =>
|
const aiResponse = await enqueueApiCall(
|
||||||
aiService.callAI(systemPrompt, {
|
() => aiService.callAI(questionsPrompt, { temperature: AI_TEMPERATURE }),
|
||||||
temperature: 0.7
|
taskId
|
||||||
}), taskId);
|
);
|
||||||
|
|
||||||
if (!aiResponse.content) {
|
if (!aiResponse?.content) {
|
||||||
return apiServerError.unavailable('No enhancement response');
|
return apiServerError.unavailable('No enhancement response');
|
||||||
}
|
}
|
||||||
|
|
||||||
let questions;
|
let parsed: unknown = JSONParser.safeParseJSON(stripJsonFences(aiResponse.content), null);
|
||||||
try {
|
|
||||||
const cleanedContent = aiResponse.content
|
|
||||||
.replace(/^```json\s*/i, '')
|
|
||||||
.replace(/\s*```\s*$/, '')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
questions = JSONParser.safeParseJSON(cleanedContent, []);
|
let questions: string[] = Array.isArray(parsed) ? parsed : [];
|
||||||
|
questions = questions
|
||||||
if (!Array.isArray(questions)) {
|
.filter(q => typeof q === 'string')
|
||||||
throw new Error('Response is not an array');
|
.map(q => q.trim())
|
||||||
}
|
.filter(q => q.endsWith('?'))
|
||||||
|
.filter(q => q.length >= Q_MIN_LEN && q.length <= Q_MAX_LEN)
|
||||||
questions = questions
|
.slice(0, Q_MAX_COUNT);
|
||||||
.filter(q => typeof q === 'string' && q.length > 20 && q.length < 200)
|
|
||||||
.filter(q => q.includes('?'))
|
|
||||||
.filter(q => {
|
|
||||||
const forensicsTerms = ['forensisch', 'log', 'dump', 'image', 'artefakt', 'evidence', 'incident', 'system', 'netzwerk', 'zeitraum', 'verfügbar'];
|
|
||||||
const lowerQ = q.toLowerCase();
|
|
||||||
return forensicsTerms.some(term => lowerQ.includes(term));
|
|
||||||
})
|
|
||||||
.map(q => q.trim())
|
|
||||||
.slice(0, 3);
|
|
||||||
|
|
||||||
if (questions.length === 0) {
|
|
||||||
questions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ENHANCE-API] Failed to parse enhancement response:', aiResponse.content);
|
|
||||||
questions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`);
|
console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`);
|
||||||
|
|
||||||
@ -168,8 +130,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('[ENHANCE-API] Enhancement error:', error);
|
console.error('[ENHANCE-API] Enhancement error:', err);
|
||||||
return apiServerError.internal('Enhancement processing failed');
|
return apiServerError.internal('Enhancement processing failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -470,13 +470,11 @@ class AIPipeline {
|
|||||||
pipelineStart: number,
|
pipelineStart: number,
|
||||||
toolsDataHash: string
|
toolsDataHash: string
|
||||||
): Promise<{ completed: number; failed: number }> {
|
): Promise<{ completed: number; failed: number }> {
|
||||||
// Evaluate ALL candidates handed over by the embeddings pre-filter.
|
|
||||||
const candidates = context.filteredData.tools || [];
|
const candidates = context.filteredData.tools || [];
|
||||||
if (!Array.isArray(candidates) || candidates.length === 0) {
|
if (!Array.isArray(candidates) || candidates.length === 0) {
|
||||||
return { completed: completedTasks, failed: failedTasks };
|
return { completed: completedTasks, failed: failedTasks };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate every candidate (no slicing here)
|
|
||||||
for (let i = 0; i < candidates.length; i++) {
|
for (let i = 0; i < candidates.length; i++) {
|
||||||
const evaluationResult = await this.evaluateSpecificTool(context, candidates[i], i + 1, pipelineStart, toolsDataHash);
|
const evaluationResult = await this.evaluateSpecificTool(context, candidates[i], i + 1, pipelineStart, toolsDataHash);
|
||||||
if (evaluationResult.success) completedTasks++; else failedTasks++;
|
if (evaluationResult.success) completedTasks++; else failedTasks++;
|
||||||
@ -484,15 +482,12 @@ class AIPipeline {
|
|||||||
await this.delay(this.config.microTaskDelay);
|
await this.delay(this.config.microTaskDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, context.selectedTools may contain 0..N evaluated items (added by evaluateSpecificTool).
|
|
||||||
// Now we sort them by AI-derived taskRelevance (after moderation) and keep ONLY the top 3 for UI.
|
|
||||||
if (Array.isArray(context.selectedTools) && context.selectedTools.length > 0) {
|
if (Array.isArray(context.selectedTools) && context.selectedTools.length > 0) {
|
||||||
context.selectedTools.sort((a: any, b: any) => {
|
context.selectedTools.sort((a: any, b: any) => {
|
||||||
const ar = typeof a.taskRelevance === 'number' ? a.taskRelevance : -1;
|
const ar = typeof a.taskRelevance === 'number' ? a.taskRelevance : -1;
|
||||||
const br = typeof b.taskRelevance === 'number' ? b.taskRelevance : -1;
|
const br = typeof b.taskRelevance === 'number' ? b.taskRelevance : -1;
|
||||||
if (br !== ar) return br - ar;
|
if (br !== ar) return br - ar;
|
||||||
|
|
||||||
// tie-breakers without domain heuristics:
|
|
||||||
const aLen = (a.justification || '').length;
|
const aLen = (a.justification || '').length;
|
||||||
const bLen = (b.justification || '').length;
|
const bLen = (b.justification || '').length;
|
||||||
if (bLen !== aLen) return bLen - aLen;
|
if (bLen !== aLen) return bLen - aLen;
|
||||||
@ -502,7 +497,6 @@ class AIPipeline {
|
|||||||
return aRank - bRank;
|
return aRank - bRank;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep top 3 only
|
|
||||||
context.selectedTools = context.selectedTools.slice(0, 3);
|
context.selectedTools = context.selectedTools.slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -877,7 +871,6 @@ class AIPipeline {
|
|||||||
): Promise<MicroTaskResult> {
|
): Promise<MicroTaskResult> {
|
||||||
const taskStart = Date.now();
|
const taskStart = Date.now();
|
||||||
|
|
||||||
// Build prompt WITHOUT any baseline score
|
|
||||||
const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank);
|
const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank);
|
||||||
const result = await this.callMicroTaskAI(prompt, context, 'tool-evaluation');
|
const result = await this.callMicroTaskAI(prompt, context, 'tool-evaluation');
|
||||||
|
|
||||||
@ -885,16 +878,13 @@ class AIPipeline {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse strictly; do NOT provide a default with a score.
|
|
||||||
const evaluation = JSONParser.safeParseJSON(result.content, null);
|
const evaluation = JSONParser.safeParseJSON(result.content, null);
|
||||||
|
|
||||||
// Require a numeric score produced by the model; otherwise, don't add this tool.
|
|
||||||
const aiProvided = evaluation && typeof evaluation.taskRelevance === 'number' && Number.isFinite(evaluation.taskRelevance)
|
const aiProvided = evaluation && typeof evaluation.taskRelevance === 'number' && Number.isFinite(evaluation.taskRelevance)
|
||||||
? Math.round(evaluation.taskRelevance)
|
? Math.round(evaluation.taskRelevance)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (aiProvided === null) {
|
if (aiProvided === null) {
|
||||||
// Log the malformed output but avoid injecting a synthetic score.
|
|
||||||
auditService.addAIDecision(
|
auditService.addAIDecision(
|
||||||
'tool-evaluation',
|
'tool-evaluation',
|
||||||
prompt,
|
prompt,
|
||||||
@ -920,7 +910,6 @@ class AIPipeline {
|
|||||||
const moderatedTaskRelevance = this.moderateTaskRelevance(aiProvided);
|
const moderatedTaskRelevance = this.moderateTaskRelevance(aiProvided);
|
||||||
const priority = this.derivePriorityFromScore(moderatedTaskRelevance);
|
const priority = this.derivePriorityFromScore(moderatedTaskRelevance);
|
||||||
|
|
||||||
// Keep original fields if present; coerce to strings/arrays safely.
|
|
||||||
const detailed_explanation = String(evaluation?.detailed_explanation || '').trim();
|
const detailed_explanation = String(evaluation?.detailed_explanation || '').trim();
|
||||||
const implementation_approach = String(evaluation?.implementation_approach || '').trim();
|
const implementation_approach = String(evaluation?.implementation_approach || '').trim();
|
||||||
const pros = Array.isArray(evaluation?.pros) ? evaluation.pros : [];
|
const pros = Array.isArray(evaluation?.pros) ? evaluation.pros : [];
|
||||||
|
@ -194,18 +194,15 @@ class ToolSelector {
|
|||||||
const softwareWithFullData = candidateSoftware.map(this.createToolData);
|
const softwareWithFullData = candidateSoftware.map(this.createToolData);
|
||||||
const conceptsWithFullData = candidateConcepts.map(this.createConceptData);
|
const conceptsWithFullData = candidateConcepts.map(this.createConceptData);
|
||||||
|
|
||||||
// Embeddings are always ON → only use embedding limits
|
|
||||||
const maxTools = Math.min(this.config.embeddingSelectionLimit, candidateTools.length);
|
const maxTools = Math.min(this.config.embeddingSelectionLimit, candidateTools.length);
|
||||||
const maxConcepts = Math.min(this.config.embeddingConceptsLimit, candidateConcepts.length);
|
const maxConcepts = Math.min(this.config.embeddingConceptsLimit, candidateConcepts.length);
|
||||||
|
|
||||||
// Respect ratios first, then fill the remaining capacity
|
|
||||||
const methodRatio = Math.max(0, Math.min(1, this.config.methodSelectionRatio));
|
const methodRatio = Math.max(0, Math.min(1, this.config.methodSelectionRatio));
|
||||||
const softwareRatio = Math.max(0, Math.min(1, this.config.softwareSelectionRatio));
|
const softwareRatio = Math.max(0, Math.min(1, this.config.softwareSelectionRatio));
|
||||||
|
|
||||||
let methodLimit = Math.round(maxTools * methodRatio);
|
let methodLimit = Math.round(maxTools * methodRatio);
|
||||||
let softwareLimit = Math.round(maxTools * softwareRatio);
|
let softwareLimit = Math.round(maxTools * softwareRatio);
|
||||||
|
|
||||||
// If rounded sum exceeds maxTools, scale down proportionally
|
|
||||||
if (methodLimit + softwareLimit > maxTools) {
|
if (methodLimit + softwareLimit > maxTools) {
|
||||||
const scale = maxTools / (methodLimit + softwareLimit);
|
const scale = maxTools / (methodLimit + softwareLimit);
|
||||||
methodLimit = Math.floor(methodLimit * scale);
|
methodLimit = Math.floor(methodLimit * scale);
|
||||||
@ -217,7 +214,6 @@ class ToolSelector {
|
|||||||
|
|
||||||
const toolsToSend: any[] = [...methodsPrimary, ...softwarePrimary];
|
const toolsToSend: any[] = [...methodsPrimary, ...softwarePrimary];
|
||||||
|
|
||||||
// Fill any remaining capacity from whichever pool still has candidates
|
|
||||||
let mIdx = methodsPrimary.length;
|
let mIdx = methodsPrimary.length;
|
||||||
let sIdx = softwarePrimary.length;
|
let sIdx = softwarePrimary.length;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user