overcuriousity f4acf39ca7 progress
2025-07-22 22:36:08 +02:00

588 lines
24 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 { getToolsData } from '../utils/dataService.js';
// Load tools data
const data = await getToolsData();
const tools = data.tools;
---
<BaseLayout title="~/">
<!-- Hero Section -->
<section style="padding: 2rem 0 1rem; border-bottom: 1px solid var(--color-border);">
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 1.5rem; color: var(--color-primary);">CC24 // DFIR - Guide</h1>
<p style="font-size: 1.25rem; margin-bottom: 1.125rem; color: var(--color-text);">
<strong>Das richtige Werkzeug zur richtigen Zeit</strong> in der digitalen Forensik entscheidet oft die Wahl der passenden Methode oder Software über Erfolg oder Misserfolg einer Untersuchung.
</p>
<p class="text-muted" style="font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.7;">
Unser kuratiertes Verzeichnis bietet euch eine strukturierte Übersicht über bewährte Methoden und Tools,
kategorisiert nach forensischen Domänen und Untersuchungsphasen nach Kent, Chevalier, Grance & Dang.
</p>
<p class="text-muted" style="font-size: 0.9rem; 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>
Infos zu SSO & Zugang
</a>
<!-- 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>
<!-- NEW: Contribution Button -->
<a href="/contribute" class="btn" style="padding: 0.75rem 1.5rem; background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button="new">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
Beitragen
</a>
<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>
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>
// Extend Window interface for custom properties
declare global {
interface Window {
toolsData: any[];
showToolDetails: (toolName: string, modalType?: string) => void;
hideToolDetails: (modalType?: string) => void;
hideAllToolDetails: () => void;
clearAllFilters?: () => void;
restoreAIResults?: () => void;
switchToAIView?: () => void;
showShareDialog: (shareButton: HTMLElement) => void;
navigateToGrid: (toolName: string) => void;
navigateToMatrix: (toolName: string) => void;
}
}
// Handle view changes and filtering
document.addEventListener('DOMContentLoaded', () => {
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
const toolsGrid = document.getElementById('tools-grid') as HTMLElement;
const matrixContainer = document.getElementById('matrix-container') as HTMLElement;
const aiInterface = document.getElementById('ai-interface') as HTMLElement;
const filtersSection = document.getElementById('filters-section') as HTMLElement;
const noResults = document.getElementById('no-results') as HTMLElement;
const aiQueryBtn = document.getElementById('ai-query-btn') as HTMLButtonElement;
// Guard against null elements
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
console.error('Required DOM elements not found');
return;
}
// Simple sorting function
function sortTools(tools: any[], sortBy = 'default') {
const sorted = [...tools];
switch (sortBy) {
case 'alphabetical':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'difficulty':
const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
return sorted.sort((a, b) =>
(difficultyOrder[a.skillLevel as keyof typeof difficultyOrder] || 999) - (difficultyOrder[b.skillLevel as keyof typeof difficultyOrder] || 999)
);
case 'type':
const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
return sorted.sort((a, b) =>
(typeOrder[a.type as keyof typeof typeOrder] || 999) - (typeOrder[b.type as keyof typeof typeOrder] || 999)
);
case 'default':
default:
return sorted;
}
}
// 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) {
const returnUrl = `${window.location.pathname}?view=ai`;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
switchToView('ai');
}
});
}
// Function to switch between different views
function switchToView(view: string) {
// Hide all views first
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 and manage filter visibility
switch (view) {
case 'ai':
aiInterface.style.display = 'block';
filtersSection.style.display = 'block';
hideFilterControls();
if (window.restoreAIResults) {
window.restoreAIResults();
}
const aiInput = document.getElementById('ai-query-input') as HTMLTextAreaElement;
if (aiInput) {
setTimeout(() => aiInput.focus(), 100);
}
break;
case 'matrix':
matrixContainer.style.display = 'block';
filtersSection.style.display = 'block';
showFilterControls();
break;
default: // grid
toolsGrid.style.display = 'block';
filtersSection.style.display = 'block';
showFilterControls();
break;
}
// Clear URL parameters after switching
if (window.location.search) {
window.history.replaceState({}, '', window.location.pathname);
}
}
// Helper functions for filter control visibility
function hideFilterControls() {
const elements = [
'.domain-phase-container',
'#search-input',
'.tag-cloud',
'.tag-header',
'.checkbox-wrapper'
];
elements.forEach(selector => {
const element = document.querySelector(selector) as HTMLElement;
if (element) element.style.display = 'none';
});
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
allInputs.forEach(input => (input as HTMLElement).style.display = 'none');
}
function showFilterControls() {
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInput = document.getElementById('search-input') as HTMLElement;
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
const tagHeader = document.querySelector('.tag-header') as HTMLElement;
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
if (domainPhaseContainer) domainPhaseContainer.style.display = 'grid';
if (searchInput) searchInput.style.display = 'block';
if (tagCloud) tagCloud.style.display = 'flex';
if (tagHeader) tagHeader.style.display = 'flex';
allInputs.forEach(input => (input as HTMLElement).style.display = 'block');
checkboxWrappers.forEach(wrapper => (wrapper as HTMLElement).style.display = 'flex');
}
// Create tool slug from name
function createToolSlug(toolName: string): string {
return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
// Find tool by name or slug
function findTool(identifier: string) {
return window.toolsData.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
// Navigation functions for sharing
window.navigateToGrid = function(toolName: string) {
console.log('Navigating to grid for tool:', toolName);
// Switch to grid view first
switchToView('grid');
// Wait for view switch, then find and scroll to tool
setTimeout(() => {
// Clear any filters first
if (window.clearAllFilters) {
window.clearAllFilters();
}
// Wait for filters to clear and re-render
setTimeout(() => {
const toolCards = document.querySelectorAll('.tool-card');
let targetCard: Element | null = null;
toolCards.forEach(card => {
const cardTitle = card.querySelector('h3');
if (cardTitle) {
// Clean title text (remove icons and extra spaces)
const titleText = cardTitle.textContent?.replace(/[^\w\s\-\.]/g, '').trim();
if (titleText === toolName) {
targetCard = card;
}
}
});
if (targetCard) {
console.log('Found target card, scrolling...');
// Cast to Element to fix TypeScript issue
(targetCard as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
(targetCard as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
setTimeout(() => {
if (targetCard) {
(targetCard as HTMLElement).style.animation = '';
}
}, 2000);
} else {
console.warn('Tool card not found in grid:', toolName);
}
}, 300);
}, 200);
};
window.navigateToMatrix = function(toolName: string) {
console.log('Navigating to matrix for tool:', toolName);
// Switch to matrix view
switchToView('matrix');
// Wait for view switch and matrix to render
setTimeout(() => {
const toolChips = document.querySelectorAll('.tool-chip');
let firstMatch: Element | null = null;
let matchCount = 0;
toolChips.forEach(chip => {
// Clean the chip text (remove emoji and extra spaces)
const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim();
if (chipText === toolName) {
// Highlight this occurrence
(chip as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
matchCount++;
// Remember the first match for scrolling
if (!firstMatch) {
firstMatch = chip;
}
// Clean up animation after it completes
setTimeout(() => {
(chip as HTMLElement).style.animation = '';
}, 8000);
}
});
if (firstMatch) {
console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`);
// Cast to Element to fix TypeScript issue
(firstMatch as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
console.warn('Tool chip not found in matrix:', toolName);
}
}, 500);
};
// Handle URL parameters on page load
function handleSharedURL() {
const urlParams = new URLSearchParams(window.location.search);
const toolParam = urlParams.get('tool');
const viewParam = urlParams.get('view');
const modalParam = urlParams.get('modal');
if (!toolParam) {
// Check for AI view parameter
if (viewParam === 'ai') {
switchToView('ai');
}
return;
}
// Find the tool by name or slug
const tool = findTool(toolParam);
if (!tool) {
console.warn('Shared tool not found:', toolParam);
return;
}
// Clear URL parameters to avoid re-triggering
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
// Handle different view types
setTimeout(() => {
switch (viewParam) {
case 'grid':
window.navigateToGrid(tool.name);
break;
case 'matrix':
window.navigateToMatrix(tool.name);
break;
case 'modal':
if (modalParam === 'secondary') {
window.showToolDetails(tool.name, 'secondary');
} else {
window.showToolDetails(tool.name, 'primary');
}
break;
default:
window.navigateToGrid(tool.name);
}
}, 100);
}
// Handle filtered results
window.addEventListener('toolsFiltered', (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') {
return;
}
// Clear container
toolsContainer.innerHTML = '';
if (filtered.length === 0) {
noResults.style.display = 'block';
} else {
noResults.style.display = 'none';
const sortedTools = sortTools(filtered, 'default');
sortedTools.forEach((tool: any) => {
const toolCard = createToolCard(tool);
toolsContainer.appendChild(toolCard);
});
}
});
// Handle view changes
window.addEventListener('viewChanged', (event) => {
const customEvent = event as CustomEvent;
const view = customEvent.detail;
switchToView(view);
});
// Make switchToView available globally
window.switchToAIView = () => switchToView('ai');
// Tool card creation function
function createToolCard(tool: any): HTMLElement {
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
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 = isConcept ? 'card card-concept tool-card' :
isMethod ? 'card card-method tool-card' :
hasValidProjectUrl ? 'card card-hosted tool-card' :
(tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
cardDiv.className = cardClass;
cardDiv.style.cursor = 'pointer';
cardDiv.onclick = () => window.showToolDetails(tool.name);
// Create tool slug for share button
const toolSlug = createToolSlug(tool.name);
cardDiv.innerHTML = `
<div class="tool-card-header">
<h3>${tool.icon ? `<span style="margin-right: 0.5rem; font-size: 1.125rem;">${tool.icon}</span>` : ''}${tool.name}</h3>
<div class="tool-card-badges">
${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
${hasKnowledgebase ? '<span class="badge badge-error">📖</span>' : ''}
<button class="share-btn share-btn--small"
data-tool-name="${tool.name}"
data-tool-slug="${toolSlug}"
data-context="card"
onclick="event.stopPropagation(); window.showShareDialog(this)"
title="${tool.name} teilen"
aria-label="${tool.name} teilen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
</div>
</div>
<p class="text-muted">
${tool.description}
</p>
<div class="tool-card-metadata" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; line-height: 1;">
<div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
<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 style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
${(tool.platforms || []).slice(0, 2).join(', ')}${tool.platforms && tool.platforms.length > 2 ? '...' : ''}
</span>
</div>
<div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 6v6l4 2"></path>
</svg>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
${tool.skillLevel}
</span>
</div>
<div class="metadata-item" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary); flex-shrink: 1; min-width: 0;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
<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 style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
${isConcept ? 'Konzept' : isMethod ? 'Methode' : tool.license === 'Proprietary' ? 'Prop.' : tool.license?.split(' ')[0] || 'N/A'}
</span>
</div>
</div>
<div class="tool-tags-container">
${(tool.tags || []).slice(0, 8).map((tag: string) => `<span class="tag">${tag}</span>`).join('')}
</div>
<div class="tool-card-buttons" onclick="event.stopPropagation();">
${isConcept ? `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-concept); border-color: var(--color-concept);">
Mehr erfahren
</a>
` : isMethod ? `
<a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button" style="background-color: var(--color-method); border-color: var(--color-method);">
Zur Methode
</a>
` : hasValidProjectUrl ? `
<div class="button-row">
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
Homepage
</a>
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
Zugreifen
</a>
</div>
` : `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary single-button">
Software-Homepage
</a>
`}
</div>
`;
return cardDiv;
}
// Initialize URL handling
handleSharedURL();
});
</script>