2025-07-17 14:57:35 +02:00

435 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import BaseLayout from '../layouts/BaseLayout.astro';
import ToolCard from '../components/ToolCard.astro';
import ToolFilters from '../components/ToolFilters.astro';
import ToolMatrix from '../components/ToolMatrix.astro';
import AIQueryInterface from '../components/AIQueryInterface.astro';
import { promises as fs } from 'fs';
import { load } from 'js-yaml';
import path from 'path';
// Load tools data
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
const yamlContent = await fs.readFile(yamlPath, 'utf8');
const data = load(yamlContent) as any;
const tools = data.tools;
---
<BaseLayout title="~/">
<!-- Hero Section -->
<section style="text-align: center; padding: 3rem 0; border-bottom: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1.5rem;">CC24 Incident Response Framework</h1>
<div style="max-width: 900px; margin: 0 auto;">
<p style="font-size: 1.25rem; margin-bottom: 1.5rem; color: var(--color-text);">
<strong>Das richtige Werkzeug zur richtigen Zeit</strong> in der digitalen Forensik entscheidet oft die Wahl des passenden Tools über Erfolg oder Misserfolg einer Untersuchung.
</p>
<p class="text-muted" style="font-size: 1.125rem; margin-bottom: 1.5rem; line-height: 1.7;">
Unser kuratiertes Verzeichnis bietet euch eine strukturierte Übersicht über bewährte DFIR-Tools,
kategorisiert nach forensischen Domänen und Untersuchungsphasen nach Kent, Chevalier, Grance & Dang.
</p>
<p class="text-muted" style="font-size: 1rem; margin-bottom: 2rem; line-height: 1.6;">
<span style="color: var(--color-primary); font-weight: 500;">Info:</span>
Die lila gekennzeichneten Einträge sind über das Single-Sign-On der CC24-Cloud direkt zugänglich.
Teilnehmer der Seminargruppe CC24-w1 können die gehostete Infrastruktur nutzen.
Sollten spezielle Berechtigungen für den Zugriff erforderlich sein oder etwas nicht funktionieren, <a href="/about#support">kontaktiert mich</a>.
</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<a href="/about" class="btn btn-primary" style="padding: 0.75rem 1.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
SSO & Zugang erfahren
</a>
<!-- New AI Query Button -->
<button id="ai-query-btn" class="btn btn-accent" style="padding: 0.75rem 1.5rem; background-color: var(--color-accent); color: white;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<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 befragen
</button>
<a href="#filters-section" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
Tools entdecken
</a>
</div>
</div>
</section>
<!-- Filters Section -->
<section id="filters-section" style="padding: 2rem 0;">
<ToolFilters />
</section>
<!-- AI Query Interface -->
<AIQueryInterface />
<!-- Tools Grid -->
<section id="tools-grid" style="padding-bottom: 2rem;">
<div class="grid-auto-fit" id="tools-container">
{tools.map((tool: any) => (
<ToolCard tool={tool} />
))}
</div>
<!-- No results message -->
<div id="no-results" style="display: none; text-align: center; padding: 4rem 0;">
<p class="text-muted" style="font-size: 1.125rem;">No tools found matching your criteria.</p>
</div>
</section>
<!-- Matrix View -->
<ToolMatrix />
</BaseLayout>
<script>
// Handle view changes and filtering
document.addEventListener('DOMContentLoaded', () => {
const toolsContainer = document.getElementById('tools-container');
const toolsGrid = document.getElementById('tools-grid');
const matrixContainer = document.getElementById('matrix-container');
const aiInterface = document.getElementById('ai-interface');
const filtersSection = document.getElementById('filters-section');
const noResults = document.getElementById('no-results');
const aiQueryBtn = document.getElementById('ai-query-btn');
// Guard against null elements
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
console.error('Required DOM elements not found');
return;
}
// Initial tools HTML
const initialToolsHTML = toolsContainer.innerHTML;
// Authentication check function
async function checkAuthentication() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired
};
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
}
// AI Query Button Handler
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => {
const authStatus = await checkAuthentication();
if (authStatus.authRequired && !authStatus.authenticated) {
// Redirect to login, then back to AI view
const returnUrl = `${window.location.pathname}?view=ai`;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
// Switch to AI view directly
switchToView('ai');
}
});
}
// Check URL parameters on page load for view switching
const urlParams = new URLSearchParams(window.location.search);
const viewParam = urlParams.get('view');
if (viewParam === 'ai') {
// User was redirected after authentication, switch to AI view
switchToView('ai');
}
// Function to switch between different views
function switchToView(view) {
// Hide all views first (using non-null assertions since we've already checked)
toolsGrid!.style.display = 'none';
matrixContainer!.style.display = 'none';
aiInterface!.style.display = 'none';
filtersSection!.style.display = 'none';
// Update view toggle buttons
const viewToggles = document.querySelectorAll('.view-toggle');
viewToggles.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
// Show appropriate view
switch (view) {
case 'ai':
aiInterface!.style.display = 'block';
// Keep filters visible in AI mode for view switching
filtersSection!.style.display = 'block';
// Hide filter controls in AI mode - AGGRESSIVE APPROACH
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInput = document.getElementById('search-tools') as HTMLElement;
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
// Hide all checkbox wrappers
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
// Hide the "Nach Tags filtern" header and button
const tagHeader = document.querySelector('.tag-header') as HTMLElement;
// Hide any elements containing "Proprietäre Software" or "Nach Tags filtern"
const filterLabels = document.querySelectorAll('label, .tag-header, h4, h3');
// Hide ALL input elements in the filters section (more aggressive)
const allInputs = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainer) domainPhaseContainer.style.display = 'none';
if (searchInput) searchInput.style.display = 'none';
if (tagCloud) tagCloud.style.display = 'none';
if (tagHeader) tagHeader.style.display = 'none';
// Hide ALL inputs in the filters section
allInputs.forEach(input => {
(input as HTMLElement).style.display = 'none';
});
checkboxWrappers.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'none';
});
// Hide specific filter section elements by text content
filterLabels.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'none';
}
});
// Restore previous AI results if they exist
if ((window as any).restoreAIResults) {
(window as any).restoreAIResults();
}
// Focus on the input
const aiInput = document.getElementById('ai-query-input');
if (aiInput) {
setTimeout(() => aiInput.focus(), 100);
}
break;
case 'matrix':
matrixContainer!.style.display = 'block';
filtersSection!.style.display = 'block';
// Show filter controls in matrix mode
const domainPhaseContainerMatrix = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInputMatrix = document.getElementById('search-tools') as HTMLElement;
const tagCloudMatrix = document.querySelector('.tag-cloud') as HTMLElement;
const checkboxWrappersMatrix = document.querySelectorAll('.checkbox-wrapper');
const tagHeaderMatrix = document.querySelector('.tag-header') as HTMLElement;
const filterLabelsMatrix = document.querySelectorAll('label, .tag-header, h4, h3');
const allInputsMatrix = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainerMatrix) domainPhaseContainerMatrix.style.display = 'grid';
if (searchInputMatrix) searchInputMatrix.style.display = 'block';
if (tagCloudMatrix) tagCloudMatrix.style.display = 'flex';
if (tagHeaderMatrix) tagHeaderMatrix.style.display = 'flex';
// Restore ALL inputs in the filters section
allInputsMatrix.forEach(input => {
(input as HTMLElement).style.display = 'block';
});
checkboxWrappersMatrix.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'flex';
});
// Restore filter section elements
filterLabelsMatrix.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'block';
}
});
break;
default: // grid
toolsGrid!.style.display = 'block';
filtersSection!.style.display = 'block';
// Show filter controls in grid mode
const domainPhaseContainerGrid = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInputGrid = document.getElementById('search-tools') as HTMLElement;
const tagCloudGrid = document.querySelector('.tag-cloud') as HTMLElement;
const checkboxWrappersGrid = document.querySelectorAll('.checkbox-wrapper');
const tagHeaderGrid = document.querySelector('.tag-header') as HTMLElement;
const filterLabelsGrid = document.querySelectorAll('label, .tag-header, h4, h3');
const allInputsGrid = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainerGrid) domainPhaseContainerGrid.style.display = 'grid';
if (searchInputGrid) searchInputGrid.style.display = 'block';
if (tagCloudGrid) tagCloudGrid.style.display = 'flex';
if (tagHeaderGrid) tagHeaderGrid.style.display = 'flex';
// Restore ALL inputs in the filters section
allInputsGrid.forEach(input => {
(input as HTMLElement).style.display = 'block';
});
checkboxWrappersGrid.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'flex';
});
// Restore filter section elements
filterLabelsGrid.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'block';
}
});
break;
}
// Clear URL parameters after switching
if (window.location.search) {
window.history.replaceState({}, '', window.location.pathname);
}
}
// Handle filtered results
window.addEventListener('toolsFiltered', (event: Event) => {
const customEvent = event as CustomEvent;
const filtered = customEvent.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix' || currentView === 'ai') {
// Matrix and AI views handle their own rendering
return;
}
// Clear container
toolsContainer.innerHTML = '';
if (filtered.length === 0) {
noResults.style.display = 'block';
} else {
noResults.style.display = 'none';
// Render filtered tools
filtered.forEach((tool: any) => {
const toolCard = createToolCard(tool);
toolsContainer.appendChild(toolCard);
});
}
});
// Handle view changes
window.addEventListener('viewChanged', (event: Event) => {
const customEvent = event as CustomEvent;
const view = customEvent.detail;
switchToView(view);
});
// Make switchToView available globally for the AI button
(window as any).switchToAIView = () => switchToView('ai');
function createToolCard(tool) {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const hasKnowledgebase = tool.knowledgebase === true;
const cardDiv = document.createElement('div');
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
cardDiv.className = cardClass;
cardDiv.style.cursor = 'pointer';
cardDiv.onclick = () => (window as any).showToolDetails(tool.name);
// Create responsive button HTML
let buttonHTML;
if (hasValidProjectUrl) {
// Two buttons for tools we're hosting - responsive layout
buttonHTML = `
<div class="button-container" style="display: flex; gap: 0.5rem;" onclick="event.stopPropagation();">
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary" style="flex: 1; text-align: center;">
Software-Homepage
</a>
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 1; text-align: center;">
Zugreifen
</a>
</div>
`;
} else {
// Single button for tools we're not hosting
buttonHTML = `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%; text-align: center;" onclick="event.stopPropagation();">
Software-Homepage
</a>
`;
}
cardDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
<h3 style="margin: 0; flex: 1; min-width: 0;">${tool.name}</h3>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; flex-shrink: 0;">
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
${hasKnowledgebase ? '<span class="badge badge-error">Infos 📖</span>' : ''}
</div>
</div>
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1rem; line-height: 1.5;">
${tool.description}
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 0.25rem; flex-wrap: wrap;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="9"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
<span class="text-muted" style="font-size: 0.75rem;">
${(tool.platforms || []).join(', ')}
</span>
</div>
<div style="display: flex; align-items: center; gap: 0.25rem; flex-wrap: wrap;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 6v6l4 2"></path>
</svg>
<span class="text-muted" style="font-size: 0.75rem;">
${tool.skillLevel}
</span>
</div>
<div style="display: flex; align-items: center; gap: 0.25rem; flex-wrap: wrap;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<span class="text-muted" style="font-size: 0.75rem;">
${tool.license}
</span>
</div>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1rem;">
${(tool.tags || []).map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
${buttonHTML}
`;
return cardDiv;
}
});
</script>