438 lines
18 KiB
Plaintext
438 lines
18 KiB
Plaintext
---
|
|
import { getToolsData } from '../utils/dataService.js';
|
|
|
|
|
|
// Load tools data for validation
|
|
const data = await getToolsData();
|
|
|
|
const tools = data.tools;
|
|
const phases = data.phases;
|
|
const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add this line
|
|
---
|
|
|
|
<!-- AI Query Interface -->
|
|
<section id="ai-interface" class="ai-interface" style="display: none;">
|
|
<div class="ai-query-section">
|
|
<div style="text-align: center; margin-bottom: 2rem;">
|
|
<h2 style="margin-bottom: 1rem; color: var(--color-primary);">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
|
|
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
|
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
|
</svg>
|
|
KI-gestützte Tool-Empfehlungen
|
|
</h2>
|
|
<p class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
|
|
Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Tool-Empfehlungen
|
|
basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="ai-input-container" style="max-width: 800px; margin: 0 auto;">
|
|
<textarea
|
|
id="ai-query-input"
|
|
placeholder="Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller mit verschlüsselten Dateien und verdächtigen Netzwerkverbindungen'"
|
|
style="min-height: 120px; resize: vertical; font-size: 0.9375rem; line-height: 1.5;"
|
|
maxlength="2000"
|
|
></textarea>
|
|
|
|
<!-- Privacy Notice -->
|
|
<div style="margin-top: 0.5rem; margin-bottom: 1rem;">
|
|
<p style="font-size: 0.75rem; color: var(--color-text-secondary); text-align: center; line-height: 1.4;">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
Ihre Anfrage wird an mistral.ai übertragen und unterliegt deren
|
|
<a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary); text-decoration: underline;">Datenschutzrichtlinien</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: center; gap: 1rem;">
|
|
<button id="ai-submit-btn" class="btn btn-accent" style="padding: 0.75rem 2rem;">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
|
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
|
|
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
|
|
<path d="M12 17h.01"/>
|
|
</svg>
|
|
Empfehlungen generieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;">
|
|
<div style="display: inline-block; margin-bottom: 1rem;">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" style="animation: pulse 2s ease-in-out infinite;">
|
|
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
|
|
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
|
|
<path d="M12 17h.01"/>
|
|
</svg>
|
|
</div>
|
|
<p style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="ai-error" class="ai-error" style="display: none; text-align: center; padding: 2rem;">
|
|
<div style="background-color: var(--color-error); color: white; padding: 1rem; border-radius: 0.5rem; max-width: 600px; margin: 0 auto;">
|
|
<h3 style="margin-bottom: 0.5rem;">Fehler bei der KI-Anfrage</h3>
|
|
<p id="ai-error-message" style="margin: 0;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Results -->
|
|
<div id="ai-results" class="ai-results" style="display: none;">
|
|
<!-- Results will be populated here by JavaScript -->
|
|
</div>
|
|
</section>
|
|
|
|
<script define:vars={{ tools, phases, domainAgnosticSoftware }}>
|
|
function sanitizeHTML(html) {
|
|
const div = document.createElement('div');
|
|
div.textContent = html;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const aiInterface = document.getElementById('ai-interface');
|
|
const aiInput = document.getElementById('ai-query-input');
|
|
const aiSubmitBtn = document.getElementById('ai-submit-btn');
|
|
const aiLoading = document.getElementById('ai-loading');
|
|
const aiError = document.getElementById('ai-error');
|
|
const aiErrorMessage = document.getElementById('ai-error-message');
|
|
const aiResults = document.getElementById('ai-results');
|
|
|
|
let currentRecommendation = null;
|
|
|
|
if (!aiInput || !aiSubmitBtn || !aiLoading || !aiError || !aiResults) {
|
|
console.error('AI interface elements not found');
|
|
return;
|
|
}
|
|
|
|
// Character counter for input
|
|
const updateCharacterCount = () => {
|
|
const length = aiInput.value.length;
|
|
const maxLength = 2000;
|
|
|
|
// Find or create character counter
|
|
let counter = document.getElementById('ai-char-counter');
|
|
if (!counter) {
|
|
counter = document.createElement('div');
|
|
counter.id = 'ai-char-counter';
|
|
counter.style.cssText = 'font-size: 0.75rem; color: var(--color-text-secondary); text-align: right; margin-top: 0.25rem;';
|
|
aiInput.parentNode.insertBefore(counter, aiInput.nextSibling);
|
|
}
|
|
|
|
counter.textContent = `${length}/${maxLength}`;
|
|
if (length > maxLength * 0.9) {
|
|
counter.style.color = 'var(--color-warning)';
|
|
} else {
|
|
counter.style.color = 'var(--color-text-secondary)';
|
|
}
|
|
};
|
|
|
|
aiInput.addEventListener('input', updateCharacterCount);
|
|
updateCharacterCount(); // Initial count
|
|
|
|
// Submit handler
|
|
const handleSubmit = async () => {
|
|
const query = aiInput.value.trim();
|
|
|
|
if (!query) {
|
|
alert('Bitte geben Sie eine Beschreibung Ihres Szenarios ein.');
|
|
return;
|
|
}
|
|
|
|
if (query.length < 10) {
|
|
alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
|
|
return;
|
|
}
|
|
|
|
// Hide previous results and errors
|
|
aiResults.style.display = 'none';
|
|
aiError.style.display = 'none';
|
|
aiLoading.style.display = 'block';
|
|
|
|
// Disable submit button
|
|
aiSubmitBtn.disabled = true;
|
|
aiSubmitBtn.textContent = 'Generiere Empfehlungen...';
|
|
|
|
try {
|
|
const response = await fetch('/api/ai/query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ query })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Unknown error');
|
|
}
|
|
|
|
// Store recommendation for restoration
|
|
currentRecommendation = data.recommendation;
|
|
|
|
// Display results
|
|
displayResults(data.recommendation, query);
|
|
aiLoading.style.display = 'none';
|
|
aiResults.style.display = 'block';
|
|
|
|
} catch (error) {
|
|
console.error('AI query failed:', error);
|
|
aiLoading.style.display = 'none';
|
|
aiError.style.display = 'block';
|
|
|
|
// Show user-friendly error messages
|
|
if (error.message.includes('429')) {
|
|
aiErrorMessage.textContent = 'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.';
|
|
} else if (error.message.includes('401')) {
|
|
aiErrorMessage.textContent = 'Authentifizierung erforderlich. Bitte melden Sie sich an.';
|
|
} else if (error.message.includes('503')) {
|
|
aiErrorMessage.textContent = 'KI-Service vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.';
|
|
} else {
|
|
aiErrorMessage.textContent = `Fehler: ${error.message}`;
|
|
}
|
|
} finally {
|
|
// Re-enable submit button
|
|
aiSubmitBtn.disabled = false;
|
|
aiSubmitBtn.innerHTML = `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
|
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
|
|
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
|
|
<path d="M12 17h.01"/>
|
|
</svg>
|
|
Empfehlungen generieren
|
|
`;
|
|
}
|
|
};
|
|
|
|
// Event listeners
|
|
aiSubmitBtn.addEventListener('click', handleSubmit);
|
|
|
|
aiInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
});
|
|
|
|
// Function to restore AI results when switching back to AI view
|
|
window.restoreAIResults = () => {
|
|
if (currentRecommendation) {
|
|
aiResults.style.display = 'block';
|
|
aiLoading.style.display = 'none';
|
|
aiError.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
// Helper function to format workflow suggestions as proper lists
|
|
function formatWorkflowSuggestion(text) {
|
|
// Check if text contains numbered list items (1., 2., 3., etc.)
|
|
const numberedListPattern = /(\d+\.\s)/g;
|
|
|
|
if (numberedListPattern.test(text)) {
|
|
// Split by numbered items and clean up
|
|
const items = text.split(/\d+\.\s/).filter(item => item.trim().length > 0);
|
|
|
|
if (items.length > 1) {
|
|
const listItems = items.map(item =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${item.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ol style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ol>`;
|
|
}
|
|
}
|
|
|
|
// Check for bullet points (-, *, •)
|
|
const bulletPattern = /^[\s]*[-\*•]\s/gm;
|
|
if (bulletPattern.test(text)) {
|
|
const items = text.split(/^[\s]*[-\*•]\s/gm).filter(item => item.trim().length > 0);
|
|
|
|
if (items.length > 1) {
|
|
const listItems = items.map(item =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${item.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ul style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ul>`;
|
|
}
|
|
}
|
|
|
|
// Check for line breaks that might indicate separate points
|
|
if (text.includes('\n')) {
|
|
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
if (lines.length > 1) {
|
|
const listItems = lines.map(line =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${line.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ul style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ul>`;
|
|
}
|
|
}
|
|
|
|
// Fallback to regular paragraph if no list format detected
|
|
return `<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${text}</p>`;
|
|
}
|
|
|
|
function displayResults(recommendation, originalQuery) {
|
|
// Group tools by phase
|
|
const toolsByPhase = {};
|
|
|
|
const phaseOrder = phases.map(phase => phase.id);
|
|
const phaseNames = phases.reduce((acc, phase) => {
|
|
acc[phase.id] = phase.name;
|
|
return acc;
|
|
}, {});
|
|
|
|
// Initialize phases
|
|
phaseOrder.forEach(phase => {
|
|
toolsByPhase[phase] = [];
|
|
});
|
|
|
|
// Group recommended tools by phase
|
|
recommendation.recommended_tools?.forEach(recTool => {
|
|
if (toolsByPhase[recTool.phase]) {
|
|
// Find full tool data
|
|
const fullTool = tools.find(t => t.name === recTool.name);
|
|
if (fullTool) {
|
|
toolsByPhase[recTool.phase].push({
|
|
...fullTool,
|
|
recommendation: recTool
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const resultsHTML = `
|
|
<div class="workflow-container">
|
|
<div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%); color: white; border-radius: 0.75rem;">
|
|
<h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">Empfohlener DFIR-Workflow</h3>
|
|
<p style="margin: 0; opacity: 0.9; line-height: 1.5;">
|
|
Basierend auf Ihrer Anfrage: "<em>${originalQuery.slice(0, 100)}${originalQuery.length > 100 ? '...' : ''}</em>"
|
|
</p>
|
|
</div>
|
|
|
|
${recommendation.scenario_analysis ? `
|
|
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-primary); display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 20h9"/>
|
|
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
</svg>
|
|
Szenario-Analyse
|
|
</h4>
|
|
${formatWorkflowSuggestion(recommendation.scenario_analysis)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${phaseOrder.map((phase, index) => {
|
|
const phaseTools = toolsByPhase[phase];
|
|
if (phaseTools.length === 0) return '';
|
|
|
|
return `
|
|
<div class="workflow-phase">
|
|
<div class="phase-header">
|
|
<div class="phase-number">${index + 1}</div>
|
|
<div class="phase-info">
|
|
<h3 class="phase-title">${phaseNames[phase]}</h3>
|
|
<div class="phase-tools">
|
|
${phaseTools.map(tool => {
|
|
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|
tool.projectUrl !== null &&
|
|
tool.projectUrl !== "" &&
|
|
tool.projectUrl.trim() !== "";
|
|
|
|
const priorityColors = {
|
|
high: 'var(--color-error)',
|
|
medium: 'var(--color-warning)',
|
|
low: 'var(--color-accent)'
|
|
};
|
|
|
|
return `
|
|
<div class="tool-recommendation ${hasValidProjectUrl ? 'hosted' : (tool.license !== 'Proprietary' ? 'oss' : '')}"
|
|
onclick="window.showToolDetails('${tool.name}')">
|
|
<div class="tool-rec-header">
|
|
<h4 class="tool-rec-name">${tool.name}</h4>
|
|
<span class="tool-rec-priority ${tool.recommendation.priority}"
|
|
style="background-color: ${priorityColors[tool.recommendation.priority]};">
|
|
${tool.recommendation.priority}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="tool-rec-justification">
|
|
"${tool.recommendation.justification}"
|
|
</div>
|
|
|
|
<div class="tool-rec-metadata">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
|
|
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
|
|
${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
|
|
<span class="badge" style="background-color: var(--color-bg-tertiary); color: var(--color-text);">${tool.skillLevel}</span>
|
|
</div>
|
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
|
${tool.platforms.join(', ')} • ${tool.license}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
${index < phaseOrder.length - 1 ? `
|
|
<div class="workflow-arrow">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<polyline points="19,12 12,19 5,12"/>
|
|
</svg>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
|
|
${recommendation.workflow_suggestion ? `
|
|
<div class="card" style="margin-top: 2rem; border-left: 4px solid var(--color-accent);">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-accent); display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="9,11 12,14 22,4"/>
|
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
|
</svg>
|
|
Workflow-Empfehlung
|
|
</h4>
|
|
${formatWorkflowSuggestion(recommendation.workflow_suggestion)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${recommendation.additional_notes ? `
|
|
<div class="card" style="margin-top: 1rem; background-color: var(--color-warning); color: white;">
|
|
<h4 style="margin: 0 0 1rem 0; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
Wichtige Hinweise
|
|
</h4>
|
|
<div style="color: white;">
|
|
${formatWorkflowSuggestion(recommendation.additional_notes).replace(/color: var\(--color-text\)/g, 'color: white')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
aiResults.innerHTML = ''; // Clear previous results first
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = resultsHTML;
|
|
// Sanitize any dynamic content before inserting
|
|
aiResults.appendChild(tempDiv);
|
|
}
|
|
});
|
|
</script> |