Merge pull request 'main' (#11) from main into forensic-ai

Reviewed-on: #11
This commit is contained in:
Mario Stöckl 2025-08-11 12:02:56 +00:00
commit eff21865ea
54 changed files with 220592 additions and 4652 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1753528124767
"lastUpdateCheck": 1754571688630
}
}

View File

@ -1,257 +1,200 @@
# ============================================================================
# ForensicPathways Environment Configuration - COMPLETE
# ForensicPathways Environment Configuration
# ============================================================================
# Copy this file to .env and adjust the values below.
# This file covers ALL environment variables used in the codebase.
# Copy this file to .env and configure the REQUIRED values below.
# Optional features can be enabled by uncommenting and configuring them.
# ============================================================================
# 1. CORE APPLICATION SETTINGS (REQUIRED)
# 🔥 CRITICAL - REQUIRED FOR BASIC OPERATION
# ============================================================================
# Your application's public URL (used for redirects and links)
PUBLIC_BASE_URL=http://localhost:4321
# Secret key for session encryption (GENERATE A SECURE RANDOM STRING!)
AUTH_SECRET=your-secret-key-change-in-production-please
# Primary AI service for query processing (REQUIRED for core functionality)
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=your-ai-api-key-here
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# ============================================================================
# ⚙️ IMPORTANT - CORE FEATURES CONFIGURATION
# ============================================================================
# Application environment
NODE_ENV=development
# Secret key for session encryption (CHANGE IN PRODUCTION!)
AUTH_SECRET=your-secret-key-change-in-production-please
# ============================================================================
# 2. AI SERVICES CONFIGURATION (REQUIRED FOR AI FEATURES)
# ============================================================================
# Main AI Analysis Service (for query processing and recommendations)
# Examples: http://localhost:11434 (Ollama), https://api.mistral.ai, https://api.openai.com
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# Vector Embeddings Service (for semantic search)
# Leave API_KEY empty for Ollama, use actual key for cloud services
AI_EMBEDDINGS_ENABLED=true
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_API_KEY=
AI_EMBEDDINGS_MODEL=mistral-embed
# ============================================================================
# 3. AI PIPELINE CONFIGURATION (CONTEXT & PERFORMANCE TUNING)
# ============================================================================
# === SIMILARITY SEARCH STAGE ===
# How many similar tools/concepts embeddings search returns as candidates
# 🔍 This is the FIRST filter - vector similarity matching
# Lower = faster, less comprehensive | Higher = slower, more comprehensive
AI_EMBEDDING_CANDIDATES=50
# Minimum similarity score threshold (0.0-1.0)
# Lower = more results but less relevant | Higher = fewer but more relevant
AI_SIMILARITY_THRESHOLD=0.3
# === AI SELECTION FROM EMBEDDINGS ===
# When embeddings are enabled, how many top tools to send with full context
# 🎯 This is the SECOND filter - take best N from embeddings results
AI_EMBEDDING_SELECTION_LIMIT=30
AI_EMBEDDING_CONCEPTS_LIMIT=15
# Maximum tools/concepts sent to AI when embeddings are DISABLED
# Set to 0 for no limit (WARNING: may cause token overflow with large datasets)
AI_NO_EMBEDDINGS_TOOL_LIMIT=0
AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
# === AI SELECTION STAGE ===
# Maximum tools the AI can select from embedding candidates
# 🤖 This is the SECOND filter - AI intelligent selection
# Should be ≤ AI_EMBEDDING_CANDIDATES
AI_MAX_SELECTED_ITEMS=25
# === EMBEDDINGS EFFICIENCY THRESHOLDS ===
# Minimum tools required for embeddings to be considered useful
AI_EMBEDDINGS_MIN_TOOLS=8
# Maximum percentage of total tools that embeddings can return to be considered "filtering"
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
# === CONTEXT FLOW SUMMARY ===
# 1. Vector Search: 111 total tools → AI_EMBEDDING_CANDIDATES (40) most similar
# 2. AI Selection: 40 candidates → AI_MAX_SELECTED_ITEMS (25) best matches
# 3. Final Output: Recommendations based on analyzed subset
# ============================================================================
# 4. AI PERFORMANCE & RATE LIMITING
# ============================================================================
# === USER RATE LIMITS (per minute) ===
# Main queries per user per minute
AI_RATE_LIMIT_MAX_REQUESTS=4
# Total AI micro-task calls per user per minute (across all micro-tasks)
AI_MICRO_TASK_TOTAL_LIMIT=30
# === PIPELINE TIMING ===
# Delay between micro-tasks within a single query (milliseconds)
# Higher = gentler on AI service | Lower = faster responses
AI_MICRO_TASK_DELAY_MS=500
# Delay between queued requests (milliseconds)
AI_RATE_LIMIT_DELAY_MS=2000
# === EMBEDDINGS BATCH PROCESSING ===
# How many embeddings to generate per API call
AI_EMBEDDINGS_BATCH_SIZE=10
# Delay between embedding batches (milliseconds)
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
# Maximum tools sent to AI for detailed analysis (micro-tasks)
AI_MAX_TOOLS_TO_ANALYZE=20
AI_MAX_CONCEPTS_TO_ANALYZE=10
# ============================================================================
# 5. AI CONTEXT & TOKEN MANAGEMENT
# ============================================================================
# Maximum context tokens to maintain across micro-tasks
# Controls how much conversation history is preserved between AI calls
AI_MAX_CONTEXT_TOKENS=4000
# Maximum tokens per individual AI prompt
# Larger = more context per call | Smaller = faster responses
AI_MAX_PROMPT_TOKENS=2500
# ============================================================================
# 6. AUTHENTICATION & AUTHORIZATION (OPTIONAL)
# ============================================================================
# Enable authentication for different features
AUTHENTICATION_NECESSARY=false
# === AUTHENTICATION & SECURITY ===
# Set to true to require authentication (RECOMMENDED for production)
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
AUTHENTICATION_NECESSARY_AI=false
# OIDC Provider Settings (only needed if authentication enabled)
OIDC_ENDPOINT=https://your-oidc-provider.com
# OIDC Provider Configuration
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
# ============================================================================
# 7. FILE UPLOADS - NEXTCLOUD INTEGRATION (OPTIONAL)
# ============================================================================
# === FILE HANDLING ===
# Nextcloud server for file uploads (knowledgebase contributions)
# Leave empty to disable file upload functionality
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
# Nextcloud credentials (app password recommended)
NEXTCLOUD_USERNAME=your-username
NEXTCLOUD_PASSWORD=your-app-password
# Upload directory on Nextcloud (will be created if doesn't exist)
NEXTCLOUD_UPLOAD_PATH=/kb-media
# Public URL base for sharing uploaded files
# Usually your Nextcloud base URL + share path
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
# ============================================================================
# 8. GIT CONTRIBUTIONS - ISSUE CREATION (OPTIONAL)
# ============================================================================
# === COLLABORATION & CONTRIBUTIONS ===
# Git provider: gitea, github, or gitlab
GIT_PROVIDER=gitea
# Repository URL (used to extract owner/name)
# Example: https://git.example.com/owner/forensic-pathways.git
GIT_REPO_URL=https://git.example.com/owner/forensic-pathways.git
# API endpoint for your git provider
# Gitea: https://git.example.com/api/v1
# GitHub: https://api.github.com
# GitLab: https://gitlab.example.com/api/v4
GIT_API_ENDPOINT=https://git.example.com/api/v1
# Personal access token or API token for creating issues
# Generate this in your git provider's settings
GIT_API_TOKEN=your-git-api-token
# ============================================================================
# 9. AUDIT & DEBUGGING (OPTIONAL)
# ============================================================================
# Enable detailed audit trail of AI decision-making
# === AUDIT TRAIL (Important for forensic work) ===
FORENSIC_AUDIT_ENABLED=true
# Audit detail level: minimal, standard, verbose
FORENSIC_AUDIT_DETAIL_LEVEL=standard
# Audit retention time (hours)
FORENSIC_AUDIT_RETENTION_HOURS=24
# Maximum audit entries per request
FORENSIC_AUDIT_MAX_ENTRIES=50
# ============================================================================
# 10. SIMPLIFIED CONFIDENCE SCORING SYSTEM
# ============================================================================
# === AI SEMANTIC SEARCH ===
# Enable semantic search (highly recommended for better results)
AI_EMBEDDINGS_ENABLED=true
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_API_KEY=your-embeddings-api-key-here
AI_EMBEDDINGS_MODEL=mistral-embed
# Confidence component weights (must sum to 1.0)
CONFIDENCE_SEMANTIC_WEIGHT=0.5 # Weight for vector similarity quality
CONFIDENCE_SUITABILITY_WEIGHT=0.5 # Weight for AI-determined task fitness
# Confidence thresholds (0-100)
CONFIDENCE_MINIMUM_THRESHOLD=50 # Below this = weak recommendation
CONFIDENCE_MEDIUM_THRESHOLD=70 # 40-59 = weak, 60-79 = moderate
CONFIDENCE_HIGH_THRESHOLD=80 # 80+ = strong recommendation
# User rate limiting (queries per minute)
AI_RATE_LIMIT_MAX_REQUESTS=4
# ============================================================================
# PERFORMANCE TUNING PRESETS
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
# ============================================================================
# 🚀 FOR FASTER RESPONSES (prevent token overflow):
# AI_NO_EMBEDDINGS_TOOL_LIMIT=25
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=10
# === AI Pipeline Configuration ===
# These values are pre-tuned for optimal performance - adjust only if needed
# Vector similarity search settings
AI_EMBEDDING_CANDIDATES=50
AI_SIMILARITY_THRESHOLD=0.3
AI_EMBEDDING_SELECTION_LIMIT=30
AI_EMBEDDING_CONCEPTS_LIMIT=15
# === METHOD/TOOL BALANCE CONFIGURATION ===
# Controls the ratio of methods vs software tools sent to AI
# Methods = procedural guidance, best practices, workflows
# Software = actual tools and applications
# Values should sum to less than 1.0 (remainder is buffer)
AI_METHOD_SELECTION_RATIO=0.4 # 40% methods (increase for more procedural guidance)
AI_SOFTWARE_SELECTION_RATIO=0.5 # 50% software tools (increase for more tool recommendations)
# AI selection limits
AI_MAX_SELECTED_ITEMS=25
AI_MAX_TOOLS_TO_ANALYZE=20
AI_MAX_CONCEPTS_TO_ANALYZE=10
# Efficiency thresholds
AI_EMBEDDINGS_MIN_TOOLS=8
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
# Fallback limits when embeddings are disabled
AI_NO_EMBEDDINGS_TOOL_LIMIT=25
AI_NO_EMBEDDINGS_CONCEPT_LIMIT=10
# === Rate Limiting & Timing ===
AI_MICRO_TASK_TOTAL_LIMIT=30
AI_MICRO_TASK_DELAY_MS=500
AI_RATE_LIMIT_DELAY_MS=2000
# === Embeddings Batch Processing ===
AI_EMBEDDINGS_BATCH_SIZE=10
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
# === Context Management ===
AI_MAX_CONTEXT_TOKENS=4000
AI_MAX_PROMPT_TOKENS=2500
# === Confidence Scoring ===
CONFIDENCE_SEMANTIC_WEIGHT=0.5
CONFIDENCE_SUITABILITY_WEIGHT=0.5
CONFIDENCE_MINIMUM_THRESHOLD=50
CONFIDENCE_MEDIUM_THRESHOLD=70
CONFIDENCE_HIGH_THRESHOLD=80
# 🎯 FOR FULL DATABASE ACCESS (risk of truncation):
# AI_NO_EMBEDDINGS_TOOL_LIMIT=0
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
# 🔋 FOR LOW-POWER SYSTEMS:
# AI_NO_EMBEDDINGS_TOOL_LIMIT=15
# ============================================================================
# FEATURE COMBINATIONS GUIDE
# 📋 QUICK SETUP CHECKLIST
# ============================================================================
# 📝 BASIC SETUP (AI only):
# - Configure AI_ANALYZER_* and AI_EMBEDDINGS_*
# - Leave authentication, file uploads, and git disabled
# 🔐 WITH AUTHENTICATION:
# - Set AUTHENTICATION_NECESSARY_* to true
# - Configure OIDC_* settings
# 📁 WITH FILE UPLOADS:
# - Configure all NEXTCLOUD_* settings
# - Test connection before enabling in UI
# 🔄 WITH CONTRIBUTIONS:
# - Configure all GIT_* settings
# - Test API token permissions for issue creation
# 🔍 WITH FULL MONITORING:
# - Enable FORENSIC_AUDIT_ENABLED=true
# - Configure audit retention and detail level
#
# MINIMUM FOR DEVELOPMENT/TESTING:
# 1. ✅ Set PUBLIC_BASE_URL to your domain/localhost
# 2. ✅ Generate secure AUTH_SECRET (use: openssl rand -base64 32)
# 3. ✅ Configure AI_ANALYZER_ENDPOINT and API_KEY for your AI service
# 4. ✅ Test basic functionality
#
# PRODUCTION-READY DEPLOYMENT:
# 5. ✅ Enable authentication (configure AUTHENTICATION_* and OIDC_*)
# 6. ✅ Configure file handling (set NEXTCLOUD_* for uploads)
# 7. ✅ Enable collaboration (set GIT_* for contributions)
# 8. ✅ Enable audit trail (verify FORENSIC_AUDIT_ENABLED=true)
# 9. ✅ Configure embeddings for better search (AI_EMBEDDINGS_*)
# 10. ✅ Adjust rate limits based on expected usage
# ============================================================================
# SETUP CHECKLIST
# 🏃‍♂️ PERFORMANCE PRESETS - UNCOMMENT ONE IF NEEDED
# ============================================================================
# ✅ 1. Set PUBLIC_BASE_URL to your domain
# ✅ 2. Change AUTH_SECRET to a secure random string
# ✅ 3. Configure AI endpoints (Ollama: leave API_KEY empty)
# ✅ 4. Start with default AI values, tune based on performance
# ✅ 5. Enable authentication if needed (configure OIDC)
# ✅ 6. Configure Nextcloud if file uploads needed
# ✅ 7. Configure Git provider if contributions needed
# ✅ 8. Test with a simple query to verify pipeline works
# ✅ 9. Enable audit trail for transparency if desired
# ✅ 10. Tune performance settings based on usage patterns
# 🚀 SPEED OPTIMIZED (faster responses, less comprehensive):
# AI_EMBEDDING_CANDIDATES=25
# AI_MAX_SELECTED_ITEMS=15
# AI_MAX_TOOLS_TO_ANALYZE=10
# AI_MICRO_TASK_DELAY_MS=250
# 🎯 ACCURACY OPTIMIZED (slower responses, more comprehensive):
# AI_EMBEDDING_CANDIDATES=100
# AI_MAX_SELECTED_ITEMS=50
# AI_MAX_TOOLS_TO_ANALYZE=40
# AI_MICRO_TASK_DELAY_MS=1000
# 🔋 RESOURCE CONSTRAINED (for limited AI quotas):
# AI_RATE_LIMIT_MAX_REQUESTS=2
# AI_MICRO_TASK_TOTAL_LIMIT=15
# AI_MAX_TOOLS_TO_ANALYZE=10
# AI_EMBEDDINGS_ENABLED=false
# 🔬 METHOD-FOCUSED (more procedural guidance, less tools):
# AI_METHOD_SELECTION_RATIO=0.6
# AI_SOFTWARE_SELECTION_RATIO=0.3
# 🛠️ TOOL-FOCUSED (more software recommendations, less methods):
# AI_METHOD_SELECTION_RATIO=0.2
# AI_SOFTWARE_SELECTION_RATIO=0.7
# ============================================================================
# 🌐 AI SERVICE EXAMPLES
# ============================================================================
# === OLLAMA (Local) ===
# AI_ANALYZER_ENDPOINT=http://localhost:11434/v1/chat/completions
# AI_ANALYZER_API_KEY=
# AI_ANALYZER_MODEL=llama3.1:8b
# AI_EMBEDDINGS_ENDPOINT=http://localhost:11434/v1/embeddings
# AI_EMBEDDINGS_API_KEY=
# AI_EMBEDDINGS_MODEL=nomic-embed-text
# === OPENAI ===
# AI_ANALYZER_ENDPOINT=https://api.openai.com/v1/chat/completions
# AI_ANALYZER_API_KEY=sk-your-openai-key
# AI_ANALYZER_MODEL=gpt-4o-mini
# AI_EMBEDDINGS_ENDPOINT=https://api.openai.com/v1/embeddings
# AI_EMBEDDINGS_API_KEY=sk-your-openai-key
# AI_EMBEDDINGS_MODEL=text-embedding-3-small
# === MISTRAL (Default) ===
# AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
# AI_ANALYZER_API_KEY=your-mistral-key
# AI_ANALYZER_MODEL=mistral-small-latest
# AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
# AI_EMBEDDINGS_API_KEY=your-mistral-key
# AI_EMBEDDINGS_MODEL=mistral-embed

3
.gitignore vendored
View File

@ -11,6 +11,7 @@ _site/
dist/
.astro/
.astro/*
# Environment variables
.env
@ -85,4 +86,4 @@ temp/
.astro/data-store.json
.astro/content.d.ts
prompt.md
data/embeddings.json
.astro/settings.json

669
README.md
View File

@ -1,232 +1,150 @@
# ForensicPathways
Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR) Tools, Methoden und Konzepte mit KI-gestützten Workflow-Empfehlungen.
Ein umfassendes Verzeichnis digitaler Forensik- und Incident-Response-Tools mit KI-gestützten Empfehlungen basierend auf der NIST SP 800-86 Methodik.
## ✨ Funktionen
## Lizenz
### 🎯 Hauptansichten
- **Kachelansicht (Grid View):** Übersichtliche Kartenansicht aller Tools/Methoden
- **Matrix-Ansicht:** Interaktive Matrix nach forensischen Domänen und Untersuchungsphasen (NIST Framework)
- **Forensic-AI:** AI-gestützte Workflow-Empfehlungen basierend auf Szenario-Beschreibungen
Dieses Projekt ist unter der BSD-3-Clause-Lizenz lizenziert.
### 🔍 Navigation & Filterung
- **Tag-System:** Intelligente Filterung nach Kategorien und Eigenschaften
- **Volltext-Suche:** Durchsuchen von Namen, Beschreibungen und Tags
- **Domain/Phase-Filter:** Filterung nach forensischen Bereichen und Ermittlungsphasen
## Funktionen
### 📚 Inhaltstypen
- **Software/Tools:** Open Source und proprietäre forensische Software
- **Methoden:** Bewährte forensische Verfahren und Prozesse
- **Konzepte:** Grundlegendes Fachwissen und theoretische Grundlagen
### Kernfunktionalität
- **Umfassende Tool-Datenbank**: 100+ forensische Tools kategorisiert nach Domänen, Phasen und Skill-Levels
- **NIST SP 800-86 Integration**: Vier-Phasen-Methodik (Sammlung → Auswertung → Analyse → Berichterstattung)
- **Multiple Ansichtsmodi**: Kachelansicht, Matrix-Übersicht und KI-gestützte Empfehlungen
- **Erweiterte Suche**: Textsuche, semantische Embedding-basierte Suche und Multi-Kriterien-Filterung
- **Responsive Design**: Dark/Light-Mode-Unterstützung, mobile-optimierte Benutzeroberfläche
### 📖 Knowledgebase
- **Erweiterte Dokumentation:** Detaillierte Artikel zu Tools und Methoden
- **Praktische Anleitungen:** Installation, Konfiguration und Best Practices
- **Markdown-basiert:** Einfache Erstellung und Wartung von Inhalten
### KI-gestützte Analyse
- **Micro-Task-Pipeline**: Intelligente Tool-Auswahl durch mehrere KI-Analyseschritte
- **Semantische Suche**: Vector-Embeddings für natürlichsprachige Tool-Entdeckung
- **Konfidenz-Bewertung**: Transparente Vertrauensmetriken für KI-Empfehlungen
- **Audit-Trail**: Vollständige Entscheidungstransparenz mit detaillierter Protokollierung
- **Rate Limiting**: Intelligente Warteschlangenverwaltung und nutzerbasierte Ratenbegrenzung
### 🤝 Contribution-System
- **Tool/Methoden-Beiträge:** Webformular für neue Einträge
- **Knowledgebase-Artikel:** Artikel-Editor mit Datei-Upload
- **Git-Integration:** Automatische Issue-Erstellung für Review-Prozess
- **File-Management:** Nextcloud-Integration für Medien-Uploads
### Zusammenarbeit & Beiträge
- **Tool-Beiträge**: Neue Tools einreichen oder bestehende über Git-Integration bearbeiten
- **Knowledgebase**: Community-beigetragene Artikel und Dokumentation
- **File-Upload-System**: Nextcloud-Integration für Medien-Anhänge
- **Authentifizierung**: OIDC-Integration mit konfigurierbaren Anbietern
### 🔐 Authentifizierung
- **OIDC-Integration:** Single Sign-On mit OpenID Connect
- **Berechtigungssteuerung:** Schutz für AI-Features und Contribution-System
- **Session-Management:** Sichere JWT-basierte Sessions
### Enterprise-Funktionen
- **Warteschlangenverwaltung**: Ratenbegrenzte KI-Verarbeitung mit Echtzeit-Status-Updates
- **Audit-Protokollierung**: Umfassender forensischer Audit-Trail für KI-Entscheidungsfindung
- **Multi-Provider-Unterstützung**: Konfigurierbare KI-Services (Mistral AI, Ollama, OpenAI)
- **Git-Integration**: Automatisierte Issue-Erstellung für Beiträge (Gitea, GitHub, GitLab)
## 🛠 Technische Grundlage
## Datenmodell
- **Framework:** Astro 4.x mit TypeScript
- **Styling:** CSS Custom Properties mit Dark/Light Mode
- **API:** Node.js Backend mit Astro API Routes
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
Das System verwendet eine YAML-basierte Konfiguration in `src/data/tools.yaml`:
## 📋 Voraussetzungen
```yaml
tools:
- name: Tool Name
type: software|method|concept
description: Detaillierte Beschreibung
skillLevel: novice|beginner|intermediate|advanced|expert
url: https://tool-homepage.com
domains: [incident-response, static-investigations, ...]
phases: [data-collection, examination, analysis, reporting]
platforms: [Windows, Linux, macOS]
license: Lizenztyp
tags: [gui, commandline, ...]
related_concepts: [konzept1, konzept2]
# Optionale Felder
projectUrl: https://hosted-instance.com # Für CC24-Server gehostete Tools
knowledgebase: true # Hat KB-Artikel
accessType: download|hosted|cloud
- **Node.js:** Version 18.x oder höher
- **npm:** Version 8.x oder höher
- **Nginx:** Für Reverse Proxy (Produktion)
domains:
- id: incident-response
name: Incident Response & Breach-Untersuchung
## 🔧 Externe Abhängigkeiten (Optional)
phases:
- id: data-collection
name: Datensammlung
description: Imaging, Akquisition, Remote-Collection-Tools
### OIDC Provider
- **Zweck:** Benutzerauthentifizierung
- **Beispiel:** Nextcloud, Keycloak, Auth0
- **Konfiguration:** `OIDC_ENDPOINT`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`
### Nextcloud
- **Zweck:** File-Upload für Knowledgebase-Beiträge
- **Features:** Medien-Management, öffentliche Links
- **Konfiguration:** `NEXTCLOUD_ENDPOINT`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD`
### AI Service (Mistral/OpenAI-kompatibel)
- **Zweck:** KI-gestützte Tool-Empfehlungen
- **Konfiguration:** `AI_ANALYZER_ENDPOINT`, `AI_ANALYZER_API_KEY`, `AI_ANALYZER_MODEL`
### Uptime Kuma
- **Zweck:** Status-Monitoring für gehostete Services
- **Integration:** Status-Badges in der Service-Übersicht
### Git Provider (Gitea/GitHub/GitLab)
- **Zweck:** Issue-Erstellung für Contributions
- **Konfiguration:** `GIT_PROVIDER`, `GIT_API_ENDPOINT`, `GIT_API_TOKEN`
## 🚀 Installation
### Lokale Entwicklung
```bash
# Repository klonen
git clone https://git.cc24.dev/mstoeck3/forensic-pathways.git
cd forensic-pathways
# Dependencies installieren
npm install
# Umgebungsvariablen konfigurieren
cp .env.example .env
# .env bearbeiten (siehe Konfiguration unten)
npm run astro build
# Development Server starten
npm run dev
scenarios:
- id: scenario:memory_dump
icon: 🧠
friendly_name: RAM-Analyse
```
Die Seite ist dann unter `http://localhost:4321` verfügbar.
## AI Concept
### Produktions-Deployment
### Micro-Task Architecture
The AI system uses a sophisticated pipeline that breaks complex analysis into focused micro-tasks:
#### 1. System vorbereiten
1. **Scenario Analysis**: Understanding the forensic context
2. **Investigation Approach**: Determining optimal methodology
3. **Critical Considerations**: Identifying potential challenges
4. **Tool Selection**: Phase-specific or problem-specific recommendations
5. **Background Knowledge**: Relevant concepts and prerequisites
6. **Final Synthesis**: Integrated recommendations with confidence scoring
### Confidence Scoring
Each recommendation includes transparent confidence metrics:
- **Semantic Relevance**: Vector similarity between query and tool descriptions
- **Task Suitability**: AI-assessed fitness for the specific scenario
- **Uncertainty Factors**: Potential limitations and considerations
- **Strength Indicators**: Why the tool is well-suited
## NIST SP 800-86 Phases
The system organizes tools according to the four-phase NIST methodology:
1. **Data Collection**: Imaging, acquisition, and evidence preservation
2. **Examination**: Parsing, extraction, and initial data processing
3. **Analysis**: Deep investigation, correlation, and insight generation
4. **Reporting**: Documentation, visualization, and presentation
Each tool is mapped to appropriate phases, enabling workflow-based recommendations.
## Deployment
### Production Setup
1. **Build and Deploy**:
```bash
# System-Updates
sudo apt update && sudo apt upgrade -y
# Node.js installieren (Ubuntu/Debian)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Nginx installieren
sudo apt install nginx -y
# Systemd für Service-Management
sudo systemctl enable nginx
npm run build
sudo ./deploy.sh # Copies dist/ to /var/www/forensic-pathways
```
#### 2. Anwendung installieren
2. **Configuration**:
```bash
# Klonen des Repositorys
sudo git clone https://git.cc24.dev/mstoeck3/forensic-pathways /opt/forensic-pathways
cd /opt/forensic-pathways
# Abhängigkeiten installieren
sudo npm install
# Production-Build erstellen
sudo npm run build
npm run astro build
# Berechtigungen setzen
sudo chown -R www-data:www-data /opt/forensic-pathways
cd /var/www/forensic-pathways
sudo cp .env.example .env
sudo nano .env # Configure AI services, authentication, etc.
```
#### 3. Umgebungsvariablen konfigurieren
3. **Systemd Service** (`/etc/systemd/system/forensic-pathways.service`):
```ini
[Unit]
Description=ForensicPathways
After=network.target
Erstelle `/opt/forensic-pathways/.env`:
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/forensic-pathways
ExecStart=/usr/bin/node server/entry.mjs
Restart=always
RestartSec=10
Environment=NODE_ENV=production
```bash
# ===========================================
# ForensicPathways Environment Configuration
# ===========================================
# Authentication & OIDC (Required)
AUTH_SECRET=change-this-to-a-strong-secret-key-in-production
OIDC_ENDPOINT=https://your-oidc-provider.com
OIDC_CLIENT_ID=your-oidc-client-id
OIDC_CLIENT_SECRET=your-oidc-client-secret
# Auth Scopes - set to true in prod
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=true
AUTHENTICATION_NECESSARY_AI=true
# Application Configuration (Required)
PUBLIC_BASE_URL=https://your-domain.com
NODE_ENV=production
# AI Service Configuration (Required for AI features)
AI_ANALYZER_MODEL=mistral-large-latest
AI_ANALYZER_ENDPOINT=https://api.mistral.ai
AI_ANALYZER_API_KEY=your-mistral-api-key
AI_RATE_LIMIT_DELAY_MS=1000
# Git Integration (Required for contributions)
GIT_REPO_URL=https://git.cc24.dev/mstoeck3/forensic-pathways
GIT_PROVIDER=gitea
GIT_API_ENDPOINT=https://git.cc24.dev/api/v1
GIT_API_TOKEN=your-git-api-token
# File Upload Configuration (Optional)
LOCAL_UPLOAD_PATH=./public/uploads
# Nextcloud Integration (Optional)
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
NEXTCLOUD_USERNAME=your-username
NEXTCLOUD_PASSWORD=your-password
NEXTCLOUD_UPLOAD_PATH=/kb-media
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
[Install]
WantedBy=multi-user.target
```
```bash
# Berechtigungen sichern
sudo chmod 600 /opt/forensic-pathways/.env
sudo chown www-data:www-data /opt/forensic-pathways/.env
```
#### 4. Nginx konfigurieren
Erstelle `/etc/nginx/sites-available/forensic-pathways`:
4. **Nginx Configuration**:
```nginx
server {
listen 80;
server_name ihre-domain.de;
server_name forensic-pathways.yourdomain.com;
client_max_body_size 50M; # Important for uploads
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name ihre-domain.de;
# SSL Konfiguration (Let's Encrypt empfohlen)
ssl_certificate /etc/letsencrypt/live/ihre-domain.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ihre-domain.de/privkey.pem;
# Security Headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Static Files
location / {
try_files $uri $uri/ @nodejs;
root /opt/forensic-pathways/dist;
index index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API Routes to Node.js
location @nodejs {
proxy_pass http://localhost:4321;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@ -236,251 +154,162 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Upload limit
client_max_body_size 50M;
}
```
5. **Enable and Start**:
```bash
# Site aktivieren
sudo ln -s /etc/nginx/sites-available/forensic-pathways /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl enable forensic-pathways
sudo systemctl start forensic-pathways
sudo systemctl reload nginx
```
#### 5. Systemd Service einrichten
### Environment Configuration
Erstelle `/etc/systemd/system/forensic-pathways.service`:
```ini
[Unit]
Description=ForensicPathways DFIR Guide
After=network.target nginx.service
Wants=nginx.service
[Service]
Type=exec
User=www-data
Group=www-data
WorkingDirectory=/opt/forensic-pathways
Environment=NODE_ENV=production
ExecStart=/usr/bin/node ./dist/server/entry.mjs
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Security
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/forensic-pathways
CapabilityBoundingSet=
# Resource Limits
LimitNOFILE=65536
MemoryMax=512M
[Install]
WantedBy=multi-user.target
```
Key configuration in `.env`:
```bash
# Service aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable forensic-pathways
sudo systemctl start forensic-pathways
# Core Application
PUBLIC_BASE_URL=https://forensic-pathways.yourdomain.com
AUTH_SECRET=your-secure-random-secret
# Status prüfen
sudo systemctl status forensic-pathways
# AI Services (Required)
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=your-api-key
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# Vector Embeddings (Recommended)
AI_EMBEDDINGS_ENABLED=true
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_MODEL=mistral-embed
# Authentication (Optional)
AUTHENTICATION_NECESSARY_AI=false
OIDC_ENDPOINT=https://your-keycloak.com/auth/realms/your-realm
OIDC_CLIENT_ID=forensic-pathways
```
## 🔧 Konfiguration
## Externe Abhängigkeiten (Optionale Features)
### Minimalkonfiguration (ohne Auth)
### File-Upload-System
- **Nextcloud**: Primärer Speicher für Beitrags-Anhänge
- **Lokaler Fallback**: Automatischer Fallback zu lokalem Speicher bei Nextcloud-Ausfall
### Authentifizierungsanbieter
- **Keycloak**: Empfohlener OIDC-Provider
- **Andere OIDC**: Jeder OIDC-konforme Provider (Auth0, Azure AD, etc.)
### Git-Integration
- **Gitea**: Primärer Git-Provider für Beiträge
- **GitHub/GitLab**: Alternative Git-Provider unterstützt
### Monitoring
- **Uptime Kuma**: Service-Monitoring und Gesundheitschecks (optional)
### KI-Services
- **Mistral AI**: Empfohlen für Produktion (API-Schlüssel erforderlich)
- **Ollama**: Lokale Deployment-Option (kein API-Schlüssel benötigt)
- **OpenAI**: Alternative kommerzielle Anbieter
## Knowledgebase-System
### Artikel hinzufügen
Knowledgebase-Artikel werden in `src/content/knowledgebase/` als Markdown-Dateien mit Frontmatter gespeichert:
```markdown
---
title: "Tool-Konfigurationsanleitung"
description: "Schritt-für-Schritt-Setup-Anweisungen"
last_updated: 2024-01-15
author: "Ihr Name"
difficulty: intermediate
# Tool-Zuordnung (optional)
tool_name: "Autopsy"
related_tools: ["Volatility 3", "YARA"]
# Kategorisierung
categories: ["konfiguration", "setup"]
tags: ["gui", "installation", "windows"]
published: true
---
# Tool-Konfigurationsanleitung
Ihr Artikel-Inhalt hier...
## Voraussetzungen
- Systemanforderungen
- Abhängigkeiten
## Installationsschritte
1. Download von offizieller Quelle
2. Installer ausführen
3. Einstellungen konfigurieren
## Häufige Probleme
Lösungen für typische Probleme...
```
### Artikel-Struktur-Richtlinien
**Erforderliche Felder**:
- `title`: Klarer, beschreibender Titel
- `description`: Einzeilige Zusammenfassung für Auflistungen
- `last_updated`: Artikel-Änderungsdatum
- `published`: Boolean-Flag für Sichtbarkeit
**Optionale Felder**:
- `tool_name`: Zuordnung zu spezifischem Tool aus Datenbank
- `author`: Mitwirkender Name (Standard: "Anon")
- `difficulty`: Komplexitätslevel passend zu Tool-Skill-Levels
- `categories`: Breite Klassifizierungen
- `tags`: Spezifische Stichwörter für Entdeckung
- `related_tools`: Array verwandter Tool-Namen
**Inhalt-Richtlinien**:
- Standard-Markdown-Formatierung verwenden
- Praktische Beispiele und Code-Snippets einschließen
- Screenshots oder Diagramme bei Bedarf hinzufügen
- Zu verwandten Tools mit `[Tool Name](/tools/tool-slug)` Format verlinken
- Troubleshooting-Abschnitte für komplexe Tools einschließen
### Automatische Verarbeitung
1. Artikel werden automatisch beim Build indexiert
2. Tool-Zuordnungen erstellen bidirektionale Links
3. Suche umfasst Volltext-Inhalt und Metadaten
4. Verwandte Artikel erscheinen in Tool-Detail-Ansichten
## Entwicklung
```bash
# Nur für Tests geeignet
AUTHENTICATION_NECESSARY=false
PUBLIC_BASE_URL=http://localhost:4321
# Setup
npm install
cp .env.example .env
# Entwicklung
npm run dev
# Build
npm run build
# Deploy
sudo ./deploy.sh
```
### Tools-Datenbank
## Konfigurationsübersicht
Die Tools werden in `src/data/tools.yaml` verwaltet. Vollständiges Beispiel:
Die `.env.example`-Datei enthält umfassende Konfigurationsoptionen für alle Features. Die meisten Optionen haben sinnvolle Standardwerte, wobei nur die KI-Service-Konfiguration für volle Funktionalität erforderlich ist.
```yaml
tools:
- name: Autopsy
type: software # software|method|concept
description: >-
Die führende Open-Source-Alternative zu kommerziellen Forensik-Suiten mit
intuitiver grafischer Oberfläche. Besonders stark in der Timeline-Analyse,
Keyword-Suche und dem Carving gelöschter Dateien. Die modulare
Plugin-Architektur erlaubt Erweiterungen für spezielle
Untersuchungsszenarien.
icon: 📦
skillLevel: intermediate # novice|beginner|intermediate|advanced|expert
url: https://www.autopsy.com/
domains:
- incident-response
- static-investigations
- malware-analysis
- mobile-forensics
- cloud-forensics
phases:
- examination
- analysis
platforms:
- Windows
- Linux
related_concepts:
- SQL Query Fundamentals
- Hash Functions & Digital Signatures
accessType: download # download|web|api|cli|service
license: Apache 2.0
knowledgebase: false # true für erweiterte Dokumentation
tags:
- gui
- filesystem
- timeline-analysis
- carving
- artifact-extraction
- keyword-search
# Optional: Für gehostete Services
projectUrl: https://autopsy.ihre-domain.de
statusUrl: https://status.ihre-domain.de/api/badge/1/status
## Architektur
# Beispiel Methode
- name: Live Response Methodology
type: method
description: >-
Strukturierte Vorgehensweise zur Sammlung volatiler Daten
von laufenden Systemen ohne Shutdown.
icon: 📋
skillLevel: advanced
url: https://www.sans.org/white-papers/live-response/
domains:
- incident-response
phases:
- data-collection
related_concepts:
- Memory Forensics Fundamentals
tags:
- volatile-data
- live-analysis
- methodology
knowledgebase: true
# Beispiel Konzept
- name: Hash Functions & Digital Signatures
type: concept
description: >-
Kryptographische Grundlagen für Datenintegrität und
Authentifizierung in der digitalen Forensik.
icon: 🔐
skillLevel: intermediate
url: https://en.wikipedia.org/wiki/Cryptographic_hash_function
domains:
- incident-response
- static-investigations
- malware-analysis
phases:
- data-collection
- examination
tags:
- cryptography
- data-integrity
- evidence-preservation
knowledgebase: false
# Konfiguration der Domänen
domains:
- id: incident-response
name: Incident Response & Breach-Untersuchung
- id: static-investigations
name: Datenträgerforensik & Ermittlungen
- id: malware-analysis
name: Malware-Analyse & Reverse Engineering
- id: mobile-forensics
name: Mobile Geräte & App-Forensik
- id: cloud-forensics
name: Cloud & Virtuelle Umgebungen
# Konfiguration der Phasen (NIST Framework)
phases:
- id: data-collection
name: Datensammlung
description: Imaging, Acquisition, Remote Collection Tools
- id: examination
name: Auswertung
description: Parsing, Extraction, Initial Analysis Tools
- id: analysis
name: Analyse
description: Deep Analysis, Correlation, Visualization Tools
- id: reporting
name: Bericht & Präsentation
description: Documentation, Visualization, Presentation Tools
# Domänenübergreifende Kategorien
domain-agnostic-software:
- id: collaboration-general
name: Übergreifend & Kollaboration
description: Cross-cutting tools and collaboration platforms
- id: specific-os
name: Betriebssysteme
description: Operating Systems which focus on forensics
```
## 📦 Updates
```bash
# Repository aktualisieren
cd /opt/forensic-pathways
sudo git pull
# Dependencies aktualisieren
sudo npm install
# Rebuild
sudo npm run build
# Service neustarten
sudo systemctl restart forensic-pathways
```
## 💾 Backup
Wichtige Dateien für Backup:
```bash
/opt/forensic-pathways/src/data/tools.yaml
/opt/forensic-pathways/.env
/etc/nginx/sites-available/forensic-pathways
/etc/systemd/system/forensic-pathways.service
```
## 🤝 Beiträge
Contributions sind willkommen! Bitte:
1. Issue im Repository erstellen
2. Feature-Branch erstellen
3. Pull Request öffnen
4. Tests durchführen
## 📞 Support
Bei Problemen oder Fragen:
- **Issues:** [Repository Issues](https://git.cc24.dev/mstoeck3/forensic-pathways/issues)
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
## 📄 Lizenz
Dieses Projekt steht unter der **BSD-3-Clause** Lizenz.
- **Frontend**: Astro mit TypeScript, responsive CSS
- **Backend**: Node.js API-Routen mit intelligenter Ratenbegrenzung
- **KI-Pipeline**: Micro-Task-Architektur mit Audit-Protokollierung
- **Daten**: YAML-basierte Tool-Datenbank mit Git-basierten Beiträgen
- **Suche**: Dual-Mode Text- und semantische Vector-Suche
- **Auth**: OIDC-Integration mit Session-Management

202482
data/embeddings.json Normal file

File diff suppressed because it is too large Load Diff

863
deploy.sh Executable file
View File

@ -0,0 +1,863 @@
#!/bin/bash
# ForensicPathways Deployment Script *ownership-aware* + VISUAL ENHANCED
# Usage: sudo ./deploy.sh
set -e
# ═══════════════════════════════════════════════════════════════════════════════
# 🎨 VISUAL ENHANCEMENT SYSTEM
# ═══════════════════════════════════════════════════════════════════════════════
# Color palette
declare -r RED='\033[0;31m'
declare -r GREEN='\033[0;32m'
declare -r YELLOW='\033[0;33m'
declare -r BLUE='\033[0;34m'
declare -r MAGENTA='\033[0;35m'
declare -r CYAN='\033[0;36m'
declare -r WHITE='\033[0;37m'
declare -r BOLD='\033[1m'
declare -r DIM='\033[2m'
declare -r ITALIC='\033[3m'
declare -r UNDERLINE='\033[4m'
declare -r BLINK='\033[5m'
declare -r REVERSE='\033[7m'
declare -r RESET='\033[0m'
# Gradient colors
declare -r GRAD1='\033[38;5;196m' # Bright red
declare -r GRAD2='\033[38;5;202m' # Orange
declare -r GRAD3='\033[38;5;208m' # Dark orange
declare -r GRAD4='\033[38;5;214m' # Yellow orange
declare -r GRAD5='\033[38;5;220m' # Yellow
declare -r GRAD6='\033[38;5;118m' # Light green
declare -r GRAD7='\033[38;5;82m' # Green
declare -r GRAD8='\033[38;5;51m' # Cyan
declare -r GRAD9='\033[38;5;33m' # Blue
declare -r GRAD10='\033[38;5;129m' # Purple
# Background colors
declare -r BG_RED='\033[41m'
declare -r BG_GREEN='\033[42m'
declare -r BG_YELLOW='\033[43m'
declare -r BG_BLUE='\033[44m'
declare -r BG_MAGENTA='\033[45m'
declare -r BG_CYAN='\033[46m'
# Unicode box drawing
declare -r BOX_H='═'
declare -r BOX_V='║'
declare -r BOX_TL='╔'
declare -r BOX_TR='╗'
declare -r BOX_BL='╚'
declare -r BOX_BR='╝'
declare -r BOX_T='╦'
declare -r BOX_B='╩'
declare -r BOX_L='╠'
declare -r BOX_R='╣'
declare -r BOX_C='╬'
# Fancy Unicode characters
declare -r ARROW_R='▶'
declare -r ARROW_D='▼'
declare -r DIAMOND='◆'
declare -r STAR='★'
declare -r BULLET='●'
declare -r CIRCLE='◯'
declare -r SQUARE='▪'
declare -r TRIANGLE='▲'
# Animation frames
SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
PULSE_FRAMES=('●' '◐' '◑' '◒' '◓' '◔' '◕' '◖' '◗' '◘')
WAVE_FRAMES=('▁' '▂' '▃' '▄' '▅' '▆' '▇' '█' '▇' '▆' '▅' '▄' '▃' '▂')
# Terminal dimensions
COLS=$(tput cols 2>/dev/null || echo 80)
LINES=$(tput lines 2>/dev/null || echo 24)
# ═══════════════════════════════════════════════════════════════════════════════
# 🎯 VISUAL FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
print_gradient_text() {
local text="$1"
local colors=("$GRAD1" "$GRAD2" "$GRAD3" "$GRAD4" "$GRAD5" "$GRAD6" "$GRAD7" "$GRAD8" "$GRAD9" "$GRAD10")
local length=${#text}
local color_count=${#colors[@]}
for ((i=0; i<length; i++)); do
local color_idx=$((i * color_count / length))
printf "${colors[$color_idx]}${text:$i:1}"
done
printf "$RESET"
}
animate_text() {
local text="$1"
local delay="${2:-0.03}"
for ((i=0; i<=${#text}; i++)); do
printf "\r${CYAN}${text:0:$i}${RESET}"
sleep "$delay"
done
echo
}
draw_box() {
local title="$1"
local content="$2"
local width=${3:-$((COLS-4))}
local color="${4:-$CYAN}"
# Top border
printf "${color}${BOX_TL}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_TR}${RESET}\n"
# Title line
if [ -n "$title" ]; then
local title_len=${#title}
local padding=$(((width-title_len-2)/2))
printf "${color}${BOX_V}${RESET}"
for ((i=0; i<padding; i++)); do printf " "; done
printf "${BOLD}${WHITE}$title${RESET}"
for ((i=0; i<width-title_len-padding-2; i++)); do printf " "; done
printf "${color}${BOX_V}${RESET}\n"
# Separator
printf "${color}${BOX_L}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_R}${RESET}\n"
fi
# Content lines
while IFS= read -r line; do
local line_len=${#line}
printf "${color}${BOX_V}${RESET} %-$((width-4))s ${color}${BOX_V}${RESET}\n" "$line"
done <<< "$content"
# Bottom border
printf "${color}${BOX_BL}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_BR}${RESET}\n"
}
progress_bar() {
local current="$1"
local total="$2"
local width="${3:-50}"
local label="$4"
local percentage=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r${BOLD}%s${RESET} [" "$label"
# Filled portion with gradient
for ((i=0; i<filled; i++)); do
local color_idx=$((i * 10 / width))
case $color_idx in
0) printf "${GRAD1}${RESET}" ;;
1) printf "${GRAD2}${RESET}" ;;
2) printf "${GRAD3}${RESET}" ;;
3) printf "${GRAD4}${RESET}" ;;
4) printf "${GRAD5}${RESET}" ;;
5) printf "${GRAD6}${RESET}" ;;
6) printf "${GRAD7}${RESET}" ;;
7) printf "${GRAD8}${RESET}" ;;
8) printf "${GRAD9}${RESET}" ;;
*) printf "${GRAD10}${RESET}" ;;
esac
done
# Empty portion
for ((i=0; i<empty; i++)); do
printf "${DIM}${RESET}"
done
printf "] ${BOLD}%d%%${RESET}" "$percentage"
}
spinner() {
local pid=$1
local message="$2"
local frame=0
while kill -0 $pid 2>/dev/null; do
printf "\r${CYAN}${SPINNER_FRAMES[$frame]}${RESET} ${message}"
frame=$(((frame + 1) % ${#SPINNER_FRAMES[@]}))
sleep 0.1
done
printf "\r${GREEN}${RESET} ${message}\n"
}
pulsing_dots() {
local count="${1:-5}"
local cycles="${2:-3}"
for ((c=0; c<cycles; c++)); do
for frame in "${PULSE_FRAMES[@]}"; do
printf "\r${MAGENTA}"
for ((i=0; i<count; i++)); do
printf "$frame "
done
printf "${RESET}"
sleep 0.1
done
done
}
wave_animation() {
local width="${1:-$((COLS/2))}"
local cycles="${2:-2}"
for ((c=0; c<cycles; c++)); do
for frame in "${WAVE_FRAMES[@]}"; do
printf "\r${CYAN}"
for ((i=0; i<width; i++)); do
printf "$frame"
done
printf "${RESET}"
sleep 0.05
done
done
echo
}
celebrate() {
local width=$((COLS-10))
# Fireworks effect
for ((i=0; i<5; i++)); do
printf "\r"
for ((j=0; j<width; j++)); do
case $((RANDOM % 10)) in
0) printf "${GRAD1}*${RESET}" ;;
1) printf "${GRAD3}${RESET}" ;;
2) printf "${GRAD5}+${RESET}" ;;
3) printf "${GRAD7}×${RESET}" ;;
4) printf "${GRAD9}${RESET}" ;;
*) printf " " ;;
esac
done
sleep 0.2
done
echo
}
typewriter() {
local text="$1"
local delay="${2:-0.02}"
local color="${3:-$GREEN}"
printf "${color}"
for ((i=0; i<${#text}; i++)); do
printf "${text:$i:1}"
sleep "$delay"
done
printf "${RESET}\n"
}
fancy_header() {
local title="$1"
local subtitle="$2"
# Calculate centering
local title_len=${#title}
local subtitle_len=${#subtitle}
local box_width=$((COLS > 80 ? 80 : COLS-4))
local title_padding=$(((box_width-title_len)/2))
local subtitle_padding=$(((box_width-subtitle_len)/2))
echo
# Top gradient border
printf "${BOLD}"
for ((i=0; i<box_width; i++)); do
local color_idx=$((i * 10 / box_width))
case $color_idx in
0|1) printf "${GRAD1}${BOX_H}" ;;
2|3) printf "${GRAD3}${BOX_H}" ;;
4|5) printf "${GRAD5}${BOX_H}" ;;
6|7) printf "${GRAD7}${BOX_H}" ;;
*) printf "${GRAD9}${BOX_H}" ;;
esac
done
printf "${RESET}\n"
# Title line
printf "${GRAD1}${BOX_V}${RESET}"
for ((i=0; i<title_padding; i++)); do printf " "; done
print_gradient_text "$title"
for ((i=0; i<box_width-title_len-title_padding-2; i++)); do printf " "; done
printf "${GRAD1}${BOX_V}${RESET}\n"
# Subtitle line
if [ -n "$subtitle" ]; then
printf "${GRAD3}${BOX_V}${RESET}"
for ((i=0; i<subtitle_padding; i++)); do printf " "; done
printf "${ITALIC}${DIM}${subtitle}${RESET}"
for ((i=0; i<box_width-subtitle_len-subtitle_padding-2; i++)); do printf " "; done
printf "${GRAD3}${BOX_V}${RESET}\n"
fi
# Bottom gradient border
printf "${BOLD}"
for ((i=0; i<box_width; i++)); do
local color_idx=$((i * 10 / box_width))
case $color_idx in
0|1) printf "${GRAD1}${BOX_H}" ;;
2|3) printf "${GRAD3}${BOX_H}" ;;
4|5) printf "${GRAD5}${BOX_H}" ;;
6|7) printf "${GRAD7}${BOX_H}" ;;
*) printf "${GRAD9}${BOX_H}" ;;
esac
done
printf "${RESET}\n"
echo
}
section_header() {
local section_num="$1"
local title="$2"
local icon="$3"
echo
printf "${BOLD}${BG_BLUE}${WHITE} PHASE %s ${RESET} " "$section_num"
printf "${BOLD}${BLUE}%s %s${RESET}\n" "$icon" "$title"
# Animated underline
printf "${BLUE}"
for ((i=0; i<$((${#title}+10)); i++)); do
printf "▄"
sleep 0.01
done
printf "${RESET}\n"
}
status_ok() {
printf "${GREEN}${BOLD}${RESET} %s\n" "$1"
}
status_error() {
printf "${RED}${BOLD}${RESET} %s\n" "$1"
}
status_warning() {
printf "${YELLOW}${BOLD}${RESET} %s\n" "$1"
}
status_info() {
printf "${CYAN}${BOLD}${RESET} %s\n" "$1"
}
status_working() {
printf "${MAGENTA}${BOLD}${RESET} %s" "$1"
}
animated_countdown() {
local seconds="$1"
local message="$2"
for ((i=seconds; i>0; i--)); do
printf "\r${YELLOW}${BOLD}$message in ${i}s...${RESET}"
sleep 1
done
printf "\r${GREEN}${BOLD}🚀 $message${RESET} \n"
}
matrix_rain() {
local duration="${1:-2}"
local chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#\$^&*()_+-=[]{}|;:,.<>?"
for ((i=0; i<duration*10; i++)); do
printf "\r${GREEN}"
for ((j=0; j<$((COLS/3)); j++)); do
printf "%s" "${chars:$((RANDOM % ${#chars})):1}"
done
printf "${RESET}"
sleep 0.1
done
echo
}
# ═══════════════════════════════════════════════════════════════════════════════
# 🚀 MAIN SCRIPT VARIABLES
# ═══════════════════════════════════════════════════════════════════════════════
WEBROOT="/var/www/forensic-pathways"
LOG_DIR="$WEBROOT/logs"
DATA_DIR="$WEBROOT/data"
UPLOADS_DIR="$WEBROOT/public/uploads"
# Get original user who called sudo
ORIGINAL_USER="${SUDO_USER:-$USER}"
ORIGINAL_HOME=$(eval echo "~$ORIGINAL_USER")
# ═══════════════════════════════════════════════════════════════════════════════
# 🎬 SPECTACULAR OPENING SEQUENCE
# ═══════════════════════════════════════════════════════════════════════════════
# Terminal setup
tput civis # Hide cursor
# ASCII Art Banner
fancy_header "FORENSIC PATHWAYS DEPLOYMENT" "Advanced Visual Enhancement System"
# Matrix effect intro
printf "${DIM}${GREEN}Initializing deployment matrix...${RESET}\n"
matrix_rain 1
# System information display
draw_box "DEPLOYMENT PARAMETERS" "$(cat << PARAMS
Timestamp: $(date '+%Y-%m-%d %H:%M:%S')
Original User: $ORIGINAL_USER
Working Directory: $(pwd)
Target Webroot: $WEBROOT
Terminal Size: ${COLS}x${LINES}
PARAMS
)" 60 "$MAGENTA"
sleep 1
# Animated countdown
animated_countdown 3 "Starting deployment"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔒 PHASE 0: SAFETY CHECKS
# ═══════════════════════════════════════════════════════════════════════════════
section_header "0" "SECURITY & SAFETY VALIDATION" "🔒"
status_working "Verifying root privileges"
pulsing_dots 3 1
if [ "$EUID" -ne 0 ]; then
status_error "Script must be run as root (use sudo)"
echo
printf "${RED}${BOLD}${BG_YELLOW} DEPLOYMENT TERMINATED ${RESET}\n"
exit 1
fi
status_ok "Root privileges confirmed"
status_working "Validating project structure"
pulsing_dots 3 1
if [ ! -f "package.json" ] || [ ! -f "astro.config.mjs" ]; then
status_error "Must run from ForensicPathways project root"
status_info "Current directory: $(pwd)"
status_info "Files found: $(ls -la)"
echo
printf "${RED}${BOLD}${BG_YELLOW} DEPLOYMENT TERMINATED ${RESET}\n"
exit 1
fi
status_ok "Project structure validated"
# Security scan animation
printf "${CYAN}${BOLD}🔍 Running security scan${RESET}"
for i in {1..20}; do
printf "${CYAN}.${RESET}"
sleep 0.05
done
echo
status_ok "Security scan completed - all clear"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔧 PHASE 1: NPM BUILD SYSTEM
# ═══════════════════════════════════════════════════════════════════════════════
find_and_use_npm() {
section_header "1" "BUILD SYSTEM INITIALIZATION" "🔧"
printf "${CYAN}${BOLD}🔍 Scanning for npm installation...${RESET}\n"
wave_animation 30 1
# A) system-wide npm
if command -v npm &>/dev/null; then
status_ok "System npm located: $(which npm)"
printf "${MAGENTA}${BOLD}📦 Installing dependencies${RESET}"
{
sudo -u "$ORIGINAL_USER" npm install > /tmp/npm_install.log 2>&1 &
spinner $! "Installing dependencies"
}
printf "${MAGENTA}${BOLD}🏗️ Building application${RESET}"
{
sudo -u "$ORIGINAL_USER" npm run build > /tmp/npm_build.log 2>&1 &
spinner $! "Building application"
}
return 0
fi
# B) nvm-managed npm
printf "${YELLOW}🔍 Scanning for nvm installation...${RESET}\n"
if sudo -u "$ORIGINAL_USER" bash -c "
export NVM_DIR='$ORIGINAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && source \"\$NVM_DIR/nvm.sh\"
[ -s '$ORIGINAL_HOME/.bashrc' ] && source '$ORIGINAL_HOME/.bashrc'
command -v npm &>/dev/null
"; then
status_ok "NVM-managed npm located"
printf "${MAGENTA}${BOLD}📦 Installing dependencies with nvm${RESET}"
{
sudo -u "$ORIGINAL_USER" bash -c "
export NVM_DIR='$ORIGINAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && source \"\$NVM_DIR/nvm.sh\"
[ -s '$ORIGINAL_HOME/.bashrc' ] && source '$ORIGINAL_HOME/.bashrc'
npm install > /tmp/npm_install.log 2>&1
npm run build > /tmp/npm_build.log 2>&1
" &
spinner $! "Building with nvm"
}
return 0
fi
# C) Installation instructions with fancy formatting
draw_box "NPM NOT FOUND" "$(cat << NPMHELP
Please install Node.js and npm first:
Option 1 (apt):
sudo apt update && sudo apt install nodejs npm
Option 2 (NodeSource recommended):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
Option 3 (nvm as user):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc && nvm install 20
NPMHELP
)" 70 "$RED"
return 1
}
# ═══════════════════════════════════════════════════════════════════════════════
# 🏗️ PHASE 2: BUILD ORCHESTRATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "2" "BUILD ORCHESTRATION" "🏗️"
if [ ! -d "dist" ] || [ ! "$(ls -A dist 2>/dev/null)" ]; then
status_info "No dist/ directory found"
typewriter "Initiating build process..." 0.05 "$YELLOW"
find_and_use_npm || exit 1
else
status_ok "Existing dist/ directory detected"
echo
printf "${YELLOW}${BOLD}🤔 Rebuild application? ${RESET}${DIM}(y/N):${RESET} "
read -r REPLY
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
typewriter "Rebuilding application..." 0.05 "$CYAN"
find_and_use_npm || {
status_warning "Build failed, using existing dist/"
}
else
typewriter "Using existing build..." 0.05 "$GREEN"
fi
fi
# Build validation with dramatic effect
printf "${CYAN}${BOLD}🔍 Validating build output${RESET}"
pulsing_dots 8 2
if [ ! -d "dist" ] || [ ! "$(ls -A dist 2>/dev/null)" ]; then
echo
draw_box "BUILD FAILURE" "Build failed or dist/ directory is empty" 50 "$RED"
exit 1
fi
# Build success celebration
echo
printf "${GREEN}${BOLD}${BG_GREEN}${WHITE} BUILD SUCCESS ${RESET}\n"
celebrate
# ═══════════════════════════════════════════════════════════════════════════════
# 📁 PHASE 3: INFRASTRUCTURE SETUP
# ═══════════════════════════════════════════════════════════════════════════════
section_header "3" "INFRASTRUCTURE PROVISIONING" "📁"
status_working "Creating directory structure"
{
mkdir -p "$WEBROOT" "$LOG_DIR" "$DATA_DIR" "$UPLOADS_DIR" "$WEBROOT/src/data" &
spinner $! "Provisioning directories"
}
# Directory creation progress
DIRS=("$WEBROOT" "$LOG_DIR" "$DATA_DIR" "$UPLOADS_DIR" "$WEBROOT/src/data")
for i in "${!DIRS[@]}"; do
progress_bar $((i+1)) ${#DIRS[@]} 40 "Creating directories"
sleep 0.1
done
echo
status_ok "Directory infrastructure ready"
# ═══════════════════════════════════════════════════════════════════════════════
# 🚀 PHASE 4: APPLICATION DEPLOYMENT
# ═══════════════════════════════════════════════════════════════════════════════
section_header "4" "APPLICATION DEPLOYMENT" "🚀"
# File copy with visual progress
status_working "Deploying application files"
{
cp -r dist/. "$WEBROOT/" &
PID=$!
# Simple animated progress while copying
frame=0
while kill -0 $PID 2>/dev/null; do
printf "\r${MAGENTA}%s${RESET} Copying files..." "${SPINNER_FRAMES[$frame]}"
frame=$(((frame + 1) % ${#SPINNER_FRAMES[@]}))
sleep 0.1
done
wait $PID
printf "\r${GREEN}${RESET} Copying files... \n"
}
echo
SIZE=$(du -sh dist | cut -f1)
TOTAL_FILES=$(find dist -type f | wc -l)
status_ok "Application deployed ($SIZE, $TOTAL_FILES files)"
# Package.json copy with flair
printf "${MAGENTA}${BOLD}📋 Deploying package.json${RESET}"
pulsing_dots 3 1
cp package.json "$WEBROOT/"
status_ok "Package configuration deployed"
# ═══════════════════════════════════════════════════════════════════════════════
# ⚙️ PHASE 5: RUNTIME DEPENDENCY MANAGEMENT
# ═══════════════════════════════════════════════════════════════════════════════
section_header "5" "RUNTIME DEPENDENCY RESOLUTION" "⚙️"
typewriter "Transferring ownership for dependency installation..." 0.03 "$YELLOW"
chown -R "$ORIGINAL_USER":"$ORIGINAL_USER" "$WEBROOT"
printf "${CYAN}${BOLD}📦 Installing runtime dependencies${RESET}\n"
{
sudo -u "$ORIGINAL_USER" bash -c '
set -e
cd "'"$WEBROOT"'"
if command -v npm &>/dev/null; then
npm install --production > /tmp/runtime_deps.log 2>&1
else
export NVM_DIR="'$ORIGINAL_HOME'/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
[ -s "'$ORIGINAL_HOME'/.bashrc" ] && source "'$ORIGINAL_HOME'/.bashrc"
npm install --production > /tmp/runtime_deps.log 2>&1
fi
' &
spinner $! "Installing runtime dependencies"
}
# Dependency success effect
printf "${GREEN}${BOLD}🎯 Dependencies locked and loaded!${RESET}\n"
wave_animation 40 1
# ═══════════════════════════════════════════════════════════════════════════════
# 🗃️ PHASE 6: DATA & CONTENT ORCHESTRATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "6" "DATA & CONTENT ORCHESTRATION" "🗃️"
status_working "Deploying core data structures"
if [ -f "src/data/tools.yaml" ]; then
cp src/data/tools.yaml "$WEBROOT/src/data/"
TOOL_COUNT=$(grep -c "^ - name:" "src/data/tools.yaml" || echo "unknown")
status_ok "Tools database deployed ($TOOL_COUNT tools)"
else
status_error "Critical file missing: src/data/tools.yaml"
exit 1
fi
status_working "Deploying knowledge base"
if [ -d "src/content/knowledgebase" ]; then
mkdir -p "$WEBROOT/src/content"
cp -r src/content/knowledgebase "$WEBROOT/src/content/"
KB_COUNT=$(find src/content/knowledgebase -name "*.md" 2>/dev/null | wc -l)
status_ok "Knowledge base deployed ($KB_COUNT articles)"
# Knowledge base visualization
printf "${BLUE}${BOLD}📚 Knowledge Base Structure:${RESET}\n"
find src/content/knowledgebase -name "*.md" | head -5 | while read -r file; do
printf " ${CYAN}${DIAMOND}${RESET} ${file#src/content/knowledgebase/}\n"
done
if [ $KB_COUNT -gt 5 ]; then
printf " ${DIM}... and $((KB_COUNT-5)) more articles${RESET}\n"
fi
else
status_warning "No knowledge base directory found (optional)"
fi
# ═══════════════════════════════════════════════════════════════════════════════
# ⚙️ PHASE 7: ENVIRONMENT CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "7" "ENVIRONMENT CONFIGURATION" "⚙️"
printf "${YELLOW}${BOLD}🔧 Configuring environment${RESET}"
pulsing_dots 5 1
cp .env.example "$WEBROOT/.env"
status_ok "Environment template deployed"
draw_box "CONFIGURATION NOTICE" "IMPORTANT: Edit $WEBROOT/.env with your configuration" 60 "$YELLOW"
# ═══════════════════════════════════════════════════════════════════════════════
# 📝 PHASE 8: LOGGING INFRASTRUCTURE
# ═══════════════════════════════════════════════════════════════════════════════
section_header "8" "LOGGING INFRASTRUCTURE" "📝"
LOG_FILES=("access.log" "error.log" "ai-pipeline.log")
for i in "${!LOG_FILES[@]}"; do
progress_bar $((i+1)) ${#LOG_FILES[@]} 30 "Creating log files"
touch "$LOG_DIR/${LOG_FILES[$i]}"
sleep 0.2
done
echo
status_ok "Logging infrastructure established"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔐 PHASE 9: PERMISSION MATRIX
# ═══════════════════════════════════════════════════════════════════════════════
section_header "9" "PERMISSION MATRIX CONFIGURATION" "🔐"
typewriter "Implementing security hardening..." 0.04 "$RED"
# Permission operations with progress
PERM_OPERATIONS=(
"chown -R www-data:www-data $WEBROOT"
"chmod -R 755 $WEBROOT"
"chmod 600 $WEBROOT/.env"
"chmod 755 $DATA_DIR $UPLOADS_DIR $LOG_DIR"
"chmod 644 $LOG_DIR/*.log"
)
for i in "${!PERM_OPERATIONS[@]}"; do
progress_bar $((i+1)) ${#PERM_OPERATIONS[@]} 40 "Setting permissions"
eval "${PERM_OPERATIONS[$i]}"
sleep 0.3
done
echo
if [ -f "$WEBROOT/server/entry.mjs" ]; then
chmod 755 "$WEBROOT/server/entry.mjs"
status_ok "Server entry point permissions configured"
fi
status_ok "Permission matrix locked down"
# ═══════════════════════════════════════════════════════════════════════════════
# ✅ PHASE 10: DEPLOYMENT VALIDATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "10" "DEPLOYMENT VALIDATION MATRIX" "✅"
VALIDATION_ERRORS=0
VALIDATIONS=(
"$WEBROOT/.env|Environment configuration"
"$WEBROOT/src/data/tools.yaml|Tools database"
)
# Check application files (either index.html OR server directory)
if [ -f "$WEBROOT/index.html" ] || [ -d "$WEBROOT/server" ]; then
VALIDATIONS+=("APPLICATION_FILES_OK|Application files")
else
VALIDATIONS+=("APPLICATION_FILES_MISSING|Application files")
fi
echo
printf "${CYAN}${BOLD}🔍 Running comprehensive validation suite...${RESET}\n"
for validation in "${VALIDATIONS[@]}"; do
IFS='|' read -r file desc <<< "$validation"
printf "${YELLOW}Testing: %s${RESET}" "$desc"
pulsing_dots 3 1
if [[ "$file" == "APPLICATION_FILES_OK" ]]; then
status_ok "$desc validated"
elif [[ "$file" == "APPLICATION_FILES_MISSING" ]]; then
status_error "$desc missing"
((VALIDATION_ERRORS++))
elif [ -f "$file" ] || [ -d "$file" ]; then
status_ok "$desc validated"
else
status_error "$desc missing"
((VALIDATION_ERRORS++))
fi
done
# ═══════════════════════════════════════════════════════════════════════════════
# 🎊 FINAL RESULTS SPECTACULAR
# ═══════════════════════════════════════════════════════════════════════════════
echo
if [ $VALIDATION_ERRORS -eq 0 ]; then
# Success celebration sequence
printf "${GREEN}${BOLD}${BG_GREEN}${WHITE}"
for ((i=0; i<COLS; i++)); do printf "="; done
printf "${RESET}\n"
# Animated success banner
fancy_header "🎉 DEPLOYMENT SUCCESSFUL! 🎉" "All systems operational"
# Fireworks celebration
celebrate
# Next steps in a beautiful box
draw_box "🎯 MISSION BRIEFING - NEXT STEPS" "$(cat << STEPS
1. 🔧 Configure environment variables in $WEBROOT/.env
• Set PUBLIC_BASE_URL, AI service endpoints
• Configure AUTH_SECRET and database connections
2. 🔄 Restart system services:
sudo systemctl restart forensic-pathways
sudo systemctl reload nginx
3. 🔍 Monitor system health:
sudo systemctl status forensic-pathways
sudo tail -f $LOG_DIR/error.log
🌐 Application fortress established at: $WEBROOT
🎯 Ready for production deployment!
STEPS
)" 70 "$GREEN"
# Final celebration
echo
printf "${BOLD}"
print_gradient_text "🚀 FORENSIC PATHWAYS DEPLOYMENT COMPLETE 🚀"
echo
else
# Error summary
draw_box "⚠️ DEPLOYMENT COMPLETED WITH WARNINGS" "$(cat << WARNINGS
Found $VALIDATION_ERRORS validation issues
Please review and resolve before proceeding
WARNINGS
)" 60 "$YELLOW"
fi
# Final timestamp with style
echo
printf "${DIM}${ITALIC}Deployment completed at: "
printf "${BOLD}$(date '+%Y-%m-%d %H:%M:%S')${RESET}\n"
# Restore cursor
tput cnorm
# Final matrix effect fade-out
printf "${DIM}${GREEN}Deployment matrix shutting down...${RESET}\n"
matrix_rain 1
echo

File diff suppressed because it is too large Load Diff

View File

@ -25,33 +25,55 @@ const sortedTags = Object.entries(tagFrequency)
<div class="filters-container">
<!-- Search Section -->
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🔍 Suche</h3>
</div>
<div class="search-wrapper">
<div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<div class="filter-section">
<div class="filter-card-compact">
<div class="filter-header-compact">
<h3>🔍 Suche</h3>
</div>
<div class="search-row">
<div class="search-wrapper">
<div class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</div>
<input
type="text"
id="search-input"
placeholder="Software, Beschreibung oder Tags durchsuchen..."
class="search-input"
/>
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<!-- Semantic Search Toggle - Inline -->
<div id="semantic-search-container" class="semantic-search-inline hidden">
<label class="semantic-toggle-wrapper" title="Semantische Suche verwendet Embeddings. Dadurch kann mit natürlicher Sprache/Begriffen gesucht werden, die Ergebnisse richten sich nach der euklidischen Distanz.">
<input type="checkbox" id="semantic-search-enabled" disabled/>
<div class="semantic-checkbox-custom"></div>
<span class="semantic-toggle-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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>
Semantisch
</span>
</label>
</div>
</div>
<!-- Status Display -->
<div id="semantic-status" class="semantic-status hidden">
<span class="semantic-results-count"></span>
</div>
<input
type="text"
id="search-input"
placeholder="Software, Beschreibung oder Tags durchsuchen..."
class="search-input"
/>
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Primary Filters Section - ONLY Domain and Phase -->
<div class="filter-section">
@ -289,6 +311,10 @@ const sortedTags = Object.entries(tagFrequency)
const elements = {
searchInput: document.getElementById('search-input'),
clearSearch: document.getElementById('clear-search'),
semanticContainer: document.getElementById('semantic-search-container'),
semanticCheckbox: document.getElementById('semantic-search-enabled'),
semanticStatus: document.getElementById('semantic-status'),
semanticResultsCount: document.querySelector('.semantic-results-count'),
domainSelect: document.getElementById('domain-select'),
phaseSelect: document.getElementById('phase-select'),
typeSelect: document.getElementById('type-select'),
@ -324,6 +350,48 @@ const sortedTags = Object.entries(tagFrequency)
let selectedTags = new Set();
let selectedPhase = '';
let isTagCloudExpanded = false;
let semanticSearchEnabled = false;
let semanticSearchAvailable = false;
let lastSemanticResults = null;
async function checkEmbeddingsAvailability() {
try {
const res = await fetch('/api/ai/embeddings-status');
const { embeddings } = await res.json();
semanticSearchAvailable = embeddings?.enabled && embeddings?.initialized;
if (semanticSearchAvailable) {
elements.semanticContainer.classList.remove('hidden');
elements.semanticCheckbox.disabled = false;
}
} catch (err) {
console.error('[EMBEDDINGS] Status check failed:', err);
}
}
async function performSemanticSearch(query) {
if (!semanticSearchAvailable || !query.trim()) {
return null;
}
try {
const response = await fetch('/api/search/semantic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query.trim() })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data.results || [];
} catch (error) {
console.error('[SEMANTIC] Search failed:', error);
return null;
}
}
function toggleCollapsible(toggleBtn, content, storageKey) {
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
@ -494,7 +562,18 @@ const sortedTags = Object.entries(tagFrequency)
: `${count} von ${total} Tools`;
}
function filterTools() {
function updateSemanticStatus(results) {
if (!elements.semanticStatus || !elements.semanticResultsCount) return;
if (semanticSearchEnabled && results?.length > 0) {
elements.semanticStatus.classList.remove('hidden');
elements.semanticResultsCount.textContent = `${results.length} semantische Treffer`;
} else {
elements.semanticStatus.classList.add('hidden');
}
}
async function filterTools() {
const searchTerm = elements.searchInput.value.trim().toLowerCase();
const selectedDomain = elements.domainSelect.value;
const selectedPhaseFromSelect = elements.phaseSelect.value;
@ -508,15 +587,29 @@ const sortedTags = Object.entries(tagFrequency)
const activePhase = selectedPhaseFromSelect || selectedPhase;
const filtered = window.toolsData.filter(tool => {
if (searchTerm && !(
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
)) {
return false;
}
let filteredTools = window.toolsData;
let semanticResults = null;
if (semanticSearchEnabled && semanticSearchAvailable && searchTerm) {
semanticResults = await performSemanticSearch(searchTerm);
lastSemanticResults = semanticResults;
if (semanticResults?.length > 0) {
filteredTools = [...semanticResults];
}
} else {
lastSemanticResults = null;
if (searchTerm) {
filteredTools = window.toolsData.filter(tool =>
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
);
}
}
filteredTools = filteredTools.filter(tool => {
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
return false;
}
@ -560,13 +653,29 @@ const sortedTags = Object.entries(tagFrequency)
return true;
});
const finalResults = searchTerm && window.prioritizeSearchResults
? window.prioritizeSearchResults(filtered, searchTerm)
: filtered;
if (semanticSearchEnabled && lastSemanticResults) {
filteredTools.sort(
(a, b) => (b._semanticSimilarity || 0) - (a._semanticSimilarity || 0)
);
}
const finalResults = semanticSearchEnabled && lastSemanticResults
? filteredTools
: (searchTerm && window.prioritizeSearchResults
? window.prioritizeSearchResults(filteredTools, searchTerm)
: filteredTools);
updateResultsCounter(finalResults.length);
updateSemanticStatus(lastSemanticResults);
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
window.dispatchEvent(
new CustomEvent('toolsFiltered', {
detail: {
tools: finalResults,
semanticSearch: semanticSearchEnabled && !!lastSemanticResults,
},
})
);
}
function resetPrimaryFilters() {
@ -599,6 +708,10 @@ const sortedTags = Object.entries(tagFrequency)
function resetAllFilters() {
elements.searchInput.value = '';
elements.clearSearch.classList.add('hidden');
elements.semanticCheckbox.checked = false;
semanticSearchEnabled = false;
lastSemanticResults = null;
updateSemanticStatus(null);
resetPrimaryFilters();
resetAdvancedFilters();
resetTags();
@ -619,16 +732,29 @@ const sortedTags = Object.entries(tagFrequency)
filterTools();
});
if (elements.semanticCheckbox) {
elements.semanticCheckbox.addEventListener('change', (e) => {
semanticSearchEnabled = e.target.checked;
filterTools();
});
}
[elements.domainSelect, elements.phaseSelect, elements.typeSelect, elements.skillSelect,
elements.platformSelect, elements.licenseSelect, elements.accessSelect].forEach(select => {
select.addEventListener('change', filterTools);
if (select) {
select.addEventListener('change', filterTools);
}
});
[elements.hostedOnly, elements.knowledgebaseOnly].forEach(checkbox => {
checkbox.addEventListener('change', filterTools);
if (checkbox) {
checkbox.addEventListener('change', filterTools);
}
});
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
if (elements.tagCloudToggle) {
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
}
elements.tagCloudItems.forEach(item => {
item.addEventListener('click', () => {
@ -649,14 +775,24 @@ const sortedTags = Object.entries(tagFrequency)
b.classList.toggle('active', b.getAttribute('data-view') === view);
});
window.dispatchEvent(new CustomEvent('viewChanged', { detail: view }));
if (view === 'hosted') {
const hosted = window.toolsData.filter(tool => isToolHosted(tool));
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: hosted }));
if (window.switchToView) {
window.switchToView(view);
} else {
filterTools();
console.error('switchToView function not available');
}
window.dispatchEvent(new CustomEvent('viewChanged', {
detail: view,
triggeredByButton: true
}));
setTimeout(() => {
if (view === 'matrix') {
filterTools();
} else if (view === 'grid') {
filterTools();
}
}, 100);
});
});
@ -676,6 +812,7 @@ const sortedTags = Object.entries(tagFrequency)
window.clearTagFilters = resetTags;
window.clearAllFilters = resetAllFilters;
checkEmbeddingsAvailability();
initializeCollapsible();
initTagCloud();
filterTagCloud();

View File

@ -193,6 +193,16 @@ domains.forEach((domain: any) => {
</div>
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
// Ensure isToolHosted is available
if (!window.isToolHosted) {
window.isToolHosted = function(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
};
}
function getSelectedPhase() {
const activePhaseChip = document.querySelector('.phase-chip.active');
return activePhaseChip ? activePhaseChip.getAttribute('data-phase') : '';
@ -216,9 +226,7 @@ domains.forEach((domain: any) => {
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
if (domainRow) {
domainRow.classList.add('highlight-row');
}
if (domainRow) domainRow.classList.add('highlight-row');
}
if (selectedPhase) {
@ -231,9 +239,7 @@ domains.forEach((domain: any) => {
const columnIndex = phaseIndex + 1;
matrixTable.querySelectorAll(`tr`).forEach(row => {
const cell = row.children[columnIndex];
if (cell) {
cell.classList.add('highlight-column');
}
if (cell) cell.classList.add('highlight-column');
});
}
}
@ -267,9 +273,7 @@ domains.forEach((domain: any) => {
const params = new URLSearchParams();
params.set('tool', toolSlug);
params.set('view', view);
if (modal) {
params.set('modal', modal);
}
if (modal) params.set('modal', modal);
return `${baseUrl}?${params.toString()}`;
}
@ -301,7 +305,7 @@ domains.forEach((domain: any) => {
}
}
window.showShareDialog = function(shareButton) {
function showShareDialog(shareButton) {
const toolName = shareButton.getAttribute('data-tool-name');
const context = shareButton.getAttribute('data-context');
@ -438,16 +442,11 @@ domains.forEach((domain: any) => {
copyToClipboard(url, btn);
});
});
};
}
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showToolDetails = function(toolName, modalType = 'primary') {
function showToolDetails(toolName, modalType = 'primary') {
const tool = toolsData.find(t => t.name === toolName);
if (!tool) {
console.error('Tool not found:', toolName);
return;
}
if (!tool) return;
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
@ -462,10 +461,7 @@ domains.forEach((domain: any) => {
};
for (const [key, element] of Object.entries(elements)) {
if (!element) {
console.error(`Element not found: tool-${key}-${modalType}`);
return;
}
if (!element) return;
}
const iconHtml = tool.icon ? `<span class="mr-3 text-xl">${tool.icon}</span>` : '';
@ -709,9 +705,9 @@ domains.forEach((domain: any) => {
if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
}
};
}
window.hideToolDetails = function(modalType = 'both') {
function hideToolDetails(modalType = 'both') {
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
@ -763,28 +759,44 @@ domains.forEach((domain: any) => {
} else {
document.body.classList.remove('modals-side-by-side');
}
};
}
window.hideAllToolDetails = function() {
window.hideToolDetails('both');
};
function hideAllToolDetails() {
hideToolDetails('both');
}
// Register all functions globally
window.showToolDetails = showToolDetails;
window.hideToolDetails = hideToolDetails;
window.hideAllToolDetails = hideAllToolDetails;
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showShareDialog = showShareDialog;
// Register matrix-prefixed versions for delegation
window.matrixShowToolDetails = showToolDetails;
window.matrixHideToolDetails = hideToolDetails;
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
if (view === 'matrix') {
setTimeout(updateMatrixHighlighting, 100);
setTimeout(() => {
if (window.filterTools && typeof window.filterTools === 'function') {
window.filterTools();
} else {
const allTools = window.toolsData || [];
window.dispatchEvent(new CustomEvent('toolsFiltered', {
detail: {
tools: allTools,
semanticSearch: false
}
}));
}
}, 100);
}
});
window.addEventListener('toolsFiltered', (event) => {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
setTimeout(updateMatrixHighlighting, 50);
}
});
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const { tools: filtered, semanticSearch } = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
@ -793,13 +805,6 @@ domains.forEach((domain: any) => {
const domainAgnosticPhaseIds = domainAgnosticSoftware.map(section => section.id);
const isDomainAgnosticPhase = domainAgnosticPhaseIds.includes(selectedPhase);
domainAgnosticSoftware.forEach(sectionData => {
const section = document.getElementById(`domain-agnostic-section-${sectionData.id}`);
const container = document.getElementById(`domain-agnostic-tools-${sectionData.id}`);
if (!section || !container) return;
});
if (!isDomainAgnosticPhase) {
document.getElementById('dfir-matrix-section').style.display = 'block';
@ -808,9 +813,7 @@ domains.forEach((domain: any) => {
});
filtered.forEach(tool => {
if (tool.type === 'concept') {
return;
}
if (tool.type === 'concept') return;
const isMethod = tool.type === 'method';
const hasValidProjectUrl = window.isToolHosted(tool);
@ -827,6 +830,7 @@ domains.forEach((domain: any) => {
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
chip.className = `tool-chip ${chipClass}`;
chip.setAttribute('data-tool-name', tool.name);
chip.setAttribute('title', `${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`);
chip.innerHTML = `${tool.name}${tool.knowledgebase === true ? '<span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>' : ''}`;
chip.onclick = () => window.showToolDetails(tool.name);

View File

@ -1,232 +1,307 @@
// src/config/prompts.ts - Centralized German prompts for AI pipeline
// src/config/prompts.ts - Enhanced with phase completion reasoning
export const AI_PROMPTS = {
toolSelection: (mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number) => {
const modeInstruction = mode === 'workflow'
? 'Der Benutzer möchte einen UMFASSENDEN WORKFLOW mit mehreren Tools/Methoden über verschiedene Phasen. Wählen Sie 15-25 Tools aus, die den vollständigen Untersuchungslebenszyklus abdecken.'
: 'Der Benutzer möchte SPEZIFISCHE TOOLS/METHODEN, die ihr konkretes Problem direkt lösen. Wählen Sie 3-8 Tools aus, die am relevantesten und effektivsten sind.';
? 'Workflow mit 15-25 Items über alle Phasen. PFLICHT: Mindestens 40% Methoden, Rest Tools/Konzepte.'
: 'Spezifische Lösung mit 4-10 Items. PFLICHT: Mindestens 30% Methoden wenn verfügbar.';
return `Sie sind ein DFIR-Experte mit Zugang zur kompletten forensischen Tool-Datenbank. Sie müssen die relevantesten Tools und Konzepte für diese spezifische Anfrage auswählen.
return `Du bist ein DFIR-Experte. Wähle die BESTEN Items aus dem vorgefilterten Set.
AUSWAHLMETHODE: ${selectionMethod}
${selectionMethod === 'embeddings_candidates' ?
'Diese Tools wurden durch Vektor-Ähnlichkeit vorgefiltert, sie sind bereits relevant. Ihre Aufgabe ist es, die BESTEN aus diesem relevanten Set auszuwählen.' :
'Sie haben Zugang zur vollständigen Tool-Datenbank. Wählen Sie die relevantesten Tools für die Anfrage aus.'}
'✓ Semantisch relevante Items bereits vorgefiltert\n✓ Wähle die BESTEN für die konkrete Aufgabe' :
'✓ Vollständige Datenbank verfügbar\n✓ Wähle die relevantesten Items'}
${modeInstruction}
BENUTZER-ANFRAGE: "${userQuery}"
ANFRAGE: "${userQuery}"
KRITISCHE AUSWAHLPRINZIPIEN:
1. **KONTEXT ÜBER POPULARITÄT**: Verwenden Sie nicht automatisch "berühmte" Tools wie Volatility, Wireshark oder Autopsy nur weil sie bekannt sind. Wählen Sie basierend auf den SPEZIFISCHEN Szenario-Anforderungen.
VERFÜGBARE ITEM-TYPEN:
- TOOLS (type: "software"/"method") praktische Anwendungen und Vorgehensweisen
- KONZEPTE (type: "concept") theoretisches Wissen und Methodiken
2. **METHODOLOGIE vs SOFTWARE**:
- Für SCHNELLE/DRINGENDE Szenarien Priorisieren Sie METHODEN und schnelle Antwort-Ansätze
- Für ZEITKRITISCHE Vorfälle Wählen Sie Triage-Methoden über tiefe Analyse-Tools
- Für UMFASSENDE Analysen Dann betrachten Sie detaillierte Software-Tools
- METHODEN (Typ: "method") sind oft besser als SOFTWARE für prozedurale Anleitung
AUSWAHLSTRATEGIE:
1. **ERSTE PRIORITÄT: Relevanz zur Anfrage**
- Direkt anwendbar auf das Problem
- Löst die Kernherausforderung
3. **SZENARIO-SPEZIFISCHE LOGIK**:
- "Schnell/Quick/Dringend/Triage" Szenarien Rapid Incident Response und Triage METHODE > Volatility
- "Industrial/SCADA/ICS" Szenarien Spezialisierte ICS-Tools > generische Netzwerk-Tools
- "Mobile/Android/iOS" Szenarien Mobile-spezifische Tools > Desktop-Forensik-Tools
- "Speicher-Analyse dringend benötigt" Schnelle Speicher-Tools/Methoden > umfassende Volatility-Analyse
2. **ZWEITE PRIORITÄT: Ausgewogene Mischung**
- Tools/Methoden für praktische Umsetzung selectedTools
- Konzepte für methodisches Verständnis selectedConcepts
- WICHTIG: Auch Konzepte auswählen, nicht nur Tools!
ANALYSE-ANWEISUNGEN:
1. Lesen Sie die VOLLSTÄNDIGE Beschreibung jedes Tools/Konzepts
2. Berücksichtigen Sie ALLE Tags, Plattformen, verwandte Tools und Metadaten
3. **PASSENDE DRINGLICHKEIT**: Schnelle Szenarien brauchen schnelle Methoden, nicht tiefe Analyse-Tools
4. **PASSENDE SPEZIFITÄT**: Spezialisierte Szenarien brauchen spezialisierte Tools, nicht generische
5. **BERÜCKSICHTIGEN SIE DEN TYP**: Methoden bieten prozedurale Anleitung, Software bietet technische Fähigkeiten
3. **QUALITÄT > QUANTITÄT**
- Lieber weniger perfekte Items als viele mittelmäßige
- Jedes Item muss begründbar sein
Wählen Sie die relevantesten Elemente aus (max ${maxSelectedItems} insgesamt).
AUSWAHLREGELN:
- Wähle ${mode === 'workflow' ? '15-25' : '4-10'} Items total, max ${maxSelectedItems}
- BEIDE Arrays füllen: selectedTools UND selectedConcepts
- Mindestens 1-2 Konzepte auswählen für methodische Fundierung
- Tools: 40% Methoden (type="method"), Rest Software (type="software")
Antworten Sie NUR mit diesem JSON-Format:
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT:
{
"selectedTools": ["Tool Name 1", "Tool Name 2", ...],
"selectedConcepts": ["Konzept Name 1", "Konzept Name 2", ...],
"reasoning": "Detaillierte Erklärung, warum diese spezifischen Tools für diese Anfrage ausgewählt wurden, und warum bestimmte populäre Tools NICHT ausgewählt wurden, falls sie für den Szenario-Kontext ungeeignet waren"
"selectedTools": ["ToolName1", "MethodName1", ...],
"selectedConcepts": ["ConceptName1", "ConceptName2", ...],
"reasoning": "Kurze Begründung mit Erwähnung der Tool/Konzept-Balance"
}`;
},
toolSelectionWithData: (basePrompt: string, toolsToSend: any[], conceptsToSend: any[]) => {
return `${basePrompt}
VERFÜGBARE TOOLS (${toolsToSend.length} Items - Methoden und Software):
${JSON.stringify(toolsToSend, null, 2)}
VERFÜGBARE KONZEPTE (${conceptsToSend.length} Items - theoretisches Wissen):
${JSON.stringify(conceptsToSend, null, 2)}
WICHTIGER HINWEIS: Wähle sowohl aus TOOLS als auch aus KONZEPTEN aus! Konzepte sind essentiell für methodische Fundierung.`;
},
scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
const analysisType = isWorkflow ? 'forensische Szenario' : 'technische Problem';
const considerations = isWorkflow ?
`- Angriffsvektoren und Bedrohungsmodellierung nach MITRE ATT&CK
- Betroffene Systeme und kritische Infrastrukturen
- Zeitkritische Faktoren und Beweiserhaltung
- Forensische Artefakte und Datenquellen` :
`- Spezifische forensische Herausforderungen
- Verfügbare Datenquellen und deren Integrität
- Methodische Anforderungen für rechtssichere Analyse`;
const analysisType = isWorkflow ? 'Szenario' : 'Problem';
const focus = isWorkflow ?
'Angriffsvektoren, betroffene Systeme, Zeitkritikalität' :
'Kernherausforderung, verfügbare Daten, methodische Anforderungen';
return `Sie sind ein erfahrener DFIR-Experte. Analysieren Sie das folgende ${analysisType}.
return `DFIR-Experte: Analysiere das ${analysisType}.
${isWorkflow ? 'FORENSISCHES SZENARIO' : 'TECHNISCHES PROBLEM'}: "${userQuery}"
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Führen Sie eine systematische ${isWorkflow ? 'Szenario-Analyse' : 'Problem-Analyse'} durch und berücksichtigen Sie dabei:
Fokus: ${focus}
${considerations}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen, Aufzählungen oder Markdown-Formatierung. Maximum 150 Wörter.`;
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
},
investigationApproach: (isWorkflow: boolean, userQuery: string) => {
const approachType = isWorkflow ? 'Untersuchungsansatz' : 'Lösungsansatz';
const considerations = isWorkflow ?
`- Triage-Prioritäten nach forensischer Dringlichkeit
- Phasenabfolge nach NIST-Methodik
- Kontaminationsvermeidung und forensische Isolierung` :
`- Methodik-Auswahl nach wissenschaftlichen Kriterien
- Validierung und Verifizierung der gewählten Ansätze
- Integration in bestehende forensische Workflows`;
const focus = isWorkflow ?
'Triage-Prioritäten, Phasenabfolge, Kontaminationsvermeidung' :
'Methodenauswahl, Validierung, Integration';
return `Basierend auf der Analyse entwickeln Sie einen fundierten ${approachType} nach NIST SP 800-86 Methodik.
return `Entwickle einen ${approachType}.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Entwickeln Sie einen systematischen ${approachType} unter Berücksichtigung von:
Fokus: ${focus}
${considerations}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 150 Wörter.`;
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
},
criticalConsiderations: (isWorkflow: boolean, userQuery: string) => {
const considerationType = isWorkflow ? 'kritische forensische Überlegungen' : 'wichtige methodische Voraussetzungen';
const aspects = isWorkflow ?
`- Time-sensitive evidence preservation
- Chain of custody requirements und rechtliche Verwertbarkeit
- Incident containment vs. evidence preservation Dilemma
- Privacy- und Compliance-Anforderungen` :
`- Tool-Validierung und Nachvollziehbarkeit
- False positive/negative Risiken bei der gewählten Methodik
- Qualifikationsanforderungen für die Durchführung
- Dokumentations- und Reporting-Standards`;
const focus = isWorkflow ?
'Beweissicherung vs. Gründlichkeit, Chain of Custody' :
'Tool-Validierung, False Positives/Negatives, Qualifikationen';
return `Identifizieren Sie ${considerationType} für diesen Fall.
return `Identifiziere kritische Überlegungen.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Berücksichtigen Sie folgende forensische Aspekte:
Fokus: ${focus}
${aspects}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.`;
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
},
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
return `Wählen Sie 2-3 Methoden/Tools für die Phase "${phase.name}" und bewerten Sie deren Aufgaben-Eignung VERGLEICHEND.
const methods = phaseTools.filter(t => t.type === 'method');
const tools = phaseTools.filter(t => t.type === 'software');
if (phaseTools.length === 0) {
return `Keine Methoden/Tools für Phase "${phase.name}" verfügbar. Antworte mit leerem Array: []`;
}
return `Du bist ein DFIR-Experte. Wähle die 2-3 BESTEN Items für Phase "${phase.name}".
SZENARIO: "${userQuery}"
SPEZIFISCHE PHASE: ${phase.name} - ${phase.description || 'Forensische Untersuchungsphase'}
PHASE: ${phase.name} - ${phase.description || ''}
VERFÜGBARE TOOLS FÜR ${phase.name.toUpperCase()}:
${phaseTools.map((tool: any, index: number) => `${index + 1}. ${tool.name}: ${tool.description.slice(0, 150)}...
- Plattformen: ${tool.platforms?.join(', ') || 'N/A'}
- Skill Level: ${tool.skillLevel}
- Tags: ${tool.tags?.join(', ') || 'N/A'}`).join('\n\n')}
VERFÜGBARE ITEMS (bereits von KI vorausgewählt):
${methods.length > 0 ? `
METHODEN (${methods.length}):
${methods.map((method: any) =>
`- ${method.name}
Typ: ${method.type}
Beschreibung: ${method.description}
Domains: ${method.domains?.join(', ') || 'N/A'}
Skill Level: ${method.skillLevel}`
).join('\n\n')}
` : 'Keine Methoden verfügbar'}
Bewerten Sie ALLE Tools vergleichend für diese spezifische Aufgabe UND Phase. Wählen Sie die 2-3 besten aus.
${tools.length > 0 ? `
SOFTWARE TOOLS (${tools.length}):
${tools.map((tool: any) =>
`- ${tool.name}
Typ: ${tool.type}
Beschreibung: ${tool.description}
Plattformen: ${tool.platforms?.join(', ') || 'N/A'}
Skill Level: ${tool.skillLevel}`
).join('\n\n')}
` : 'Keine Software-Tools verfügbar'}
BEWERTUNGSKRITERIEN:
- Wie gut löst das Tool das forensische Problem im SZENARIO-Kontext?
- Wie gut passt es zur spezifischen PHASE "${phase.name}"?
- Wie vergleicht es sich mit den anderen verfügbaren Tools für diese Phase?
AUSWAHLREGELN FÜR PHASE "${phase.name}":
1. Wähle die 2-3 BESTEN Items für diese spezifische Phase
2. Priorisiere Items, die DIREKT für "${phase.name}" relevant sind
3. Mindestens 1 Methode wenn verfügbar, Rest Software-Tools
4. Begründe WARUM jedes Item für diese Phase optimal ist
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
WICHTIG: Verwende EXAKT die Namen wie oben aufgelistet (ohne Präfixe wie M1./T2.)!
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB:
[
{
"toolName": "Exakter Tool-Name",
"toolName": "Exakter Name aus der Liste oben",
"taskRelevance": 85,
"justification": "Vergleichende Begründung warum dieses Tool für diese Phase und Aufgabe besser/schlechter als die anderen geeignet ist",
"limitations": ["Spezifische Einschränkung 1", "Einschränkung 2"]
}
]
WICHTIG:
- taskRelevance: 0-100 Score basierend auf Szenario-Eignung UND Phasen-Passung im VERGLEICH zu anderen Tools
- Nur die 2-3 BESTEN Tools auswählen und bewerten
- justification soll VERGLEICHEND sein ("besser als X weil...", "für diese Phase ideal weil...")`;
},
toolEvaluation: (userQuery: string, tool: any, rank: number, taskRelevance: number) => {
return `Sie sind ein DFIR-Experte. Erklären Sie DETAILLIERT die Anwendung dieses bereits bewerteten Tools.
PROBLEM: "${userQuery}"
TOOL: ${tool.name} (bereits bewertet mit ${taskRelevance}% Aufgaben-Eignung)
BESCHREIBUNG: ${tool.description}
Das Tool wurde bereits als Rang ${rank} für diese Aufgabe bewertet. Erklären Sie nun:
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
{
"detailed_explanation": "Detaillierte Erklärung warum und wie dieses Tool für diese spezifische Aufgabe eingesetzt wird",
"implementation_approach": "Konkrete Schritt-für-Schritt Anleitung zur korrekten Anwendung",
"pros": ["Spezifischer Vorteil 1", "Spezifischer Vorteil 2"],
"limitations": ["Spezifische Einschränkung 1", "Spezifische Einschränkung 2"],
"alternatives": "Alternative Ansätze oder Tools falls dieses nicht verfügbar ist"
}
WICHTIG:
- Keine erneute Bewertung - nur detaillierte Erklärung der bereits bewerteten Eignung
- "limitations" soll spezifische technische/methodische Einschränkungen des Tools auflisten
- "pros" soll die Stärken für diese spezifische Aufgabe hervorheben`;
},
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
return `Wählen Sie relevante forensische Konzepte für das Verständnis der empfohlenen Methodik.
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ')}
VERFÜGBARE KONZEPTE:
${availableConcepts.slice(0, 15).map((concept: any) => `- ${concept.name}: ${concept.description.slice(0, 80)}...`).join('\n')}
Wählen Sie 2-4 Konzepte aus, die für das Verständnis der forensischen Methodik essentiell sind.
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
[
{
"conceptName": "Exakter Konzept-Name",
"relevance": "Forensische Relevanz: Warum dieses Konzept für das Verständnis der Methodik kritisch ist"
"justification": "Detaillierte Begründung (60-80 Wörter) warum optimal für ${phase.name} - erkläre Anwendung, Vorteile und spezifische Relevanz",
"limitations": ["Mögliche Einschränkung für diese Phase"]
}
]`;
},
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
const prompt = isWorkflow ?
`Erstellen Sie eine Workflow-Empfehlung basierend auf DFIR-Prinzipien.
toolEvaluation: (userQuery: string, tool: any, rank: number, taskRelevance: number) => {
const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
SZENARIO: "${userQuery}"
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ') || 'Keine Tools ausgewählt'}
Erstellen Sie konkrete methodische Workflow-Schritte für dieses spezifische Szenario unter Berücksichtigung forensischer Best Practices, Objektivität und rechtlicher Verwertbarkeit.
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.` :
`Erstellen Sie wichtige methodische Überlegungen für die korrekte Methoden-/Tool-Anwendung.
return `Erkläre die Anwendung dieser/dieses ${itemType}.
PROBLEM: "${userQuery}"
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ') || 'Keine Methoden/Tools ausgewählt'}
${itemType.toUpperCase()}: ${tool.name} (${taskRelevance}% Eignung)
TYP: ${tool.type}
Geben Sie kritische methodische Überlegungen, Validierungsanforderungen und Qualitätssicherungsmaßnahmen für die korrekte Anwendung der empfohlenen Methoden/Tools.
Bereits als Rang ${rank} bewertet.
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 100 Wörter.`;
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB DER JSON-STRUKTUR:
{
"detailed_explanation": "Warum und wie einsetzen",
"implementation_approach": "Konkrete Schritte",
"pros": ["Vorteil 1", "Vorteil 2"],
"limitations": ["Einschränkung 1"],
"alternatives": "Alternative Ansätze"
}`;
},
return prompt;
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
return `Wähle 2-4 relevante Konzepte.
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ')}
VERFÜGBARE KONZEPTE (${availableConcepts.length} KI-kuratiert):
${availableConcepts.map((c: any) =>
`- ${c.name}: ${c.description}...`
).join('\n')}
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB DER JSON-STRUKTUR:
[
{
"conceptName": "Name",
"relevance": "Warum kritisch für Methodik"
}
]`;
},
phaseCompletionReasoning: (
originalQuery: string,
phase: any,
selectedToolName: string,
tool: any,
completionContext: string
) => {
return `Du bist ein DFIR-Experte. Erkläre warum dieses Tool nachträglich zur Vervollständigung hinzugefügt wurde.
KONTEXT DER NACHTRÄGLICHEN ERGÄNZUNG:
- Ursprüngliche KI-Auswahl war zu spezifisch/eng gefasst
- Phase "${phase.name}" war unterrepräsentiert in der initialen Auswahl
- Semantische Suche fand zusätzlich relevante Tools für diese Phase
- Tool wird nachträglich hinzugefügt um Vollständigkeit zu gewährleisten
URSPRÜNGLICHE ANFRAGE: "${originalQuery}"
PHASE ZU VERVOLLSTÄNDIGEN: ${phase.name} - ${phase.description || ''}
HINZUGEFÜGTES TOOL: ${selectedToolName} (${tool.type})
TOOL-BESCHREIBUNG: ${tool.description}
BEGRÜNDUNGSKONTEXT: ${completionContext}
Erstelle eine präzise Begründung (max. 40 Wörter), die erklärt:
1. WARUM dieses Tool nachträglich hinzugefügt wurde
2. WIE es die ${phase.name}-Phase ergänzt
3. DASS es die ursprünglich zu spezifische Auswahl erweitert
Antwort: Prägnanter Fließtext, knappe Begründung für Nachergänzung. Vermeide Begriffe wie "Das Tool" und gib keinen einleitenden Text wie "Begründung (40 Wörter):" an.`;
},
generatePhaseCompletionPrompt(
originalQuery: string,
phase: any,
candidateTools: any[],
candidateConcepts: any[]
): string {
return `Du bist ein DFIR-Experte. Die initiale KI-Auswahl war zu spezifisch - die Phase "${phase.name}" ist unterrepräsentiert.
KONTEXT: Die Hauptauswahl hat zu wenige Tools für "${phase.name}" identifiziert. Wähle jetzt ergänzende Tools aus semantischer Nachsuche.
ORIGINAL ANFRAGE: "${originalQuery}"
UNTERREPRÄSENTIERTE PHASE: ${phase.name} - ${phase.description || ''}
SEMANTISCH GEFUNDENE KANDIDATEN für Nachergänzung:
VERFÜGBARE TOOLS (${candidateTools.length}):
${candidateTools.map((tool: any) => `
- ${tool.name} (${tool.type})
Beschreibung: ${tool.description}
Skill Level: ${tool.skillLevel}
`).join('')}
${candidateConcepts.length > 0 ? `
VERFÜGBARE KONZEPTE (${candidateConcepts.length}):
${candidateConcepts.map((concept: any) => `
- ${concept.name}
Beschreibung: ${concept.description}
`).join('')}
` : ''}
AUSWAHLREGELN FÜR NACHERGÄNZUNG:
1. Wähle 1-2 BESTE Methoden/Tools die die ${phase.name}-Phase optimal ergänzen
2. Methoden/Tools müssen für die ursprüngliche Anfrage relevant sein
3. Ergänzen, nicht ersetzen - erweitere die zu spezifische Erstauswahl
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT:
{
"selectedTools": ["ToolName1", "ToolName2"],
"selectedConcepts": ["ConceptName1"],
"completionReasoning": "Kurze Erklärung warum diese Nachergänzung für ${phase.name} notwendig war"
}`;
},
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
const focus = isWorkflow ?
'Workflow-Schritte, Best Practices, Objektivität' :
'Methodische Überlegungen, Validierung, Qualitätssicherung';
return `Erstelle ${isWorkflow ? 'Workflow-Empfehlung' : 'methodische Überlegungen'}.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
AUSGEWÄHLT: ${selectedToolNames.join(', ')}${selectedToolNames.length > 5 ? '...' : ''}
Fokus: ${focus}
Antwort: Fließtext ohne Listen, max ${isWorkflow ? '100' : '80'} Wörter.`;
}
} as const;
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number): 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: 'investigationApproach', isWorkflow: boolean, userQuery: string): string;
export function getPrompt(key: 'criticalConsiderations', isWorkflow: boolean, userQuery: string): string;
export function getPrompt(key: 'phaseToolSelection', userQuery: string, phase: any, phaseTools: any[]): string;
export function getPrompt(key: 'toolEvaluation', userQuery: string, tool: any, rank: number, taskRelevance: number): string;
export function getPrompt(key: 'backgroundKnowledgeSelection', userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]): string;
export function getPrompt(key: 'phaseCompletionReasoning', originalQuery: string, phase: any, selectedToolName: string, tool: any, completionContext: 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(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
try {
const promptFunction = AI_PROMPTS[promptKey];

View File

@ -0,0 +1,616 @@
---
title: "Digital Evidence Chain of Custody: Lückenlose Beweisführung in der digitalen Forensik"
description: "Umfassender Leitfaden für die rechtssichere Dokumentation digitaler Beweise von der Sicherstellung bis zur Gerichtsverhandlung. Praktische Umsetzung von ISO 27037, Dokumentationsstandards und häufige Fallstricke."
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: advanced
categories: ["standards", "documentation", "legal-compliance", "case-management"]
tags: ["chain-of-custody", "iso-27037", "court-admissible", "audit-trail", "hash-verification", "tamper-evidence", "legal-compliance", "documentation", "process-management", "evidence-handling"]
published: true
---
# Digital Evidence Chain of Custody: Lückenlose Beweisführung in der digitalen Forensik
Die **Chain of Custody** (Beweiskette) ist das Rückgrat jeder forensischen Untersuchung und entscheidet oft über Erfolg oder Misserfolg vor Gericht. Dieser Leitfaden erklärt die rechtssicheren Verfahren für die lückenlose Dokumentation digitaler Beweise von der Sicherstellung bis zur Gerichtsverhandlung.
## Warum ist die Chain of Custody entscheidend?
In der digitalen Forensik können Beweise innerhalb von Sekunden manipuliert, gelöscht oder verfälscht werden. Eine ordnungsgemäße Chain of Custody gewährleistet:
- **Gerichtliche Verwertbarkeit** der Beweise
- **Nachweis der Authentizität** und Integrität
- **Schutz vor Manipulationsvorwürfen**
- **Rechtssicherheit** für alle Beteiligten
- **Compliance** mit internationalen Standards
> **Warnung**: Bereits kleine Fehler in der Beweiskette können zur kompletten Verwerfung der Beweise führen und jahrelange Ermittlungsarbeit zunichte machen.
## Rechtliche Grundlagen und Standards
### Internationale Standards
**ISO/IEC 27037:2012** - "Guidelines for identification, collection, acquisition and preservation of digital evidence"
- Definiert Best Practices für digitale Beweismittel
- International anerkannter Standard
- Basis für nationale Implementierungen
**ISO/IEC 27041:2015** - "Guidance on assuring suitability and adequacy of incident investigative method"
- Ergänzt ISO 27037 um Qualitätssicherung
- Fokus auf Angemessenheit der Methoden
### Nationale Rahmenwerke
**Deutschland**:
- § 81a StPO (Körperliche Untersuchung)
- § 94 ff. StPO (Beschlagnahme)
- BSI-Standards zur IT-Forensik
**USA**:
- Federal Rules of Evidence (Rule 901, 902)
- NIST Special Publication 800-86
**EU**:
- GDPR-Compliance bei der Beweissicherung
- eIDAS-Verordnung für digitale Signaturen
## Die vier Säulen der Chain of Custody
### 1. Authentizität (Echtheit)
**Definition**: Nachweis, dass die Beweise tatsächlich von der behaupteten Quelle stammen.
**Praktische Umsetzung**:
```bash
# Cryptographic Hash Generation
sha256sum /dev/sdb1 > evidence_hash.txt
md5sum /dev/sdb1 >> evidence_hash.txt
# Mit Zeitstempel
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ): $(sha256sum /dev/sdb1)" >> chain_log.txt
```
### 2. Integrität (Unversehrtheit)
**Definition**: Sicherstellung, dass die Beweise seit der Sicherstellung unverändert geblieben sind.
**Maßnahmen**:
- **Write-Blocker** bei allen Zugriffen
- **Hash-Verifizierung** vor und nach jeder Bearbeitung
- **Versionskontrolle** für alle Arbeitskopien
### 3. Nachvollziehbarkeit (Traceability)
**Definition**: Lückenlose Dokumentation aller Personen, die Zugang zu den Beweisen hatten.
**Dokumentationspflicht**: Wer, Was, Wann, Wo, Warum
### 4. Nicht-Abstreitbarkeit (Non-Repudiation)
**Definition**: Verhinderung, dass Beteiligte ihre Handlungen später abstreiten können.
**Technische Lösung**: Digitale Signaturen, Blockchain-Timestamping
## Praktische Implementierung: Schritt-für-Schritt
### Phase 1: Vorbereitung der Sicherstellung
**Equipment-Check**:
```checklist
□ Kalibrierte Write-Blocker
□ Forensische Imaging-Tools
□ Chain of Custody Formulare
□ Tamper-evident Bags/Labels
□ Digitalkamera für Dokumentation
□ Messgeräte (falls erforderlich)
□ Backup-Ausrüstung
```
**Dokumentation vor Ort**:
1. **Umgebungsfotografie** (360°-Dokumentation)
2. **Hardware-Identifikation** (Seriennummern, Labels)
3. **Netzwerkzustand** (aktive Verbindungen)
4. **Bildschirmzustand** (Screenshots vor Herunterfahren)
### Phase 2: Sichere Akquisition
**Write-Blocker Setup**:
```bash
# Hardware Write-Blocker Verification
lsblk -o NAME,SIZE,RO,TYPE,MOUNTPOINT
# RO sollte "1" anzeigen für geschützte Devices
# Software Write-Blocker (Linux)
blockdev --setro /dev/sdb
blockdev --getro /dev/sdb # Should return 1
```
**Imaging mit Integrity Check**:
```bash
# dd mit Hash-Berechnung
dd if=/dev/sdb | tee >(sha256sum > image.sha256) | dd of=evidence.dd
# Oder mit dcfldd für bessere Forensik-Features
dcfldd if=/dev/sdb of=evidence.dd hash=sha256,md5 hashlog=hashlog.txt bs=4096
```
### Phase 3: Dokumentation und Versiegelung
**Chain of Custody Form - Kernelemente**:
```
DIGITAL EVIDENCE CUSTODY FORM
Fall-ID: _______________ Datum: _______________
Ermittler: _______________ Badge/ID: _______________
BEWEISMITTEL DETAILS:
- Beschreibung: ________________________________
- Seriennummer: _______________________________
- Hersteller/Modell: ___________________________
- Kapazität: __________________________________
- Hash-Werte:
* SHA256: ___________________________________
* MD5: _____________________________________
CUSTODY CHAIN:
[Datum/Zeit] [Übernommen von] [Übergeben an] [Zweck] [Unterschrift]
_________________________________________________________________
_________________________________________________________________
INTEGRITÄT BESTÄTIGT:
□ Write-Blocker verwendet
□ Hash-Werte verifiziert
□ Tamper-evident versiegelt
□ Fotos dokumentiert
```
**Versiegelung**:
```
Tamper-Evident Label Nummer: ______________
Siegeltyp: _______________________________
Platzierung: _____________________________
Foto-Referenz: ___________________________
```
### Phase 4: Transport und Lagerung
**Sichere Aufbewahrung**:
- **Klimakontrollierte Umgebung** (15-25°C, <60% Luftfeuchtigkeit)
- **Elektromagnetische Abschirmung** (Faraday-Käfig)
- **Zugangskontrolle** (Biometrie, Kartenleser)
- **Überwachung** (24/7 Video, Alarme)
**Transport-Protokoll**:
```
TRANSPORT LOG
Von: ______________________ Nach: ______________________
Datum/Zeit Start: _____________ Ankunft: _______________
Transportmittel: ___________________________________
Begleitpersonen: ___________________________________
Spezielle Vorkehrungen: ____________________________
Integrität bei Ankunft:
□ Siegel unversehrt
□ Hash-Werte überprüft
□ Keine physischen Schäden
□ Dokumentation vollständig
Empfänger: _________________ Unterschrift: _____________
```
## Digitale Chain of Custody Tools
### Laboratory Information Management Systems (LIMS)
**Kommerzielle Lösungen**:
- **FRED (Forensic Recovery of Evidence Device)**
- **CaseGuard** von AccessData
- **EnCase Legal** von OpenText
**Open Source Alternativen**:
```python
# Beispiel: Python-basierte CoC Tracking
import hashlib
import datetime
import json
from cryptography.fernet import Fernet
class ChainOfCustody:
def __init__(self):
self.evidence_log = []
self.key = Fernet.generate_key()
self.cipher = Fernet(self.key)
def add_custody_event(self, evidence_id, handler, action, location):
event = {
'timestamp': datetime.datetime.utcnow().isoformat(),
'evidence_id': evidence_id,
'handler': handler,
'action': action,
'location': location,
'hash': self.calculate_hash(evidence_id)
}
# Encrypt sensitive data
encrypted_event = self.cipher.encrypt(json.dumps(event).encode())
self.evidence_log.append(encrypted_event)
return event
def calculate_hash(self, evidence_path):
"""Calculate SHA256 hash of evidence file"""
hash_sha256 = hashlib.sha256()
with open(evidence_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
```
### Blockchain-basierte Lösungen
**Unveränderliche Timestamps**:
```solidity
// Ethereum Smart Contract Beispiel
pragma solidity ^0.8.0;
contract EvidenceChain {
struct CustodyEvent {
uint256 timestamp;
string evidenceId;
string handler;
string action;
string hashValue;
}
mapping(string => CustodyEvent[]) public evidenceChain;
event CustodyTransfer(
string indexed evidenceId,
string handler,
uint256 timestamp
);
function addCustodyEvent(
string memory _evidenceId,
string memory _handler,
string memory _action,
string memory _hashValue
) public {
evidenceChain[_evidenceId].push(CustodyEvent({
timestamp: block.timestamp,
evidenceId: _evidenceId,
handler: _handler,
action: _action,
hashValue: _hashValue
}));
emit CustodyTransfer(_evidenceId, _handler, block.timestamp);
}
}
```
## Häufige Fehler und Fallstricke
### Kritische Dokumentationsfehler
**1. Unvollständige Handler-Information**
```
❌ Falsch: "IT-Abteilung"
✅ Richtig: "Max Mustermann, IT-Administrator, Badge #12345, Abteilung IT-Security"
```
**2. Unspezifische Aktionsbeschreibungen**
```
❌ Falsch: "Analyse durchgeführt"
✅ Richtig: "Keyword-Suche nach 'vertraulich' mit EnCase v21.2,
Read-Only Zugriff, Image Hash vor/nach verifiziert"
```
**3. Lückenhafte Zeiterfassung**
```
❌ Falsch: "15:30"
✅ Richtig: "2024-01-15T15:30:27Z (UTC), Zeitzone CET+1"
```
### Technische Fallstricke
**Hash-Algorithmus Schwächen**:
```bash
# Vermeide MD5 für neue Fälle (Kollisionsanfällig)
❌ md5sum evidence.dd
# Verwende stärkere Algorithmen
✅ sha256sum evidence.dd
✅ sha3-256sum evidence.dd # Noch sicherer
```
**Write-Blocker Bypass**:
```bash
# Prüfe IMMER Write-Protection
blockdev --getro /dev/sdb
if [ $? -eq 0 ]; then
echo "Write protection AKTIV"
else
echo "WARNUNG: Write protection NICHT aktiv!"
exit 1
fi
```
### Rechtliche Fallstricke
**GDPR-Compliance bei EU-Fällen**:
- **Datenschutz-Folgenabschätzung** vor Imaging
- **Zweckbindung** der Beweiserhebung
- **Löschfristen** nach Verfahrensabschluss
**Jurisdiktionsprobleme**:
- **Cloud-Evidence** in verschiedenen Ländern
- **Verschiedene Beweisstandards** (Common Law vs. Civil Law)
- **Internationale Rechtshilfe** erforderlich
## Qualitätssicherung und Audit
### Peer Review Verfahren
**4-Augen-Prinzip**:
```
Imaging-Protokoll:
Techniker A: _________________ (Durchführung)
Techniker B: _________________ (Verifikation)
Supervisor: __________________ (Freigabe)
```
**Hash-Verifikation Zeitplan**:
```
Initial: SHA256 bei Akquisition
Transport: Hash-Check vor/nach Transport
Labor: Hash-Check bei Laborankunft
Analyse: Hash-Check vor jeder Analyse
Archiv: Hash-Check bei Archivierung
Vernichtung: Final Hash-Check vor Vernichtung
```
### Continuous Monitoring
**Automated Integrity Checks**:
```bash
#!/bin/bash
# integrity_monitor.sh
EVIDENCE_DIR="/secure/evidence"
LOG_FILE="/var/log/evidence_integrity.log"
for evidence_file in "$EVIDENCE_DIR"/*.dd; do
stored_hash=$(cat "${evidence_file}.sha256")
current_hash=$(sha256sum "$evidence_file" | cut -d' ' -f1)
if [ "$stored_hash" != "$current_hash" ]; then
echo "ALERT: Integrity violation detected for $evidence_file" | \
tee -a "$LOG_FILE"
# Send immediate alert
mail -s "Evidence Integrity Alert" admin@forensics.org < \
"$LOG_FILE"
fi
done
```
## Internationale Gerichtspraxis
### Deutschland - BGH Rechtsprechung
**BGH 1 StR 142/18** (2018):
- Digitale Beweise müssen **nachvollziehbar erhoben** werden
- **Hash-Werte allein** reichen nicht aus
- **Gesamter Erhebungsprozess** muss dokumentiert sein
### USA - Federal Courts
**United States v. Tank (2018)**:
- **Authentication** unter Federal Rule 901(b)(9)
- **Best Practices** sind nicht immer **rechtlich erforderlich**
- **Totality of circumstances** entscheidet
### EU - EuGH Rechtsprechung
**Rechtssache C-203/15** (2016):
- **Grundrechte** vs. **Strafverfolgung**
- **Verhältnismäßigkeit** der Beweiserhebung
- **GDPR-Compliance** auch bei strafrechtlichen Ermittlungen
## Fallstudien aus der Praxis
### Case Study 1: Ransomware-Angriff Automobilhersteller
**Szenario**:
Ransomware-Angriff auf Produktionssysteme, 50+ Systeme betroffen
**CoC-Herausforderungen**:
- **Zeitdruck** durch Produktionsstillstand
- **Verschiedene Standorte** (Deutschland, Tschechien, Mexiko)
- **Rechtliche Anforderungen** in 3 Jurisdiktionen
**Lösung**:
```
Parallel Teams:
- Team 1: Incident Response (Live-Analyse)
- Team 2: Evidence Preservation (Imaging)
- Team 3: Documentation (CoC-Protokoll)
Zentrale Koordination:
- Shared CoC-Database (Cloud-basiert)
- Video-Calls für Custody-Transfers
- Digital Signatures für Remote-Bestätigung
```
**Lessons Learned**:
- **Vorab-Planung** für Multi-Jurisdiktion essentiell
- **Remote-CoC-Verfahren** erforderlich
- **24/7-Verfügbarkeit** der Dokumentationssysteme
### Case Study 2: Betrugsermittlung Finanzdienstleister
**Szenario**:
Verdacht auf Insiderhandel, E-Mail-Analyse von 500+ Mitarbeitern
**CoC-Komplexität**:
- **Privacy Laws** (GDPR, Bankengeheimnis)
- **Privileged Communications** (Anwalt-Mandant)
- **Regulatory Oversight** (BaFin, SEC)
**Chain of Custody Strategie**:
```
Segregated Processing:
1. Initial Triage (Automated)
2. Legal Review (Attorney-Client Privilege)
3. Regulatory Notification (Compliance)
4. Technical Analysis (Forensik-Team)
Access Controls:
- Role-based Evidence Access
- Need-to-know Principle
- Audit Log for every Access
```
## Technologie-Trends und Zukunftsausblick
### KI-basierte CoC-Automatisierung
**Machine Learning für Anomalie-Erkennung**:
```python
from sklearn.ensemble import IsolationForest
import pandas as pd
# CoC Event Anomaly Detection
def detect_custody_anomalies(custody_events):
"""
Detect unusual patterns in custody transfers
"""
features = pd.DataFrame(custody_events)
# Feature Engineering
features['time_delta'] = features['timestamp'].diff()
features['handler_changes'] = features['handler'].ne(features['handler'].shift())
# Anomaly Detection
model = IsolationForest(contamination=0.1)
anomalies = model.fit_predict(features.select_dtypes(include=[np.number]))
return features[anomalies == -1]
```
### Quantum-Safe Cryptography
**Vorbereitung auf Post-Quantum Era**:
```
Current: RSA-2048, SHA-256
Transitional: RSA-4096, SHA-3
Future: Lattice-based, Hash-based Signatures
```
### Cloud-Native Evidence Management
**Container-basierte Forensik-Pipelines**:
```yaml
# docker-compose.yml für Forensik-Lab
version: '3.8'
services:
evidence-intake:
image: forensics/evidence-intake:v2.1
volumes:
- ./evidence:/data
environment:
- AUTO_HASH=true
- BLOCKCHAIN_LOGGING=true
chain-tracker:
image: forensics/chain-tracker:v1.5
depends_on:
- postgres
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/custody
```
## Best Practices Zusammenfassung
### Präventive Maßnahmen
**1. Standardisierte Verfahren**
```
□ SOPs für alle Custody-Schritte
□ Regelmäßige Team-Schulungen
□ Tool-Kalibrierung und -Wartung
□ Backup-Verfahren für Ausfälle
```
**2. Technische Safeguards**
```
□ Redundante Hash-Algorithmen
□ Automated Integrity Monitoring
□ Secure Transport Protocols
□ Environmental Monitoring
```
**3. Rechtliche Compliance**
```
□ Jurisdiction-spezifische SOPs
□ Regular Legal Updates
□ Attorney Consultation Process
□ International Cooperation Agreements
```
### Reaktive Maßnahmen
**Incident Response bei CoC-Verletzungen**:
```
1. Immediate Containment
- Stop all evidence processing
- Secure affected items
- Document incident details
2. Impact Assessment
- Determine scope of compromise
- Identify affected cases
- Assess legal implications
3. Remediation
- Re-establish chain where possible
- Alternative evidence strategies
- Legal notification requirements
4. Prevention
- Root cause analysis
- Process improvements
- Additional controls
```
## Fazit
Die Chain of Custody ist mehr als eine administrative Pflicht - sie ist das **Fundament der digitalen Forensik**. Ohne ordnungsgemäße Beweiskette können selbst die stärksten technischen Beweise vor Gericht wertlos werden.
**Schlüsselprinzipien für den Erfolg**:
1. **Vorbereitung ist alles** - SOPs und Tools vor dem Incident
2. **Dokumentation über alles** - Im Zweifel mehr dokumentieren
3. **Technologie als Enabler** - Automatisierung wo möglich
4. **Menschen im Fokus** - Training und Awareness entscheidend
5. **Kontinuierliche Verbesserung** - Lessons Learned Integration
Die Investition in robuste Chain of Custody Verfahren zahlt sich langfristig aus - durch höhere Erfolgsraten vor Gericht, reduzierte Compliance-Risiken und erhöhte Glaubwürdigkeit der forensischen Arbeit.
> **Merksatz**: "Eine Kette ist nur so stark wie ihr schwächstes Glied - in der digitalen Forensik ist das oft die menschliche Komponente, nicht die technische."
## Weiterführende Ressourcen
**Standards und Guidelines**:
- [ISO/IEC 27037:2012](https://www.iso.org/standard/44381.html) - Digital Evidence Guidelines
- [NIST SP 800-86](https://csrc.nist.gov/publications/detail/sp/800-86/final) - Computer Forensics Guide
- [RFC 3227](https://tools.ietf.org/html/rfc3227) - Evidence Collection Guidelines
**Training und Zertifizierung**:
- SANS FOR500 (Windows Forensic Analysis)
- SANS FOR508 (Advanced Incident Response)
- IACIS Certified Forensic Computer Examiner (CFCE)
- CISSP (Chain of Custody Domain)
**Tools und Software**:
- [FTK Imager](https://www.exterro.com/digital-forensics-software/ftk-imager) - Free Imaging Tool
- [Autopsy](https://www.sleuthkit.org/autopsy/) - Open Source Platform
- [MSAB XRY](https://www.msab.com/) - Mobile Forensics
- [Cellebrite UFED](https://www.cellebrite.com/) - Mobile Evidence Extraction

View File

@ -0,0 +1,471 @@
---
title: "Dateisystem-Forensik: Von NTFS-Strukturen bis Cloud-Storage-Artefakten"
description: "Umfassender Leitfaden zur forensischen Analyse von Dateisystemen - NTFS-Metadaten, ext4-Journaling, APFS-Snapshots und Cloud-Storage-Forensik für professionelle Datenrekonstruktion"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: intermediate
categories: ["analysis", "configuration", "troubleshooting"]
tags: ["filesystem-analysis", "metadata-extraction", "deleted-data-recovery", "slack-space", "journaling-analysis", "timestamp-forensics", "partition-analysis", "cloud-storage", "ntfs", "ext4", "apfs", "data-carving"]
tool_name: "File Systems & Storage Forensics"
related_tools: ["Autopsy", "The Sleuth Kit", "FTK Imager", "Volatility", "X-Ways Forensics"]
published: true
---
# Dateisystem-Forensik: Von NTFS-Strukturen bis Cloud-Storage-Artefakten
Die forensische Analyse von Dateisystemen bildet das Fundament moderner Digital Forensics. Dieser umfassende Leitfaden behandelt die kritischen Aspekte der Dateisystem-Forensik von traditionellen lokalen Speichermedien bis hin zu modernen Cloud-Storage-Umgebungen.
## Grundlagen der Dateisystem-Forensik
### Was ist Dateisystem-Forensik?
Dateisystem-Forensik umfasst die systematische Untersuchung von Speicherstrukturen zur Rekonstruktion digitaler Beweise. Dabei werden nicht nur sichtbare Dateien analysiert, sondern auch Metadaten, gelöschte Inhalte und versteckte Artefakte untersucht.
### Zentrale forensische Konzepte
**Metadaten-Analyse**: Jedes Dateisystem speichert umfangreiche Metadaten über Dateien, Verzeichnisse und Systemaktivitäten. Diese Informationen sind oft aussagekräftiger als der eigentliche Dateiinhalt.
**Slack Space**: Der ungenutzte Bereich zwischen dem Ende einer Datei und dem Ende des zugewiesenen Clusters kann Reste vorheriger Dateien enthalten.
**Journaling**: Moderne Dateisysteme protokollieren Änderungen in Journal-Dateien, die wertvolle Timeline-Informationen liefern.
**Timeline-Rekonstruktion**: Durch Kombination verschiedener Timestamp-Quellen lassen sich detaillierte Aktivitätszeitlinien erstellen.
## NTFS-Forensik: Das Windows-Dateisystem im Detail
### Master File Table (MFT) Analyse
Die MFT ist das Herzstück von NTFS und enthält Einträge für jede Datei und jeden Ordner auf dem Volume.
**Struktur eines MFT-Eintrags:**
```
Offset 0x00: FILE-Signatur
Offset 0x04: Update Sequence Array Offset
Offset 0x06: Update Sequence Array Größe
Offset 0x08: $LogFile Sequence Number (LSN)
Offset 0x10: Sequence Number
Offset 0x12: Hard Link Count
Offset 0x14: Erste Attribut-Offset
```
**Forensisch relevante Attribute:**
- `$STANDARD_INFORMATION`: Timestamps, Dateiberechtigungen
- `$FILE_NAME`: Dateiname, zusätzliche Timestamps
- `$DATA`: Dateiinhalt oder Cluster-Referenzen
- `$SECURITY_DESCRIPTOR`: Zugriffsberechtigungen
**Praktische Analyse-Techniken:**
1. **Gelöschte MFT-Einträge identifizieren**: Einträge mit FILE0-Signatur sind oft gelöschte Dateien
2. **Timeline-Anomalien erkennen**: Vergleich zwischen $STANDARD_INFORMATION und $FILE_NAME Timestamps
3. **Resident vs. Non-Resident Data**: Kleine Dateien (< 700 Bytes) werden direkt in der MFT gespeichert
### $LogFile Analyse für Aktivitäts-Tracking
Das NTFS-Journal protokolliert alle Dateisystem-Änderungen und ermöglicht detaillierte Aktivitäts-Rekonstruktion.
**Relevante Log-Record-Typen:**
- `CreateFile`: Datei-/Ordnererstellung
- `DeleteFile`: Löschvorgänge
- `RenameFile`: Umbenennungen
- `SetInformationFile`: Metadaten-Änderungen
**Analyse-Workflow:**
```bash
# Mit istat (Sleuth Kit) MFT-Eintrag analysieren
istat /dev/sda1 5 # MFT-Eintrag 5 anzeigen
# Mit fls gelöschte Dateien auflisten
fls -r -d /dev/sda1
# Mit tsk_recover gelöschte Dateien wiederherstellen
tsk_recover /dev/sda1 /recovery/
```
### Alternate Data Streams (ADS) Detection
ADS können zur Datenverbergung missbraucht werden und sind oft übersehen.
**Erkennungsstrategien:**
1. **MFT-Analyse auf mehrere $DATA-Attribute**: Dateien mit ADS haben multiple $DATA-Einträge
2. **Powershell-Erkennung**: `Get-Item -Path C:\file.txt -Stream *`
3. **Forensik-Tools**: Autopsy zeigt ADS automatisch in der File-Analyse
### Volume Shadow Copies für Timeline-Rekonstruktion
VSCs bieten Snapshots des Dateisystems zu verschiedenen Zeitpunkten.
**Forensische Relevanz:**
- Wiederherstellung gelöschter/überschriebener Dateien
- Timeline-Rekonstruktion über längere Zeiträume
- Registry-Hive-Vergleiche zwischen Snapshots
**Zugriff auf VSCs:**
```cmd
# VSCs auflisten
vssadmin list shadows
# VSC mounten
vshadow -p C: -script=shadow.cmd
```
## ext4-Forensik: Linux-Dateisystem-Analyse
### Ext4-Journal-Analyse
Das ext4-Journal (`/journal`) protokolliert Transaktionen und bietet wertvolle forensische Artefakte.
**Journal-Struktur:**
- **Descriptor Blocks**: Beschreiben bevorstehende Transaktionen
- **Data Blocks**: Enthalten die eigentlichen Datenänderungen
- **Commit Blocks**: Markieren abgeschlossene Transaktionen
- **Revoke Blocks**: Listen widerrufene Blöcke auf
**Praktische Analyse:**
```bash
# Journal-Informationen anzeigen
tune2fs -l /dev/sda1 | grep -i journal
# Mit debugfs Journal untersuchen
debugfs /dev/sda1
debugfs: logdump -a journal_file
# Ext4-Metadaten extrahieren
icat /dev/sda1 8 > journal.raw # Inode 8 ist typisch das Journal
```
### Inode-Struktur und Deleted-File-Recovery
**Ext4-Inode-Aufbau:**
```
struct ext4_inode {
__le16 i_mode; # Dateityp und Berechtigungen
__le16 i_uid; # Benutzer-ID
__le32 i_size; # Dateigröße
__le32 i_atime; # Letzter Zugriff
__le32 i_ctime; # Inode-Änderung
__le32 i_mtime; # Letzte Modifikation
__le32 i_dtime; # Löschzeitpunkt
...
__le32 i_block[EXT4_N_BLOCKS]; # Block-Pointer
};
```
**Recovery-Techniken:**
1. **Inode-Scanning**: Suche nach Inodes mit gesetztem dtime aber erhaltenen Blöcken
2. **Journal-Recovery**: Replay von Journal-Einträgen vor Löschzeitpunkt
3. **Directory-Entry-Recovery**: Undelfs-Techniken für kürzlich gelöschte Dateien
### Extended Attributes (xattr) Forensik
Extended Attributes speichern zusätzliche Metadaten und Sicherheitskontext.
**Forensisch relevante xattrs:**
- `security.selinux`: SELinux-Kontext
- `user.*`: Benutzerdefinierte Attribute
- `system.posix_acl_*`: ACL-Informationen
- `security.capability`: File-Capabilities
```bash
# Alle xattrs einer Datei anzeigen
getfattr -d /path/to/file
# Spezifisches Attribut extrahieren
getfattr -n user.comment /path/to/file
```
## APFS und HFS+ Forensik: macOS-Dateisysteme
### APFS-Snapshots für Point-in-Time-Analysis
APFS erstellt automatisch Snapshots, die forensische Goldgruben darstellen.
**Snapshot-Management:**
```bash
# Snapshots auflisten
tmutil listlocalsnapshots /
# Snapshot mounten
diskutil apfs mount -snapshot snapshot_name
# Snapshot-Metadaten analysieren
diskutil apfs list
```
**Forensische Anwendung:**
- Vergleich von Dateisystem-Zuständen über Zeit
- Recovery von gelöschten/modifizierten Dateien
- Malware-Persistenz-Analyse
### HFS+-Katalog-Datei-Forensik
Die Katalog-Datei ist das Äquivalent zur NTFS-MFT in HFS+.
**Struktur:**
- **Header Node**: Baum-Metadaten
- **Index Nodes**: Verweise auf Leaf Nodes
- **Leaf Nodes**: Eigentliche Datei-/Ordner-Records
- **Map Nodes**: Freie/belegte Nodes
**Forensische Techniken:**
```bash
# Mit hfsdump Katalog analysieren
hfsdump -c /dev/disk1s1
# Gelöschte Dateien suchen
fls -r -f hfsplus /dev/disk1s1
```
## Cloud Storage Forensics
### OneDrive-Artefakt-Analyse
**Lokale Artefakte:**
- `%USERPROFILE%\OneDrive\*`: Synchronisierte Dateien
- Registry: `HKCU\Software\Microsoft\OneDrive`
- Event Logs: OneDrive-spezifische Ereignisse
**Forensische Analyse-Punkte:**
1. **Sync-Status**: Welche Dateien wurden synchronisiert?
2. **Conflict-Resolution**: Wie wurden Konflikte gelöst?
3. **Version-History**: Zugriff auf vorherige Datei-Versionen
4. **Sharing-Activities**: Geteilte Dateien und Berechtigungen
```powershell
# OneDrive-Status abfragen
Get-ItemProperty -Path "HKCU:\Software\Microsoft\OneDrive\Accounts\*"
# Sync-Engine-Logs analysieren
Get-WinEvent -LogName "Microsoft-Windows-OneDrive/Operational"
```
### Google Drive Forensik
**Client-seitige Artefakte:**
- `%LOCALAPPDATA%\Google\Drive\*`: Lokaler Cache
- SQLite-Datenbanken: Sync-Metadaten
- Temporary Files: Unvollständige Downloads
**Wichtige Datenbanken:**
- `sync_config.db`: Sync-Konfiguration
- `cloud_graph.db`: Cloud-Dateienstruktur
- `metadata_database`: Datei-Metadaten
```bash
# SQLite-Datenbank analysieren
sqlite3 sync_config.db
.tables
SELECT * FROM data WHERE key LIKE '%sync%';
```
### Dropbox-Forensik
**Forensische Artefakte:**
- `%APPDATA%\Dropbox\*`: Konfiguration und Logs
- `.dropbox.cache\*`: Lokaler Cache
- Database-Dateien: Sync-Historie
**Wichtige Dateien:**
- `config.dbx`: Verschlüsselte Konfiguration
- `filecache.dbx`: Datei-Cache-Informationen
- `deleted.dbx`: Gelöschte Dateien-Tracking
## File Carving und Datenrekonstruktion
### Header/Footer-basiertes Carving
**Klassische Ansätze:**
```bash
# Mit foremost File-Carving durchführen
foremost -t jpg,pdf,doc -i /dev/sda1 -o /recovery/
# Mit scalpel erweiterte Pattern verwenden
scalpel -b -o /recovery/ /dev/sda1
# Mit photorec interaktives Recovery
photorec /dev/sda1
```
**Custom Carving-Patterns:**
```
# scalpel.conf Beispiel
jpg y 200000000 \xff\xd8\xff\xe0\x00\x10 \xff\xd9
pdf y 200000000 %PDF- %%EOF\x0d
zip y 100000000 PK\x03\x04 PK\x05\x06
```
### Fragmentierte Datei-Rekonstruktion
**Bifragment-Gap-Carving:**
1. Identifikation von Header-Fragmenten
2. Berechnung wahrscheinlicher Fragment-Größen
3. Gap-Analyse zwischen Fragmenten
4. Reassembly mit Plausibilitätsprüfung
**Smart-Carving-Techniken:**
- Semantic-aware Carving für Office-Dokumente
- JPEG-Quantization-Table-Matching
- Video-Keyframe-basierte Rekonstruktion
## Timestamp-Manipulation und -Analyse
### MACB-Timeline-Erstellung
**Timestamp-Kategorien:**
- **M** (Modified): Letzter Schreibzugriff auf Dateiinhalt
- **A** (Accessed): Letzter Lesezugriff (oft deaktiviert)
- **C** (Changed): Metadaten-Änderung (Inode/MFT)
- **B** (Born): Erstellungszeitpunkt
```bash
# Mit fls Timeline erstellen
fls -r -m C: > timeline.bodyfile
mactime -d -b timeline.bodyfile > timeline.csv
# Mit log2timeline umfassende Timeline
log2timeline.py --storage-file timeline.plaso image.dd
psort.py -o l2tcsv -w timeline_full.csv timeline.plaso
```
### Timestamp-Manipulation-Detection
**Erkennungsstrategien:**
1. **Chronologie-Anomalien**: Created > Modified Timestamps
2. **Präzisions-Analyse**: Unnatürliche Rundung auf Sekunden/Minuten
3. **Filesystem-Vergleich**: Inkonsistenzen zwischen verschiedenen Timestamp-Quellen
4. **Batch-Manipulation**: Verdächtige Muster bei mehreren Dateien
**Registry-basierte Evidenz:**
```
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\NtfsDisableLastAccessUpdate
```
## Häufige Herausforderungen und Lösungsansätze
### Performance-Optimierung bei großen Images
**Problem**: Analyse von Multi-TB-Images dauert Tage
**Lösungen**:
1. **Selective Processing**: Nur relevante Partitionen analysieren
2. **Parallel Processing**: Multi-threaded Tools verwenden
3. **Hardware-Optimierung**: NVMe-SSDs für temporäre Dateien
4. **Cloud-Processing**: Verteilte Analyse in der Cloud
### Verschlüsselte Container und Volumes
**BitLocker-Forensik**:
```bash
# Mit dislocker BitLocker-Volume mounten
dislocker -r -V /dev/sda1 -p password -- /tmp/bitlocker
# Recovery-Key-basierter Zugriff
dislocker -r -V /dev/sda1 -k recovery.key -- /tmp/bitlocker
```
**VeraCrypt-Analyse**:
- Header-Backup-Analyse für mögliche Passwort-Recovery
- Hidden-Volume-Detection durch Entropie-Analyse
- Keyfile-basierte Entschlüsselung
### Anti-Forensik-Techniken erkennen
**Wiping-Detection**:
- Pattern-Analyse für DoD 5220.22-M Wiping
- Random-Data vs. Encrypted-Data Unterscheidung
- Unvollständige Wiping-Artefakte
**Timestomp-Detection**:
```bash
# Mit analyzeMFT.py Timestamp-Anomalien finden
analyzeMFT.py -f $MFT -o analysis.csv
# Analyse der $SI vs. $FN Timestamp-Diskrepanzen
```
## Tool-Integration und Workflows
### Autopsy-Integration
**Workflow-Setup**:
1. **Image-Import**: E01/DD-Images mit Hash-Verifikation
2. **Ingest-Module**: File-Type-Detection, Hash-Lookup, Timeline-Creation
3. **Analysis**: Keyword-Search, Timeline-Analysis, File-Category-Review
4. **Reporting**: Automatisierte Report-Generierung
### TSK-Kommandozeilen-Pipeline
```bash
#!/bin/bash
# Vollständiger Dateisystem-Analyse-Workflow
IMAGE="/cases/evidence.dd"
OUTPUT="/analysis/case001"
# 1. Partitionstabelle analysieren
mmls "$IMAGE" > "$OUTPUT/partitions.txt"
# 2. Dateisystem-Info extrahieren
fsstat "$IMAGE" > "$OUTPUT/filesystem_info.txt"
# 3. Timeline erstellen
fls -r -m "$IMAGE" > "$OUTPUT/timeline.bodyfile"
mactime -d -b "$OUTPUT/timeline.bodyfile" > "$OUTPUT/timeline.csv"
# 4. Gelöschte Dateien auflisten
fls -r -d "$IMAGE" > "$OUTPUT/deleted_files.txt"
# 5. File-Carving durchführen
foremost -t all -i "$IMAGE" -o "$OUTPUT/carved/"
# 6. Hash-Analyse
hfind -i nsrl "$OUTPUT/timeline.bodyfile" > "$OUTPUT/known_files.txt"
```
## Best Practices und Methodologie
### Dokumentation und Chain of Custody
**Kritische Dokumentationspunkte**:
1. **Acquisition-Details**: Tool, Version, Hash-Werte, Zeitstempel
2. **Analysis-Methodik**: Verwendete Tools und Parameter
3. **Findings-Dokumentation**: Screenshots, Befund-Zusammenfassung
4. **Timeline-Rekonstruktion**: Chronologische Ereignis-Dokumentation
### Qualitätssicherung
**Verifikations-Checkliste**:
- [ ] Hash-Integrität von Original-Images
- [ ] Tool-Version-Dokumentation
- [ ] Kreuz-Validierung mit verschiedenen Tools
- [ ] Timeline-Plausibilitätsprüfung
- [ ] Anti-Forensik-Artefakt-Suche
### Rechtliche Aspekte
**Admissibility-Faktoren**:
1. **Tool-Reliability**: Verwendung etablierter, validierter Tools
2. **Methodology-Documentation**: Nachvollziehbare Analyse-Schritte
3. **Error-Rate-Analysis**: Bekannte Limitationen dokumentieren
4. **Expert-Qualification**: Forensiker-Qualifikation nachweisen
## Weiterführende Ressourcen
### Spezialisierte Tools
- **X-Ways Forensics**: Kommerzielle All-in-One-Lösung
- **EnCase**: Enterprise-Forensik-Platform
- **AXIOM**: Mobile und Computer-Forensik
- **Oxygen Detective**: Mobile-Spezialist
- **BlackBag**: macOS-Forensik-Spezialist
### Fortgeschrittene Techniken
- **Memory-Forensics**: Volatility für RAM-Analyse
- **Network-Forensics**: Wireshark für Netzwerk-Traffic
- **Mobile-Forensics**: Cellebrite/Oxygen für Smartphone-Analyse
- **Cloud-Forensics**: KAPE für Cloud-Artefakt-Collection
### Continuous Learning
- **SANS FOR508**: Advanced Digital Forensics
- **Volatility Training**: Memory-Forensics-Spezialisierung
- **FIRST Conference**: Internationale Forensik-Community
- **DFRWS**: Digital Forensics Research Workshop
Die moderne Dateisystem-Forensik erfordert ein tiefes Verständnis verschiedener Speichertechnologien und deren forensischer Artefakte. Durch systematische Anwendung der beschriebenen Techniken und kontinuierliche Weiterbildung können Forensiker auch komplexeste Fälle erfolgreich bearbeiten und gerichtsfeste Beweise sicherstellen.

View File

@ -0,0 +1,377 @@
---
title: "Hash-Funktionen und digitale Signaturen: Grundlagen der digitalen Beweissicherung"
description: "Umfassender Leitfaden zu kryptographischen Hash-Funktionen, digitalen Signaturen und deren praktischer Anwendung in der digitalen Forensik für Integritätsprüfung und Beweissicherung"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: advanced
categories: ["analysis", "configuration", "case-study"]
tags: ["hashing", "integrity-check", "chain-of-custody", "standards-compliant", "deduplication", "known-bad-detection", "fuzzy-hashing", "digital-signatures", "timestamping", "blockchain-evidence", "md5", "sha256", "ssdeep"]
tool_name: "Hash Functions & Digital Signatures"
published: true
---
# Hash-Funktionen und digitale Signaturen: Grundlagen der digitalen Beweissicherung
Hash-Funktionen und digitale Signaturen bilden das fundamentale Rückgrat der digitalen Forensik. Sie gewährleisten die Integrität von Beweismitteln, ermöglichen die Authentifizierung von Daten und sind essentiell für die rechtssichere Dokumentation forensischer Untersuchungen.
## Was sind kryptographische Hash-Funktionen?
Eine kryptographische Hash-Funktion ist ein mathematisches Verfahren, das aus beliebig großen Eingabedaten einen festen, eindeutigen "Fingerabdruck" (Hash-Wert) erzeugt. Dieser Wert verändert sich drastisch, wenn auch nur ein einzelnes Bit der Eingabe modifiziert wird.
### Eigenschaften einer kryptographischen Hash-Funktion
**Einwegfunktion (One-Way Function)**
- Aus dem Hash-Wert kann nicht auf die ursprünglichen Daten geschlossen werden
- Mathematisch praktisch irreversibel
**Determinismus**
- Identische Eingabe erzeugt immer identischen Hash-Wert
- Reproduzierbare Ergebnisse für forensische Dokumentation
**Kollisionsresistenz**
- Extrem schwierig, zwei verschiedene Eingaben zu finden, die denselben Hash erzeugen
- Gewährleistet Eindeutigkeit in forensischen Anwendungen
**Lawineneffekt**
- Minimale Änderung der Eingabe führt zu völlig anderem Hash-Wert
- Erkennung von Manipulationen
## Wichtige Hash-Algorithmen in der Forensik
### MD5 (Message Digest Algorithm 5)
```bash
# MD5-Hash berechnen
md5sum evidence.dd
# Output: 5d41402abc4b2a76b9719d911017c592 evidence.dd
```
**Eigenschaften:**
- 128-Bit Hash-Wert (32 Hexadezimal-Zeichen)
- Entwickelt 1991, kryptographisch gebrochen seit 2004
- **Nicht mehr sicher**, aber weit verbreitet in Legacy-Systemen
- Kollisionen sind praktisch erzeugbar
**Forensische Relevanz:**
- Noch in vielen bestehenden Systemen verwendet
- Für forensische Zwecke nur bei bereits vorhandenen MD5-Hashes
- Niemals für neue forensische Implementierungen verwenden
### SHA-1 (Secure Hash Algorithm 1)
```bash
# SHA-1-Hash berechnen
sha1sum evidence.dd
# Output: aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d evidence.dd
```
**Eigenschaften:**
- 160-Bit Hash-Wert (40 Hexadezimal-Zeichen)
- Entwickelt von NSA, standardisiert 1995
- **Deprecated seit 2017** aufgrund praktischer Kollisionsangriffe
- SHAttered-Angriff bewies Schwachstellen 2017
### SHA-2-Familie (SHA-256, SHA-512)
```bash
# SHA-256-Hash berechnen
sha256sum evidence.dd
# Output: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 evidence.dd
# SHA-512-Hash berechnen
sha512sum evidence.dd
```
**SHA-256 Eigenschaften:**
- 256-Bit Hash-Wert (64 Hexadezimal-Zeichen)
- Aktueller Standard für forensische Anwendungen
- NIST-approved, FIPS 180-4 konform
- Keine bekannten praktischen Angriffe
**SHA-512 Eigenschaften:**
- 512-Bit Hash-Wert (128 Hexadezimal-Zeichen)
- Höhere Sicherheit, aber größerer Hash-Wert
- Optimal für hochsensible Ermittlungen
### SHA-3 (Keccak)
- Neuester Standard (seit 2015)
- Andere mathematische Grundlage als SHA-2
- Zukünftiger Standard bei SHA-2-Kompromittierung
## Forensische Anwendungen von Hash-Funktionen
### 1. Datenträger-Imaging und Verifikation
**Vor dem Imaging:**
```bash
# Original-Datenträger hashen
sha256sum /dev/sdb > original_hash.txt
```
**Nach dem Imaging:**
```bash
# Image-Datei hashen
sha256sum evidence.dd > image_hash.txt
# Vergleichen
diff original_hash.txt image_hash.txt
```
**Best Practice:**
- Immer mehrere Hash-Algorithmen verwenden (SHA-256 + SHA-512)
- Hash-Berechnung vor, während und nach dem Imaging
- Dokumentation in Chain-of-Custody-Protokoll
### 2. Deduplizierung mit Hash-Sets
Hash-Sets ermöglichen die Identifikation bekannter Dateien zur Effizienzsteigerung:
**NSRL (National Software Reference Library)**
```bash
# NSRL-Hash-Set laden
autopsy --load-hashset /path/to/nsrl/NSRLFile.txt
# Bekannte Dateien ausschließen
hashdeep -s -e nsrl_hashes.txt /evidence/mount/
```
**Eigene Hash-Sets erstellen:**
```bash
# Hash-Set von bekannten guten Dateien
hashdeep -r /clean_system/ > clean_system_hashes.txt
# Vergleich mit verdächtigem System
hashdeep -s -e clean_system_hashes.txt /suspect_system/
```
### 3. Known-Bad-Erkennung
**Malware-Hash-Datenbanken:**
- VirusTotal API-Integration
- Threat Intelligence Feeds
- Custom IoC-Listen
```python
# Beispiel: Datei-Hash gegen Known-Bad-Liste prüfen
import hashlib
def check_malware_hash(filepath, malware_hashes):
with open(filepath, 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
if file_hash in malware_hashes:
return True, file_hash
return False, file_hash
```
### 4. Fuzzy Hashing mit ssdeep
Fuzzy Hashing erkennt ähnliche, aber nicht identische Dateien:
```bash
# ssdeep-Hash berechnen
ssdeep malware.exe
# Output: 768:gQA1M2Ua3QqQm8+1QV7Q8+1QG8+1Q:gQ1Ma3qmP1QV7P1QGP1Q
# Ähnlichkeit zwischen Dateien prüfen
ssdeep -d malware_v1.exe malware_v2.exe
# Output: 85 (85% Ähnlichkeit)
```
**Anwendungsfälle:**
- Erkennung von Malware-Varianten
- Identifikation modifizierter Dokumente
- Versionsverfolgung von Dateien
### 5. Timeline-Analyse und Integritätsprüfung
```bash
# Erweiterte Metadaten mit Hashes
find /evidence/mount -type f -exec stat -c "%Y %n" {} \; | while read timestamp file; do
hash=$(sha256sum "$file" | cut -d' ' -f1)
echo "$timestamp $hash $file"
done > timeline_with_hashes.txt
```
## Digitale Signaturen in der Forensik
Digitale Signaturen verwenden asymmetrische Kryptographie zur Authentifizierung und Integritätssicherung.
### Funktionsweise digitaler Signaturen
1. **Erstellung:**
- Hash des Dokuments wird mit privatem Schlüssel verschlüsselt
- Verschlüsselter Hash = digitale Signatur
2. **Verifikation:**
- Signatur wird mit öffentlichem Schlüssel entschlüsselt
- Entschlüsselter Hash wird mit neuem Hash des Dokuments verglichen
### Certificate Chain Analysis
**X.509-Zertifikate untersuchen:**
```bash
# Zertifikat-Details anzeigen
openssl x509 -in certificate.crt -text -noout
# Zertifikatskette verfolgen
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt certificate.crt
```
**Forensische Relevanz:**
- Authentizität von Software-Downloads
- Erkennung gefälschter Zertifikate
- APT-Gruppenattribution durch Code-Signing-Zertifikate
### Timestamping für Chain-of-Custody
**RFC 3161-Zeitstempel:**
```bash
# Zeitstempel für Beweisdatei erstellen
openssl ts -query -data evidence.dd -no_nonce -sha256 -out request.tsq
openssl ts -verify -in response.tsr -data evidence.dd -CAfile tsa-ca.crt
```
**Blockchain-basierte Zeitstempel:**
- Unveränderliche Zeitstempel in öffentlichen Blockchains
- OriginStamp, OpenTimestamps für forensische Anwendungen
## Praktische Tools und Integration
### Autopsy Integration
```xml
<!-- Autopsy Hash Database Configuration -->
<hashDb>
<dbType>NSRL</dbType>
<dbPath>/usr/share/autopsy/nsrl/NSRLFile.txt</dbPath>
<searchDuringIngest>true</searchDuringIngest>
</hashDb>
```
### YARA-Integration mit Hash-Regeln
```yara
rule Malware_Hash_Detection {
condition:
hash.sha256(0, filesize) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
```
### FTK Imager Hash-Verifikation
- Automatische Hash-Berechnung während Imaging
- MD5, SHA-1, SHA-256 parallel
- Verify-Funktion für Image-Integrität
## Advanced Topics
### Rainbow Table Attacks
**Funktionsweise:**
- Vorberechnete Hash-Tabellen für Passwort-Cracking
- Trade-off zwischen Speicher und Rechenzeit
- Effektiv gegen unsalted Hashes
**Forensische Anwendung:**
```bash
# Hashcat mit Rainbow Tables
hashcat -m 0 -a 0 hashes.txt wordlist.txt
# John the Ripper mit Rainbow Tables
john --format=NT --wordlist=rockyou.txt ntlm_hashes.txt
```
### Blockchain Evidence Management
**Konzept:**
- Unveränderliche Speicherung von Hash-Werten
- Distributed Ledger für Chain-of-Custody
- Smart Contracts für automatisierte Verifikation
**Implementierung:**
```solidity
// Ethereum Smart Contract für Evidence Hashes
contract EvidenceRegistry {
mapping(bytes32 => bool) public evidenceHashes;
function registerEvidence(bytes32 _hash) public {
evidenceHashes[_hash] = true;
}
}
```
## Häufige Probleme und Lösungsansätze
### Hash-Kollisionen
**Problem:** Zwei verschiedene Dateien mit identischem Hash
**Lösung:**
- Verwendung mehrerer Hash-Algorithmen
- Sichere Algorithmen (SHA-256+) verwenden
- Bei Verdacht: Bitweise Vergleich der Originaldateien
### Performance bei großen Datenmengen
**Problem:** Langsame Hash-Berechnung bei TB-großen Images
**Optimierung:**
```bash
# Parallele Hash-Berechnung
hashdeep -r -j 8 /large_dataset/ # 8 Threads
# Hardware-beschleunigte Hashing
sha256sum --tag /dev/nvme0n1 # NVMe für bessere I/O
```
### Rechtliche Anforderungen
**Problem:** Verschiedene Standards in verschiedenen Jurisdiktionen
**Lösung:**
- NIST-konforme Algorithmen verwenden
- Dokumentation aller verwendeten Verfahren
- Regelmäßige Algorithmus-Updates
## Best Practices
### 1. Algorithmus-Auswahl
- **Neu:** SHA-256 oder SHA-3 verwenden
- **Legacy:** MD5/SHA-1 nur bei vorhandenen Systemen
- **High-Security:** SHA-512 oder SHA-3-512
### 2. Dokumentation
```text
Evidence Hash Verification Report
=================================
Evidence ID: CASE-2024-001-HDD
Original Hash (SHA-256): a1b2c3d4...
Image Hash (SHA-256): a1b2c3d4...
Verification Status: VERIFIED
Timestamp: 2024-01-15 14:30:00 UTC
Investigator: John Doe
```
### 3. Redundanz
- Mindestens zwei verschiedene Hash-Algorithmen
- Mehrfache Verifikation zu verschiedenen Zeitpunkten
- Verschiedene Tools für Cross-Validation
### 4. Automation
```bash
#!/bin/bash
# Automatisiertes Hash-Verification-Script
EVIDENCE_FILE="$1"
LOG_FILE="hash_verification.log"
echo "Starting hash verification for $EVIDENCE_FILE" >> $LOG_FILE
MD5_HASH=$(md5sum "$EVIDENCE_FILE" | cut -d' ' -f1)
SHA256_HASH=$(sha256sum "$EVIDENCE_FILE" | cut -d' ' -f1)
SHA512_HASH=$(sha512sum "$EVIDENCE_FILE" | cut -d' ' -f1)
echo "MD5: $MD5_HASH" >> $LOG_FILE
echo "SHA-256: $SHA256_HASH" >> $LOG_FILE
echo "SHA-512: $SHA512_HASH" >> $LOG_FILE
echo "Verification completed at $(date)" >> $LOG_FILE
```
## Zukunftsperspektiven
### Quantum-Resistant Hashing
- Vorbereitung auf Quantum Computing
- NIST Post-Quantum Cryptography Standards
- Migration bestehender Systeme
### AI/ML-Integration
- Anomalie-Erkennung in Hash-Mustern
- Automated Similarity Analysis
- Intelligent Deduplizierung
Hash-Funktionen und digitale Signaturen sind und bleiben das Fundament der digitalen Forensik. Das Verständnis ihrer mathematischen Grundlagen, praktischen Anwendungen und rechtlichen Implikationen unterscheidet professionelle Forensiker von Amateuren. Mit der kontinuierlichen Weiterentwicklung der Technologie müssen auch forensische Praktiken angepasst werden, um die Integrität und Authentizität digitaler Beweise zu gewährleisten.

View File

@ -0,0 +1,666 @@
---
title: "Memory Forensics und Process Analysis: Advanced Malware Detection in Volatile Memory"
description: "Umfassender Leitfaden zur forensischen Analyse von Arbeitsspeicher-Strukturen, Process-Injection-Techniken und Advanced-Malware-Detection. Von Kernel-Analysis bis Cross-Platform-Memory-Forensik."
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: advanced
categories: ["analysis", "advanced-techniques", "malware-investigation"]
tags: ["memory-structures", "process-injection", "rootkit-detection", "kernel-analysis", "address-space", "live-analysis", "malware-hiding", "system-internals", "volatility", "dll-hollowing", "process-ghosting"]
related_tools: ["Volatility 3", "Rekall", "WinDbg", "GDB"]
published: true
---
# Memory Forensics und Process Analysis: Advanced Malware Detection in Volatile Memory
Memory Forensics stellt eine der komplexesten und gleichzeitig aufschlussreichsten Disziplinen der digitalen Forensik dar. Während traditionelle Festplatten-Forensik auf persistente Daten zugreift, ermöglicht die Analyse des Arbeitsspeichers Einblicke in aktive Prozesse, verschleierte Malware und Angriffstechniken, die keine Spuren auf der Festplatte hinterlassen.
## Einführung in Memory Forensics
### Was ist Memory Forensics?
Memory Forensics ist die Wissenschaft der Analyse von Computer-Arbeitsspeicher (RAM) zur Aufdeckung digitaler Artefakte. Im Gegensatz zur traditionellen Festplatten-Forensik konzentriert sich Memory Forensics auf volatile Daten, die nur temporär im Speicher existieren.
**Zentrale Vorteile:**
- Erkennung von Malware, die nur im Speicher residiert
- Aufdeckung von Process-Injection und Code-Hiding-Techniken
- Analyse von verschlüsselten oder obfuscierten Prozessen
- Rekonstruktion von Netzwerkverbindungen und Benutzeraktivitäten
- Untersuchung von Kernel-Level-Rootkits
### Virtual Memory Layout verstehen
Das Virtual Memory System moderner Betriebssysteme bildet die Grundlage für Memory Forensics. Jeder Prozess erhält einen eigenen virtuellen Adressraum, der in verschiedene Segmente unterteilt ist:
**Windows Virtual Memory Layout:**
```
0x00000000 - 0x7FFFFFFF: User Space (2GB)
0x80000000 - 0xFFFFFFFF: Kernel Space (2GB)
User Space Segmente:
- 0x00000000 - 0x0000FFFF: NULL Pointer Region
- 0x00010000 - 0x7FFEFFFF: User Code und Data
- 0x7FFF0000 - 0x7FFFFFFF: System DLLs (ntdll.dll)
```
**Linux Virtual Memory Layout:**
```
0x00000000 - 0xBFFFFFFF: User Space (3GB)
0xC0000000 - 0xFFFFFFFF: Kernel Space (1GB)
User Space Segmente:
- Text Segment: Executable Code
- Data Segment: Initialized Variables
- BSS Segment: Uninitialized Variables
- Heap: Dynamic Memory Allocation
- Stack: Function Calls und Local Variables
```
## Process Internals und Strukturen
### Process Control Blocks (PCB)
Jeder Prozess wird durch eine zentrale Datenstruktur repräsentiert, die alle relevanten Informationen enthält:
**Windows EPROCESS Structure:**
```c
typedef struct _EPROCESS {
KPROCESS Pcb; // Process Control Block
EX_PUSH_LOCK ProcessLock; // Process Lock
LARGE_INTEGER CreateTime; // Creation Timestamp
LARGE_INTEGER ExitTime; // Exit Timestamp
EX_RUNDOWN_REF RundownProtect; // Rundown Protection
HANDLE UniqueProcessId; // Process ID (PID)
LIST_ENTRY ActiveProcessLinks; // Double Linked List
RTL_AVL_TREE VadRoot; // Virtual Address Descriptors
// ... weitere Felder
} EPROCESS, *PEPROCESS;
```
**Wichtige Felder für Forensik:**
- `ImageFileName`: Name der ausführbaren Datei
- `Peb`: Process Environment Block Pointer
- `VadRoot`: Virtual Address Descriptor Tree
- `Token`: Security Token des Prozesses
- `HandleTable`: Tabelle geöffneter Handles
### Thread Control Blocks (TCB)
Threads sind die ausführbaren Einheiten innerhalb eines Prozesses:
**Windows ETHREAD Structure:**
```c
typedef struct _ETHREAD {
KTHREAD Tcb; // Thread Control Block
LARGE_INTEGER CreateTime; // Thread Creation Time
LIST_ENTRY ThreadListEntry; // Process Thread List
EX_RUNDOWN_REF RundownProtect; // Rundown Protection
PEPROCESS ThreadsProcess; // Parent Process Pointer
PVOID StartAddress; // Thread Start Address
// ... weitere Felder
} ETHREAD, *PETHREAD;
```
## Advanced Malware Detection Techniken
### Process Injection Erkennung
Process Injection ist eine häufig verwendete Technik zur Umgehung von Security-Lösungen. Verschiedene Injection-Methoden erfordern spezifische Erkennungsansätze:
#### DLL Injection Detection
**Erkennungsmerkmale:**
```bash
# Volatility 3 Command
python vol.py -f memory.dmp windows.dlllist.DllList --pid 1234
# Verdächtige Indikatoren:
# - Ungewöhnliche DLL-Pfade
# - DLLs ohne digitale Signatur
# - Temporäre oder versteckte Pfade
# - Diskrepanzen zwischen Image und Memory
```
**Manuelle Verifikation:**
```python
# Pseudocode für DLL-Validierung
def validate_dll_integrity(dll_base, dll_path):
memory_hash = calculate_memory_hash(dll_base)
disk_hash = calculate_file_hash(dll_path)
if memory_hash != disk_hash:
return "POTENTIAL_INJECTION_DETECTED"
return "CLEAN"
```
#### Process Hollowing Detection
Process Hollowing ersetzt den ursprünglichen Code eines legitimen Prozesses:
**Erkennungsmerkmale:**
- Diskrepanz zwischen ImageFileName und tatsächlichem Code
- Ungewöhnliche Memory Protection Flags
- Fehlende oder modifizierte PE Header
- Unerwartete Entry Points
**Volatility Detection:**
```bash
# Process Hollowing Indicators
python vol.py -f memory.dmp windows.malfind.Malfind
python vol.py -f memory.dmp windows.vadinfo.VadInfo --pid 1234
```
#### Process Ghosting Detection
Eine der neuesten Evasion-Techniken, die Prozesse ohne korrespondierende Dateien auf der Festplatte erstellt:
**Erkennungsmerkmale:**
```bash
# File Object Analysis
python vol.py -f memory.dmp windows.handles.Handles --pid 1234
# Suche nach:
# - Deleted File Objects
# - Processes ohne korrespondierende Image Files
# - Ungewöhnliche Creation Patterns
```
### DLL Hollowing und Memory Manipulation
DLL Hollowing überschreibt legitimierte DLL-Sektionen mit malicious Code:
**Detection Workflow:**
1. **Section Analysis:**
```bash
python vol.py -f memory.dmp windows.vadinfo.VadInfo --pid 1234
```
2. **Memory Permission Analysis:**
```bash
# Suche nach ungewöhnlichen Permissions
# RWX (Read-Write-Execute) Bereiche sind verdächtig
```
3. **Entropy Analysis:**
```python
def calculate_section_entropy(memory_region):
entropy = 0
for byte_value in range(256):
probability = memory_region.count(byte_value) / len(memory_region)
if probability > 0:
entropy += probability * math.log2(probability)
return -entropy
```
## Kernel-Level Analysis
### System Call Hooking Detection
Rootkits manipulieren häufig System Call Tables (SSDT):
**Windows SSDT Analysis:**
```bash
# System Service Descriptor Table
python vol.py -f memory.dmp windows.ssdt.SSDT
# Verdächtige Indikatoren:
# - Hooks außerhalb bekannter Module
# - Ungewöhnliche Sprungadressen
# - Modifizierte System Call Nummern
```
**Linux System Call Table:**
```bash
# System Call Table Analysis für Linux
python vol.py -f linux.dmp linux.check_syscall.Check_syscall
```
### Driver Analysis
Kernel-Mode-Rootkits nutzen Device Driver für persistente Angriffe:
**Windows Driver Enumeration:**
```bash
# Loaded Modules Analysis
python vol.py -f memory.dmp windows.modules.Modules
# Driver IRP Analysis
python vol.py -f memory.dmp windows.driverscan.DriverScan
```
**Verdächtige Driver-Eigenschaften:**
- Fehlende Code-Signierung
- Ungewöhnliche Load-Adressen
- Versteckte oder gelöschte Driver-Files
- Modifizierte IRP (I/O Request Packet) Handler
### Rootkit Detection Methoden
#### Direct Kernel Object Manipulation (DKOM)
DKOM-Rootkits manipulieren Kernel-Datenstrukturen direkt:
**Process Hiding Detection:**
```bash
# Process Scan vs. Process List Comparison
python vol.py -f memory.dmp windows.psscan.PsScan > psscan.txt
python vol.py -f memory.dmp windows.pslist.PsList > pslist.txt
# Vergleich zeigt versteckte Prozesse
diff psscan.txt pslist.txt
```
#### EPROCESS Link Manipulation
```python
# Pseudocode für EPROCESS Validation
def validate_process_links(eprocess_list):
for process in eprocess_list:
flink = process.ActiveProcessLinks.Flink
blink = process.ActiveProcessLinks.Blink
# Validate bidirectional links
if flink.Blink != process or blink.Flink != process:
return "LINK_MANIPULATION_DETECTED"
```
## Memory Dump Acquisition Strategien
### Live Memory Acquisition
**Windows Memory Acquisition:**
```bash
# DumpIt (Comae)
DumpIt.exe /output C:\memory.dmp
# WinPmem
winpmem-2.1.post4.exe C:\memory.raw
# Magnet RAM Capture
MRCv1.20.exe /go /output C:\memory.dmp
```
**Linux Memory Acquisition:**
```bash
# LiME (Linux Memory Extractor)
insmod lime.ko "path=/tmp/memory.lime format=lime"
# AVML (Azure Virtual Machine Memory Extractor)
./avml memory.dmp
# dd (für /dev/mem falls verfügbar)
dd if=/dev/mem of=memory.dd bs=1M
```
### Memory Acquisition Challenges
**Volatility Considerations:**
- Memory-Inhalte ändern sich kontinuierlich
- Acquisition-Tools können Memory-Layout beeinflussen
- Anti-Forensic-Techniken können Acquisition verhindern
- Verschlüsselte Memory-Bereiche
**Best Practices:**
- Multiple Acquisition-Methoden verwenden
- Acquisition-Logs dokumentieren
- Hash-Werte für Integrität generieren
- Timestamp-Synchronisation
## Address Space Reconstruction
### Virtual Address Translation
Das Verständnis der Address Translation ist essentiell für Memory Forensics:
**Windows Page Table Walkthrough:**
```
Virtual Address (32-bit):
┌─────────────┬─────────────┬──────────────┐
│ PDE (10bit) │ PTE (10bit) │ Offset(12bit)│
└─────────────┴─────────────┴──────────────┘
1. Page Directory Entry → Page Table Base
2. Page Table Entry → Physical Page Frame
3. Offset → Byte within Physical Page
```
**Linux Page Table Structure:**
```
Virtual Address (64-bit):
┌───┬───┬───┬───┬──────────┐
│PGD│PUD│PMD│PTE│ Offset │
└───┴───┴───┴───┴──────────┘
4-Level Page Table (x86_64):
- PGD: Page Global Directory
- PUD: Page Upper Directory
- PMD: Page Middle Directory
- PTE: Page Table Entry
```
### Memory Mapping Analysis
**Windows VAD (Virtual Address Descriptor) Trees:**
```bash
# VAD Tree Analysis
python vol.py -f memory.dmp windows.vadinfo.VadInfo --pid 1234
# Memory Mapping Details
python vol.py -f memory.dmp windows.memmap.Memmap --pid 1234
```
**Linux Memory Maps:**
```bash
# Process Memory Maps
python vol.py -f linux.dmp linux.proc_maps.Maps --pid 1234
```
## Cross-Platform Memory Forensics
### Windows-Specific Artefakte
**Registry in Memory:**
```bash
# Registry Hives
python vol.py -f memory.dmp windows.registry.hivelist.HiveList
# Registry Keys
python vol.py -f memory.dmp windows.registry.printkey.PrintKey --key "Software\Microsoft\Windows\CurrentVersion\Run"
```
**Windows Event Logs:**
```bash
# Event Log Analysis
python vol.py -f memory.dmp windows.evtlogs.EvtLogs
```
### Linux-Specific Artefakte
**Process Environment:**
```bash
# Environment Variables
python vol.py -f linux.dmp linux.envars.Envars
# Process Arguments
python vol.py -f linux.dmp linux.psaux.PsAux
```
**Network Connections:**
```bash
# Network Sockets
python vol.py -f linux.dmp linux.netstat.Netstat
```
### macOS Memory Forensics
**Darwin Kernel Structures:**
```bash
# Process List (macOS)
python vol.py -f macos.dmp mac.pslist.PsList
# Network Connections
python vol.py -f macos.dmp mac.netstat.Netstat
```
## Live Analysis vs. Dead Analysis
### Live Memory Analysis
**Vorteile:**
- Vollständige System-Sicht
- Kontinuierliche Überwachung möglich
- Interaktive Analysis-Möglichkeiten
- Integration mit Incident Response
**Tools für Live Analysis:**
- Rekall (Live Mode)
- WinDbg (Live Debugging)
- GDB (Linux Live Debugging)
- Volatility mit Live Memory Plugins
**Live Analysis Workflow:**
```bash
# Rekall Live Analysis
rekall --live Memory
# Memory-basierte Malware Detection
rekall> pslist
rekall> malfind
rekall> hollowfind
```
### Dead Memory Analysis
**Vorteile:**
- Stabile Analysis-Umgebung
- Reproduzierbare Ergebnisse
- Tiefere forensische Untersuchung
- Legal-konforme Beweisführung
**Typical Workflow:**
```bash
# 1. Memory Dump Analysis
python vol.py -f memory.dmp windows.info.Info
# 2. Process Analysis
python vol.py -f memory.dmp windows.pslist.PsList
python vol.py -f memory.dmp windows.pstree.PsTree
# 3. Malware Detection
python vol.py -f memory.dmp windows.malfind.Malfind
# 4. Network Analysis
python vol.py -f memory.dmp windows.netstat.NetStat
# 5. Registry Analysis
python vol.py -f memory.dmp windows.registry.hivelist.HiveList
```
## Encrypted Memory Handling
### Windows BitLocker Memory
BitLocker-verschlüsselte Systeme stellen besondere Herausforderungen dar:
**Memory Encryption Bypass:**
- Cold Boot Attacks auf Encryption Keys
- DMA (Direct Memory Access) Attacks
- Hibernation File Analysis
### Full Memory Encryption (TME)
Intel Total Memory Encryption (TME) verschlüsselt den gesamten Arbeitsspeicher:
**Forensic Implications:**
- Hardware-basierte Key-Extraktion erforderlich
- Firmware-Level-Access notwendig
- Acquisition vor Memory-Locking
## Advanced Analysis Techniken
### Machine Learning in Memory Forensics
**Anomaly Detection:**
```python
# Pseudocode für ML-basierte Process Analysis
def detect_process_anomalies(memory_dump):
features = extract_process_features(memory_dump)
# Features: Memory Permissions, API Calls, Network Connections
model = load_trained_model('process_anomaly_detection.pkl')
anomalies = model.predict(features)
return anomalies
```
### Timeline Reconstruction
**Memory-basierte Timeline:**
```bash
# Process Creation Timeline
python vol.py -f memory.dmp windows.pslist.PsList --output-format=timeline
# File Object Timeline
python vol.py -f memory.dmp windows.handles.Handles --object-type=File
```
### Memory Forensics Automation
**Automated Analysis Framework:**
```python
#!/usr/bin/env python3
class MemoryForensicsAutomation:
def __init__(self, memory_dump):
self.dump = memory_dump
self.results = {}
def run_baseline_analysis(self):
# Basic System Information
self.results['info'] = self.run_volatility_plugin('windows.info.Info')
# Process Analysis
self.results['processes'] = self.run_volatility_plugin('windows.pslist.PsList')
# Malware Detection
self.results['malware'] = self.run_volatility_plugin('windows.malfind.Malfind')
# Network Analysis
self.results['network'] = self.run_volatility_plugin('windows.netstat.NetStat')
return self.results
def detect_anomalies(self):
# Implementation für automatisierte Anomaly Detection
pass
```
## Häufige Herausforderungen und Lösungsansätze
### Anti-Forensic Techniken
**Memory Wiping:**
- Erkennung durch Memory Allocation Patterns
- Analyse von Memory Page Timestamps
- Reconstruction durch Memory Slack
**Process Masquerading:**
- PE Header Validation
- Import Address Table (IAT) Analysis
- Code Signing Verification
**Timing Attacks:**
- Memory Acquisition Race Conditions
- Process Termination während Acquisition
- Kontinuierliche Monitoring-Strategien
### Performance Optimierung
**Large Memory Dumps:**
```bash
# Parallel Processing
python vol.py -f memory.dmp --parallel=4 windows.pslist.PsList
# Targeted Analysis
python vol.py -f memory.dmp windows.pslist.PsList --pid 1234,5678
```
**Memory Usage Optimization:**
- Streaming Analysis für große Dumps
- Indexed Memory Access
- Selective Plugin Execution
## Tools und Framework Integration
### Volatility 3 Framework
**Plugin Development:**
```python
class CustomMalwareDetector(interfaces.plugins.PluginInterface):
"""Custom Plugin für Advanced Malware Detection"""
@classmethod
def get_requirements(cls):
return [requirements.TranslationLayerRequirement(name='primary'),
requirements.SymbolTableRequirement(name="nt_symbols")]
def run(self):
# Implementation der Detection-Logik
pass
```
### Integration mit SIEM-Systemen
**ElasticSearch Integration:**
```python
def export_to_elasticsearch(memory_analysis_results):
es = Elasticsearch(['localhost:9200'])
for artifact in memory_analysis_results:
doc = {
'timestamp': artifact.timestamp,
'process_name': artifact.process_name,
'suspicious_score': artifact.score,
'detection_method': artifact.method
}
es.index(index='memory-forensics', body=doc)
```
## Best Practices und Empfehlungen
### Forensic Methodology
1. **Preservation First**: Memory Dump Acquisition vor anderen Aktionen
2. **Documentation**: Vollständige Dokumentation aller Analysis-Schritte
3. **Validation**: Cross-Referencing verschiedener Evidence Sources
4. **Chain of Custody**: Lückenlose Beweiskette
5. **Reproducibility**: Wiederholbare Analysis-Prozesse
### Quality Assurance
**Hash Verification:**
```bash
# MD5/SHA256 Hashes für Memory Dumps
md5sum memory.dmp > memory.dmp.md5
sha256sum memory.dmp > memory.dmp.sha256
```
**Analysis Documentation:**
```markdown
# Memory Forensics Analysis Report
## System Information
- OS Version: Windows 10 Pro 1909
- Architecture: x64
- Memory Size: 16GB
- Acquisition Time: 2024-01-15 14:30:00 UTC
## Tools Used
- Volatility 3.2.0
- Rekall 1.7.2
- Custom Scripts: malware_detector.py
## Key Findings
1. Process Injection detected in explorer.exe (PID 1234)
2. Unknown driver loaded: malicious.sys
3. Network connections to suspicious IPs
```
## Fazit
Memory Forensics stellt ein mächtiges Werkzeug für die Aufdeckung komplexer Angriffe dar, die traditionelle Festplatten-Forensik umgehen. Die kontinuierliche Weiterentwicklung von Angriffstechniken erfordert eine entsprechende Evolution der forensischen Methoden.
**Zukünftige Entwicklungen:**
- Hardware-basierte Memory Protection Bypass
- Machine Learning für Automated Threat Detection
- Cloud Memory Forensics
- Containerized Environment Analysis
- Real-time Memory Threat Hunting
Die Beherrschung von Memory Forensics erfordert ein tiefes Verständnis von Betriebssystem-Internals, Malware-Techniken und forensischen Methoden. Kontinuierliche Weiterbildung und praktische Erfahrung sind essentiell für erfolgreiche Memory-basierte Investigations.
## Weiterführende Ressourcen
- **Volatility Labs Blog**: Aktuelle Research zu Memory Forensics
- **SANS FOR508**: Advanced Incident Response und Digital Forensics
- **Black Hat/DEF CON**: Security Conference Presentations
- **Academic Papers**: IEEE Security & Privacy, USENIX Security
- **Open Source Tools**: GitHub Repositories für Custom Plugins

View File

@ -0,0 +1,517 @@
---
title: "Netzwerkprotokoll-Analyse für forensische Untersuchungen"
description: "Umfassender Leitfaden zur forensischen Analyse von Netzwerkprotokollen Layer 2-7, Session-Rekonstruktion aus PCAP-Dateien, C2-Kommunikations-Pattern-Erkennung und APT-Hunting-Techniken für Incident Response."
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: intermediate
categories: ["analysis", "troubleshooting", "case-study"]
tags: ["protocol-analysis", "packet-inspection", "session-reconstruction", "c2-analysis", "traffic-patterns", "network-baseline", "payload-extraction", "anomaly-detection", "incident-response", "apt-hunting"]
tool_name: "Network Protocols & Packet Analysis"
related_tools: ["Wireshark", "NetworkMiner", "tcpdump"]
published: true
---
# Netzwerkprotokoll-Analyse für forensische Untersuchungen
Die forensische Analyse von Netzwerkprotokollen ist ein fundamentaler Baustein moderner Incident Response und APT-Hunting-Aktivitäten. Dieser Leitfaden vermittelt systematische Methoden zur Untersuchung von Netzwerkverkehr von Layer 2 bis Layer 7 des OSI-Modells.
## Warum Netzwerkprotokoll-Forensik?
In komplexen Cyberangriffen hinterlassen Angreifer Spuren in der Netzwerkkommunikation, die oft die einzigen verfügbaren Beweise darstellen. Command & Control (C2) Kommunikation, Datenexfiltration und laterale Bewegungen manifestieren sich als charakteristische Netzwerkmuster, die durch systematische Protokoll-Analyse erkennbar werden.
## Voraussetzungen
### Technische Kenntnisse
- Grundverständnis des OSI-7-Schichten-Modells
- TCP/IP-Stack-Funktionsweise
- HTTP/HTTPS-Request/Response-Struktur
- DNS-Query-Mechanismen
- Grundlagen der Kryptographie (TLS/SSL)
### Systemanforderungen
- Wireshark 4.0+ oder vergleichbare Packet-Analyzer
- Leistungsfähiges System für große PCAP-Analysen (16GB+ RAM)
- NetworkMiner oder ähnliche Session-Rekonstruktions-Tools
- Python 3.8+ für Automatisierungsskripte
### Rechtliche Überlegungen
- Erforderliche Genehmigungen für Netzwerk-Monitoring
- Datenschutzbestimmungen bei der Payload-Analyse
- Chain-of-Custody-Anforderungen für Netzwerk-Evidence
## Fundamentale Protokoll-Analyse-Methodik
### Layer 2 - Data Link Layer Forensik
**Ethernet-Frame-Analyse für Asset-Discovery:**
```bash
# MAC-Adressen-Inventarisierung aus PCAP
tshark -r capture.pcap -T fields -e eth.src -e eth.dst | sort -u
```
**Switch-Infrastruktur-Mapping:**
- Spanning Tree Protocol (STP) Topologie-Rekonstruktion
- VLAN-Segmentierung-Analyse
- ARP-Spoofing-Detection durch MAC-IP-Binding-Inkonsistenzen
**Kritische Anomalien:**
- Unerwartete MAC-Präfixe (OUI-Analysis)
- ARP-Reply ohne vorhergehende ARP-Request
- Broadcast-Storm-Patterns bei DDoS-Aktivitäten
### Layer 3 - Network Layer Investigation
**IP-Header-Forensik für Geolocation und Routing:**
```python
# IP-Geolocation-Mapping mit Python
import ipaddress
from geolite2 import geolite2
def analyze_ip_origins(pcap_ips):
reader = geolite2.reader()
for ip in pcap_ips:
if not ipaddress.ip_address(ip).is_private:
location = reader.get(ip)
print(f"{ip}: {location['country']['names']['en']}")
```
**TTL-Fingerprinting für OS-Detection:**
- Windows: TTL 128 (typisch 128, 64, 32)
- Linux/Unix: TTL 64
- Cisco/Network-Equipment: TTL 255
**Fragmentierungs-Analyse:**
- Evil Fragmentation für IDS-Evasion
- Teardrop-Attack-Patterns
- Fragment-Overlap-Anomalien
### Layer 4 - Transport Layer Forensik
**TCP-Session-Rekonstruktion:**
```bash
# TCP-Streams extrahieren und analysieren
tshark -r capture.pcap -q -z follow,tcp,ascii,0
```
**TCP-Fingerprinting-Techniken:**
- Initial Window Size (IWS) Analysis
- TCP-Options-Sequenz-Patterns
- Maximum Segment Size (MSS) Charakteristika
**UDP-Traffic-Anomalien:**
- DNS-Tunneling über ungewöhnliche Record-Types
- VoIP-Protokoll-Missbrauch für Datenexfiltration
- TFTP-basierte Malware-Distribution
## HTTP/HTTPS-Forensik für Web-basierte Angriffe
### HTTP-Header-Deep-Dive
**User-Agent-String-Forensik:**
```python
# Verdächtige User-Agent-Patterns
suspicious_agents = [
"curl/", # Command-line tools
"python-requests", # Scripted access
"Nikto", # Vulnerability scanners
"sqlmap" # SQL injection tools
]
```
**HTTP-Method-Anomalien:**
- PUT/DELETE-Requests auf produktiven Servern
- TRACE-Method für XSS-Exploitation
- Nicht-standard Methods (PATCH, OPTIONS) Analysis
**Content-Type-Diskrepanzen:**
- Executable-Content mit image/jpeg MIME-Type
- JavaScript-Code in PDF-Dateien
- Suspicious Content-Length vs. Actual-Payload-Size
### HTTPS-Traffic-Analysis ohne Decryption
**TLS-Handshake-Fingerprinting:**
```bash
# TLS-Version und Cipher-Suite-Analyse
tshark -r capture.pcap -Y "tls.handshake.type == 1" \
-T fields -e tls.handshake.version -e tls.handshake.ciphersuites
```
**Certificate-Chain-Investigation:**
- Self-signed Certificate-Anomalien
- Certificate-Transparency-Log-Validation
- Subject Alternative Name (SAN) Missbrauch
**Encrypted-Traffic-Patterns:**
- Packet-Size-Distribution-Analysis
- Inter-arrival-Time-Patterns
- Burst-Communication vs. Steady-State-Traffic
## DNS-Forensik und Tunneling-Detection
### DNS-Query-Pattern-Analysis
**DNS-Tunneling-Indicators:**
```python
# DNS-Query-Length-Distribution-Analysis
def analyze_dns_queries(pcap_file):
queries = extract_dns_queries(pcap_file)
avg_length = sum(len(q) for q in queries) / len(queries)
# Normal DNS: 15-30 chars, Tunneling: 50+ chars
if avg_length > 50:
return "POTENTIAL_TUNNELING"
```
**Subdomain-Enumeration-Detection:**
- Excessive NXDOMAIN-Responses
- Sequential-Subdomain-Queries
- High-Entropy-Subdomain-Names
**DNS-over-HTTPS (DoH) Investigation:**
- DoH-Provider-Identification (Cloudflare, Google, Quad9)
- Encrypted-DNS-vs-Clear-DNS-Ratio-Analysis
- Bootstrap-DNS-Query-Patterns
## Command & Control (C2) Communication-Patterns
### C2-Channel-Identification
**HTTP-basierte C2-Kommunikation:**
```bash
# Beaconing-Pattern-Detection
tshark -r capture.pcap -T fields -e frame.time_epoch -e ip.dst \
-Y "http" | awk 'script für regelmäßige Intervalle'
```
**Timing-Analysis für Beaconing:**
- Jitter-Analyse bei Sleep-Intervallen
- Callback-Frequency-Patterns
- Network-Outage-Response-Behavior
**Payload-Obfuscation-Techniques:**
- Base64-encoded Commands in HTTP-Bodies
- Steganographie in Bilddateien
- JSON/XML-Structure-Abuse für Command-Transport
### Advanced Persistent Threat (APT) Network-Signatures
**Long-Duration-Connection-Analysis:**
```python
# Langzeit-Verbindungs-Identifikation
def find_persistent_connections(pcap_data):
for session in tcp_sessions:
duration = session.end_time - session.start_time
if duration > timedelta(hours=24):
analyze_session_behavior(session)
```
**Multi-Stage-Payload-Delivery:**
- Initial-Compromise-Vector-Analysis
- Secondary-Payload-Download-Patterns
- Lateral-Movement-Network-Signatures
## Protokoll-Anomalie-Detection-Algorithmen
### Statistical-Baseline-Establishment
**Traffic-Volume-Baselines:**
```python
# Netzwerk-Baseline-Erstellung
def establish_baseline(historical_data):
baseline = {
'avg_bandwidth': calculate_average_bps(historical_data),
'peak_hours': identify_peak_traffic_windows(historical_data),
'protocol_distribution': analyze_protocol_ratios(historical_data)
}
return baseline
```
**Port-Usage-Pattern-Analysis:**
- Unexpected-Port-Combinations
- High-Port-Range-Communication (> 32768)
- Service-Port-Mismatches (HTTP on Port 443 without TLS)
### Machine-Learning-Enhanced-Detection
**Traffic-Classification-Models:**
- Protocol-Identification via Payload-Analysis
- Encrypted-Traffic-Classification
- Anomaly-Score-Calculation für Unknown-Traffic
## Session-Rekonstruktion und Payload-Extraktion
### TCP-Stream-Reassembly
**Bidirectional-Communication-Timeline:**
```bash
# Vollständige Session-Rekonstruktion
mkdir session_analysis
cd session_analysis
# TCP-Streams einzeln extrahieren
for stream in $(tshark -r ../capture.pcap -T fields -e tcp.stream | sort -u); do
tshark -r ../capture.pcap -q -z follow,tcp,raw,$stream > stream_$stream.raw
done
```
**File-Carving aus Network-Streams:**
- HTTP-File-Download-Reconstruction
- Email-Attachment-Extraction via SMTP/POP3
- FTP-Data-Channel-File-Recovery
### Application-Layer-Protocol-Parsing
**Custom-Protocol-Analysis:**
```python
# Proprietary-Protocol-Reverse-Engineering
def analyze_custom_protocol(payload):
# Header-Structure-Identification
if len(payload) > 8:
magic_bytes = payload[:4]
length_field = struct.unpack('>I', payload[4:8])[0]
if validate_structure(magic_bytes, length_field, payload):
return parse_protocol_fields(payload)
```
## Verschlüsselte Protokoll-Forensik
### TLS/SSL-Traffic-Analysis
**Certificate-Chain-Validation:**
```bash
# Certificate-Extraktion aus PCAP
tshark -r capture.pcap -Y "tls.handshake.certificate" \
-T fields -e tls.handshake.certificate > certificates.hex
# Certificate-Parsing
xxd -r -p certificates.hex | openssl x509 -inform DER -text
```
**TLS-Version-Downgrade-Attacks:**
- Forced-SSLv3-Negotiation-Detection
- Weak-Cipher-Suite-Selection-Patterns
- Certificate-Pinning-Bypass-Indicators
### VPN-Traffic-Characterization
**VPN-Protocol-Identification:**
- OpenVPN: UDP Port 1194, specific packet-patterns
- IPSec: ESP (Protocol 50), IKE (UDP 500)
- WireGuard: UDP mit characteristic handshake-patterns
**VPN-Tunnel-Analysis:**
```python
# VPN-Endpoint-Discovery
def identify_vpn_endpoints(pcap_data):
potential_endpoints = []
for packet in pcap_data:
if detect_vpn_signature(packet):
potential_endpoints.append(packet.src_ip)
return analyze_endpoint_patterns(potential_endpoints)
```
## Häufige Herausforderungen und Troubleshooting
### Performance-Optimierung bei großen PCAP-Dateien
**Memory-Management:**
```bash
# Große PCAP-Dateien in kleinere Segmente aufteilen
editcap -c 100000 large_capture.pcap segment.pcap
# Zeitbasierte Segmentierung
editcap -A "2024-01-01 00:00:00" -B "2024-01-01 01:00:00" \
large_capture.pcap hour_segment.pcap
```
**Selective-Filtering:**
```bash
# Nur relevanten Traffic extrahieren
tshark -r large_capture.pcap -w filtered.pcap \
-Y "ip.addr == 192.168.1.100 or dns or http"
```
### False-Positive-Reduction
**Legitimate-Traffic-Whitelisting:**
- Corporate-Application-Signatures
- Known-Good-Certificate-Authorities
- Approved-Remote-Access-Solutions
**Context-Aware-Analysis:**
```python
# Business-Context-Integration
def validate_alert(network_event, business_context):
if is_maintenance_window(network_event.timestamp):
return False
if is_authorized_admin(network_event.source_ip):
return validate_admin_action(network_event)
return True
```
## Praktische Anwendungsszenarien
### Szenario 1: Data Exfiltration Detection
**Ausgangslage:** Verdacht auf Datendiebstahl aus dem Unternehmensnetzwerk
**Analyse-Workflow:**
1. **Baseline-Establishment:** Normale ausgehende Datenvolumen ermitteln
2. **Spike-Detection:** Ungewöhnlich hohe Upload-Aktivitäten identifizieren
3. **Destination-Analysis:** Externe Ziele der Datenübertragungen
4. **Content-Classification:** Art der übertragenen Daten (soweit möglich)
```bash
# Ausgehende Datenvolumen-Analyse
tshark -r capture.pcap -q -z io,stat,300 \
-Y "ip.src == 192.168.0.0/16 and ip.dst != 192.168.0.0/16"
```
### Szenario 2: APT-Lateral-Movement-Investigation
**Ausgangslage:** Kompromittierter Host, Verdacht auf laterale Bewegung
**Detection-Methoden:**
- SMB-Authentication-Patterns (Pass-the-Hash-Attacks)
- RDP-Session-Establishment-Chains
- WMI/PowerShell-Remote-Execution-Signatures
```python
# Lateral-Movement-Timeline-Construction
def construct_movement_timeline(network_data):
timeline = []
for connection in extract_internal_connections(network_data):
if detect_admin_protocols(connection):
timeline.append({
'timestamp': connection.start_time,
'source': connection.src_ip,
'target': connection.dst_ip,
'protocol': connection.protocol,
'confidence': calculate_suspicion_score(connection)
})
return sort_by_timestamp(timeline)
```
### Szenario 3: Malware C2 Communication Analysis
**Ausgangslage:** Identifizierte Malware-Infection, C2-Channel-Mapping erforderlich
**Systematic C2-Analysis:**
1. **Beaconing-Pattern-Identification**
2. **C2-Server-Geolocation**
3. **Command-Structure-Reverse-Engineering**
4. **Kill-Chain-Reconstruction**
```bash
# C2-Communication-Timeline
tshark -r malware_capture.pcap -T fields \
-e frame.time -e ip.src -e ip.dst -e tcp.dstport \
-Y "ip.src == <infected_host>" | \
awk '{print $1, $4}' | sort | uniq -c
```
## Erweiterte Analyse-Techniken
### Protocol-State-Machine-Analysis
**TCP-State-Tracking:**
```python
class TCPStateAnalyzer:
def __init__(self):
self.connections = {}
def process_packet(self, packet):
key = (packet.src_ip, packet.src_port, packet.dst_ip, packet.dst_port)
if key not in self.connections:
self.connections[key] = TCPConnection()
conn = self.connections[key]
conn.update_state(packet.tcp_flags)
if conn.is_anomalous():
self.flag_suspicious_connection(key, conn)
```
**Application-Protocol-State-Validation:**
- HTTP-Request/Response-Pairing-Validation
- DNS-Query/Response-Correlation
- SMTP-Session-Command-Sequence-Analysis
### Geospatial-Network-Analysis
**IP-Geolocation-Correlation:**
```python
# Geographische Anomalie-Detection
def detect_geographic_anomalies(connections):
for conn in connections:
src_country = geolocate_ip(conn.src_ip)
dst_country = geolocate_ip(conn.dst_ip)
if calculate_distance(src_country, dst_country) > 10000: # km
if not is_known_global_service(conn.dst_ip):
flag_suspicious_connection(conn)
```
## Automatisierung und Tool-Integration
### SIEM-Integration
**Log-Format-Standardization:**
```python
# Network-Events zu SIEM-Format
def convert_to_siem_format(network_event):
return {
'timestamp': network_event.time_iso,
'event_type': 'network_connection',
'source_ip': network_event.src_ip,
'destination_ip': network_event.dst_ip,
'protocol': network_event.protocol,
'risk_score': calculate_risk_score(network_event),
'indicators': extract_iocs(network_event)
}
```
### Threat-Intelligence-Integration
**IOC-Matching:**
```bash
# Threat-Feed-Integration
curl -s "https://threatfeed.example.com/api/ips" | \
tee threat_ips.txt
tshark -r capture.pcap -T fields -e ip.dst | \
sort -u | \
grep -f threat_ips.txt
```
## Nächste Schritte und Vertiefung
### Weiterführende Analyse-Techniken
- **Behavioral-Analysis:** Machine-Learning-basierte Anomalie-Detection
- **Graph-Analysis:** Netzwerk-Relationship-Mapping
- **Temporal-Analysis:** Time-Series-basierte Pattern-Recognition
### Spezialisierung-Richtungen
- **Cloud-Network-Forensics:** AWS VPC Flow Logs, Azure NSG Analysis
- **IoT-Network-Analysis:** Constrained-Device-Communication-Patterns
- **Industrial-Network-Security:** SCADA/Modbus-Protocol-Forensics
### Tool-Ecosystem-Erweiterung
- **Zeek (Bro):** Scriptable Network Security Monitor
- **Suricata:** IDS/IPS mit Network-Forensik-Capabilities
- **Moloch:** Full-Packet-Capture und Search-Platform
Die systematische Netzwerkprotokoll-Analyse bildet das Fundament moderner Cyber-Forensik. Durch die Kombination von Deep-Protocol-Knowledge, statistischer Analyse und Threat-Intelligence entsteht ein mächtiges Arsenal für die Aufdeckung und Untersuchung von Cyberangriffen.
**Empfohlene Übungen:**
1. Analysieren Sie einen selbst erzeugten Netzwerk-Capture mit bekanntem "böswilligem" Traffic
2. Implementieren Sie ein automatisiertes C2-Detection-Script
3. Führen Sie eine komplette APT-Simulation durch und dokumentieren Sie die Netzwerk-Artefakte
Die kontinuierliche Weiterentwicklung von Angriffstechniken erfordert permanente Aktualisierung der Analyse-Methoden. Bleiben Sie über aktuelle Threat-Research und neue Protocol-Exploitation-Techniques informiert.

View File

@ -0,0 +1,556 @@
---
title: "Regular Expressions in der Digitalen Forensik: Vom Grundmuster zur Beweisextraktion"
description: "Umfassender Leitfaden für Regex-Anwendungen in der forensischen Analyse: IP-Adressen, E-Mails, Hashes und komplexe Logparser-Patterns für effiziente Beweissammlung"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: intermediate
categories: ["analysis", "automation", "log-analysis"]
tags: ["regex", "pattern-matching", "log-analysis", "data-extraction", "text-processing", "automation", "yara-rules", "grep", "powershell", "python"]
tool_name: "Regular Expressions (Regex)"
related_tools: ["YARA", "Grep", "PowerShell", "Python"]
published: true
---
# Regular Expressions in der Digitalen Forensik: Vom Grundmuster zur Beweisextraktion
Regular Expressions (Regex) sind das Schweizer Taschenmesser der digitalen Forensik. Diese universelle Mustererkennungssprache ermöglicht es Forensikern, komplexe Textsuchen durchzuführen, relevante Daten aus Terabytes von Logs zu extrahieren und Beweise systematisch zu identifizieren. Von der einfachen IP-Adressen-Suche bis zur komplexen Malware-Signaturerstellung - Regex-Kenntnisse unterscheiden oft einen guten von einem großartigen Forensiker.
## Warum Regex in der Forensik unverzichtbar ist
In modernen Untersuchungen konfrontieren uns massive Datenmengen: Gigabytes von Logfiles, Speicherabbilder, Netzwerkverkehr und Dateisysteme mit Millionen von Einträgen. Manuelle Durchsuchung ist unmöglich - hier kommt Regex ins Spiel:
- **Präzise Mustersuche**: Findet spezifische Datenformate (IP-Adressen, E-Mails, Hashes) in unstrukturierten Texten
- **Automatisierung**: Ermöglicht Skripterstellung für wiederkehrende Analysemuster
- **Tool-Integration**: Kernfunktionalität in allen Major-Forensik-Tools
- **Effizienzsteigerung**: Reduziert Analysezeit von Stunden auf Minuten
## Forensik-relevante Regex-Grundlagen
### Grundlegende Metacharakter
```regex
. # Beliebiges Zeichen (außer Newline)
* # 0 oder mehr Wiederholungen des vorherigen Elements
+ # 1 oder mehr Wiederholungen
? # 0 oder 1 Wiederholung (optional)
^ # Zeilenanfang
$ # Zeilenende
[] # Zeichenklasse
() # Gruppierung
| # ODER-Verknüpfung
\ # Escape-Zeichen
```
### Quantifizierer für präzise Treffer
```regex
{n} # Exakt n Wiederholungen
{n,} # Mindestens n Wiederholungen
{n,m} # Zwischen n und m Wiederholungen
{,m} # Maximal m Wiederholungen
```
### Zeichenklassen für strukturierte Daten
```regex
\d # Ziffer (0-9)
\w # Wort-Zeichen (a-z, A-Z, 0-9, _)
\s # Whitespace (Leerzeichen, Tab, Newline)
\D # Nicht-Ziffer
\W # Nicht-Wort-Zeichen
\S # Nicht-Whitespace
[a-z] # Kleinbuchstaben
[A-Z] # Großbuchstaben
[0-9] # Ziffern
[^abc] # Alles außer a, b, c
```
## Forensische Standardmuster
### IP-Adressen (IPv4)
```regex
# Basis-Pattern (weniger präzise)
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
# Präzise IPv4-Validierung
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
# Praktisches Pattern für Log-Analyse
(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)
```
**Anwendungsbeispiel**: Extraktion aller IP-Adressen aus IIS-Logs:
```bash
grep -oE '(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)' access.log | sort | uniq -c | sort -nr
```
### E-Mail-Adressen
```regex
# Einfaches Pattern für schnelle Suche
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
# RFC-konforme E-Mail (vereinfacht)
^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$
# Für Forensik optimiert (weniger strikt)
\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b
```
### Hash-Werte
```regex
# MD5 (32 Hexadezimalzeichen)
\b[a-fA-F0-9]{32}\b
# SHA-1 (40 Hexadezimalzeichen)
\b[a-fA-F0-9]{40}\b
# SHA-256 (64 Hexadezimalzeichen)
\b[a-fA-F0-9]{64}\b
# Universelles Hash-Pattern
\b[a-fA-F0-9]{32,64}\b
```
### Bitcoin-Adressen
```regex
# Legacy Bitcoin-Adressen (P2PKH und P2SH)
\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b
# Bech32 (SegWit) Adressen
\bbc1[a-z0-9]{39,59}\b
# Kombiniert
\b(?:[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39,59})\b
```
### Windows-Dateipfade
```regex
# Vollständiger Windows-Pfad
^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$
# UNC-Pfade
^\\\\[^\\]+\\[^\\]+(?:\\[^\\]*)*$
# Für Log-Parsing (flexibler)
[a-zA-Z]:\\[^"\s<>|]*
```
### Kreditkartennummern
```regex
# Visa (13-19 Ziffern, beginnt mit 4)
4[0-9]{12,18}
# MasterCard (16 Ziffern, beginnt mit 5)
5[1-5][0-9]{14}
# American Express (15 Ziffern, beginnt mit 34 oder 37)
3[47][0-9]{13}
# Universell (mit optionalen Trennzeichen)
(?:\d{4}[-\s]?){3,4}\d{4}
```
## Tool-spezifische Regex-Implementierungen
### PowerShell-Integration
```powershell
# Suche nach IP-Adressen in Eventlogs
Get-WinEvent -LogName Security | Where-Object {
$_.Message -match '\b(?:\d{1,3}\.){3}\d{1,3}\b'
} | Select-Object TimeCreated, Id, Message
# E-Mail-Extraktion aus Speicherabbild
Select-String -Path "memdump.raw" -Pattern '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' -AllMatches
# Hash-Werte aus Malware-Samples
Get-ChildItem -Recurse | Get-FileHash | Where-Object {
$_.Hash -match '^[a-fA-F0-9]{64}$'
}
```
### Grep-Anwendungen
```bash
# Verdächtige ausführbare Dateien
grep -r -E '\.(exe|dll|scr|bat|cmd)$' /mnt/evidence/
# Zeitstempel-Extraktion (ISO 8601)
grep -oE '\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}' application.log
# Base64-kodierte Daten
grep -oE '[A-Za-z0-9+/]{20,}={0,2}' suspicious.txt
# Windows-Ereignis-IDs
grep -E 'Event ID: (4624|4625|4648|4656)' security.log
```
### Python-Implementierung
```python
import re
import hashlib
# IP-Adressen mit Kontext extrahieren
def extract_ips_with_context(text, context_chars=50):
ip_pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
matches = []
for match in re.finditer(ip_pattern, text):
start = max(0, match.start() - context_chars)
end = min(len(text), match.end() + context_chars)
context = text[start:end]
matches.append({
'ip': match.group(),
'position': match.start(),
'context': context
})
return matches
# Malware-Signaturen generieren
def generate_yara_strings(binary_data, min_length=10):
# Suche nach druckbaren ASCII-Strings
ascii_pattern = rb'[ -~]{' + str(min_length).encode() + rb',}'
strings = re.findall(ascii_pattern, binary_data)
yara_strings = []
for i, string in enumerate(strings[:20]): # Erste 20 Strings
# Escape problematische Zeichen
escaped = string.decode('ascii').replace('\\', '\\\\').replace('"', '\\"')
yara_strings.append(f'$s{i} = "{escaped}"')
return yara_strings
```
## YARA-Rules mit Regex
```yara
rule SuspiciousEmailPattern {
strings:
$email = /[a-zA-Z0-9._%+-]+@(tempmail|guerrillamail|10minutemail)\.(com|net|org)/ nocase
$bitcoin = /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/
$ransom_msg = /your files have been encrypted/i
condition:
$email and ($bitcoin or $ransom_msg)
}
rule LogAnalysisPattern {
strings:
$failed_login = /Failed login.*from\s+(\d{1,3}\.){3}\d{1,3}/
$brute_force = /authentication failure.*rhost=(\d{1,3}\.){3}\d{1,3}/
$suspicious_ua = /User-Agent:.*(?:sqlmap|nikto|nmap|masscan)/i
condition:
any of them
}
```
## Performance-Optimierung und Fallstricke
### Catastrophic Backtracking vermeiden
**Problematisch**:
```regex
(a+)+b # Exponentieller Zeitverbrauch bei "aaaa...c"
(.*)* # Verschachtelte Quantifizierer
```
**Optimiert**:
```regex
a+b # Atomare Gruppierung
[^b]*b # Negierte Zeichenklasse statt .*
```
### Anker für Effizienz nutzen
```regex
# Langsam
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
# Schneller mit Wortgrenzen
\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
# Am schnellsten für Zeilensuche
^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$
```
### Compiled Patterns verwenden
```python
import re
# Einmal kompilieren, oft verwenden
ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')
email_pattern = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}')
def analyze_log_file(filepath):
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
ips = ip_pattern.findall(content)
emails = email_pattern.findall(content)
return ips, emails
```
## Praktische Forensik-Szenarien
### Incident Response: Lateral Movement Detection
```bash
# Suche nach PsExec-Aktivitäten
grep -E 'PSEXESVC.*started|PsExec.*\\\\[^\\]+\\' security.log
# Pass-the-Hash Angriffe
grep -E 'Logon Type:\s+9.*NTLM.*[0-9a-fA-F]{32}' security.log
# WMI-basierte Ausführung
grep -E 'WmiPrvSE.*ExecuteShellCommand|wmic.*process.*call.*create' system.log
```
### Malware-Analyse: C2-Kommunikation
```python
# Domain Generation Algorithm (DGA) Detection
dga_pattern = re.compile(r'\b[a-z]{8,20}\.(com|net|org|info)\b')
def detect_suspicious_domains(pcap_text):
# Extrahiere DNS-Queries
dns_pattern = r'DNS.*query.*?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})'
domains = re.findall(dns_pattern, pcap_text)
suspicious = []
for domain in domains:
# Prüfe auf DGA-Charakteristika
if dga_pattern.match(domain.lower()):
# Zusätzliche Heuristiken
vowel_ratio = len(re.findall(r'[aeiou]', domain.lower())) / len(domain)
if vowel_ratio < 0.2: # Wenige Vokale = verdächtig
suspicious.append(domain)
return suspicious
```
### Data Exfiltration: Ungewöhnliche Datenübertragungen
```regex
# Base64-kodierte Daten in URLs
[?&]data=([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?
# DNS-Tunneling (ungewöhnlich lange Subdomains)
\b[a-z0-9]{20,}\.[a-z0-9.-]+\.[a-z]{2,}\b
# Hex-kodierte Dateninhalte
[?&]payload=[0-9a-fA-F]{40,}
```
## Debugging und Testing
### Online-Tools für Regex-Entwicklung
1. **regex101.com**: Interaktive Regex-Entwicklung mit Erklärungen
2. **regexr.com**: Visuelle Regex-Darstellung
3. **regexpal.com**: Schnelle Tests ohne Anmeldung
### Regex-Validierung in der Praxis
```python
import re
def validate_regex_pattern(pattern, test_cases):
"""
Validiert Regex-Pattern gegen bekannte Test-Cases
"""
try:
compiled = re.compile(pattern)
except re.error as e:
return False, f"Regex-Syntax-Fehler: {e}"
results = []
for test_input, expected in test_cases:
match = compiled.search(test_input)
found = match.group() if match else None
results.append({
'input': test_input,
'expected': expected,
'found': found,
'correct': found == expected
})
return True, results
# Test-Cases für IP-Pattern
ip_tests = [
('192.168.1.1', '192.168.1.1'),
('999.999.999.999', None), # Ungültige IP
('text 10.0.0.1 more text', '10.0.0.1'),
('no.ip.here', None)
]
pattern = r'\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b'
valid, results = validate_regex_pattern(pattern, ip_tests)
```
## Häufige Fehler und Lösungen
### Problem: Gierige vs. nicht-gierige Quantifizierer
```regex
# Problematisch: Gierig
<.*> # Matched "<tag>content</tag>" komplett
# Lösung: Nicht-gierig
<.*?> # Matched nur "<tag>"
# Alternative: Spezifisch
<[^>]*> # Matched keine ">" innerhalb
```
### Problem: Unbeabsichtigte Metacharakter
```regex
# Falsch: . als Literalzeichen gemeint
192.168.1.1 # Matched auch "192x168x1x1"
# Richtig: Escape von Metacharaktern
192\.168\.1\.1 # Matched nur echte IP
```
### Problem: Fehlende Wortgrenzen
```regex
# Problematisch: Matcht Teilstrings
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # Matched "1192.168.1.10"
# Lösung: Wortgrenzen verwenden
\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b # Nur vollständige IPs
```
## Integration in Forensik-Workflows
### Automatisierte Triage-Scripts
```bash
#!/bin/bash
# forensic_triage.sh - Automatisierte erste Analyse
LOG_DIR="/evidence/logs"
OUTPUT_DIR="/analysis/regex_results"
# IP-Adressen extrahieren und häufigste finden
echo "=== IP-Analyse ===" > $OUTPUT_DIR/summary.txt
find $LOG_DIR -name "*.log" -exec grep -h -oE '\b(?:\d{1,3}\.){3}\d{1,3}\b' {} \; | \
sort | uniq -c | sort -nr | head -20 >> $OUTPUT_DIR/summary.txt
# E-Mail-Adressen sammeln
echo -e "\n=== E-Mail-Adressen ===" >> $OUTPUT_DIR/summary.txt
find $LOG_DIR -name "*.log" -exec grep -h -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' {} \; | \
sort | uniq >> $OUTPUT_DIR/summary.txt
# Verdächtige Prozessnamen
echo -e "\n=== Verdächtige Prozesse ===" >> $OUTPUT_DIR/summary.txt
find $LOG_DIR -name "*.log" -exec grep -h -iE '(powershell|cmd|wmic|psexec|mimikatz)' {} \; | \
head -50 >> $OUTPUT_DIR/summary.txt
```
### PowerShell-Module für wiederkehrende Aufgaben
```powershell
function Get-ForensicPatterns {
param(
[string]$Path,
[string[]]$Patterns = @(
'\b(?:\d{1,3}\.){3}\d{1,3}\b', # IP-Adressen
'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', # E-Mails
'\b[a-fA-F0-9]{32,64}\b' # Hash-Werte
)
)
$results = @{}
foreach ($pattern in $Patterns) {
$matches = Select-String -Path $Path -Pattern $pattern -AllMatches
$results[$pattern] = $matches | ForEach-Object {
[PSCustomObject]@{
File = $_.Filename
Line = $_.LineNumber
Match = $_.Matches.Value
Context = $_.Line
}
}
}
return $results
}
```
## Weiterführende Techniken
### Lookahead und Lookbehind
```regex
# Positive Lookahead: Password gefolgt von Ziffer
password(?=.*\d)
# Negative Lookahead: IP nicht in private ranges
(?!(?:10\.|192\.168\.|172\.(?:1[6-9]|2[0-9]|3[01])\.))(?:\d{1,3}\.){3}\d{1,3}
# Positive Lookbehind: Zahl nach "Port:"
(?<=Port:)\d+
# Negative Lookbehind: Nicht nach "Comment:"
(?<!Comment:).+@.+\..+
```
### Named Capture Groups
```python
import re
# Strukturierte Log-Parsing
log_pattern = re.compile(
r'(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) '
r'\[(?P<level>\w+)\] '
r'(?P<source>\w+): '
r'(?P<message>.*)'
)
def parse_log_entry(line):
match = log_pattern.match(line)
if match:
return match.groupdict()
return None
# Verwendung
log_line = "2024-01-15 14:30:25 [ERROR] auth: Failed login from 192.168.1.100"
parsed = parse_log_entry(log_line)
# Result: {'timestamp': '2024-01-15 14:30:25', 'level': 'ERROR',
# 'source': 'auth', 'message': 'Failed login from 192.168.1.100'}
```
## Nächste Schritte
Nach diesem umfassenden Überblick können Sie:
1. **Praktische Übung**: Implementieren Sie die vorgestellten Patterns in Ihren aktuellen Untersuchungen
2. **Tool-Integration**: Integrieren Sie Regex in Ihre bevorzugten Forensik-Tools
3. **Automatisierung**: Entwickeln Sie Scripts für wiederkehrende Analysemuster
4. **Spezialisierung**: Vertiefen Sie sich in tool-spezifische Regex-Implementierungen
5. **Community**: Teilen Sie Ihre Patterns und lernen Sie von anderen Forensikern
### Weiterführende Ressourcen
- **SANS Regex Cheat Sheet**: Kompakte Referenz für Forensiker
- **RegexBuddy**: Professionelle Regex-Entwicklungsumgebung
- **Python re-Modul Dokumentation**: Detaillierte Syntax-Referenz
- **YARA-Rules Repository**: Sammlung forensik-relevanter Regex-Patterns
Regular Expressions sind ein mächtiges Werkzeug, das Zeit spart und die Präzision forensischer Analysen erhöht. Die Investition in solide Regex-Kenntnisse zahlt sich in jeder Untersuchung aus und ermöglicht es, komplexe Muster zu erkennen, die manuell übersehen werden würden.

View File

@ -0,0 +1,770 @@
---
title: "SQL in der digitalen Forensik: Von SQLite-Datenbanken zur Timeline-Analyse"
description: "Umfassender Leitfaden für SQL-basierte Forensik-Analysen: SQLite-Datenbanken untersuchen, Timeline-Rekonstruktion durchführen, mobile App-Daten analysieren und komplexe Korrelationen aufdecken."
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: intermediate
categories: ["analysis", "configuration", "case-study"]
tags: ["sqlite-viewer", "correlation-engine", "mobile-app-data", "browser-history", "data-extraction", "timeline-queries", "join-operations", "aggregate-analysis", "wal-analysis", "python-integration"]
tool_name: "SQL"
related_tools: ["DB Browser for SQLite", "Autopsy", "Cellebrite UFED"]
published: true
---
# SQL in der digitalen Forensik: Von SQLite-Datenbanken zur Timeline-Analyse
SQL (Structured Query Language) ist eine der mächtigsten und unterschätztesten Fähigkeiten in der modernen digitalen Forensik. Während viele Ermittler auf GUI-basierte Tools setzen, ermöglicht SQL direkten Zugriff auf Rohdaten und komplexe Analysen, die mit herkömmlichen Tools unmöglich wären.
## Warum SQL in der Forensik unverzichtbar ist
### SQLite dominiert die mobile Forensik
- **WhatsApp-Chats**: Nachrichten, Metadaten, gelöschte Inhalte
- **Browser-History**: Zeitstempel, Besuchshäufigkeit, Suchverläufe
- **App-Daten**: Standortdaten, Nutzerverhalten, Cache-Inhalte
- **System-Logs**: Verbindungsprotokoll, Fehleraufzeichnungen
### Vorteile gegenüber GUI-Tools
- **Flexibilität**: Komplexe Abfragen jenseits vordefinierter Filter
- **Performance**: Direkte Datenbankzugriffe ohne Interface-Overhead
- **Automatisierung**: Skript-basierte Analysen für wiederkehrende Aufgaben
- **Tiefe**: Zugriff auf Metadaten und versteckte Tabellenstrukturen
## Grundlagen: SQLite-Struktur verstehen
### Datenbank-Anatomie in der Forensik
```sql
-- Tabellen einer WhatsApp-Datenbank analysieren
.tables
-- Tabellenstruktur untersuchen
.schema messages
-- Beispiel-Output:
CREATE TABLE messages (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
key_remote_jid TEXT,
key_from_me INTEGER,
key_id TEXT,
status INTEGER,
needs_push INTEGER,
data TEXT,
timestamp INTEGER,
media_url TEXT,
media_mime_type TEXT,
media_wa_type INTEGER,
media_size INTEGER,
latitude REAL,
longitude REAL
);
```
### SQLite-spezifische Forensik-Herausforderungen
**WAL-Mode (Write-Ahead Logging)**:
```sql
-- WAL-Datei auf nicht-committete Transaktionen prüfen
PRAGMA journal_mode;
-- Temporäre Daten in WAL-Datei finden
-- (Erfordert spezielle Tools wie sqlitewalreader)
```
**Gelöschte Records**:
```sql
-- Freespace-Analyse für gelöschte Daten
-- Hinweis: Erfordert spezialisierte Recovery-Tools
```
## Timeline-Rekonstruktion: Der Forensik-Klassiker
### Grundlegende Timeline-Abfrage
```sql
-- Chronologische Ereignisübersicht erstellen
SELECT
datetime(timestamp/1000, 'unixepoch', 'localtime') as ereignis_zeit,
CASE
WHEN key_from_me = 1 THEN 'Ausgehend'
ELSE 'Eingehend'
END as richtung,
key_remote_jid as kontakt,
substr(data, 1, 50) || '...' as nachricht_preview
FROM messages
WHERE timestamp > 0
ORDER BY timestamp DESC
LIMIT 100;
```
### Erweiterte Timeline mit Kontextinformationen
```sql
-- Timeline mit Geolocation und Media-Daten
SELECT
datetime(m.timestamp/1000, 'unixepoch', 'localtime') as zeitstempel,
c.display_name as kontakt_name,
CASE
WHEN m.key_from_me = 1 THEN '→ Gesendet'
ELSE '← Empfangen'
END as richtung,
CASE
WHEN m.media_wa_type IS NOT NULL THEN 'Media: ' || m.media_mime_type
ELSE 'Text'
END as nachricht_typ,
CASE
WHEN m.latitude IS NOT NULL THEN
'Standort: ' || ROUND(m.latitude, 6) || ', ' || ROUND(m.longitude, 6)
ELSE substr(m.data, 1, 100)
END as inhalt
FROM messages m
LEFT JOIN wa_contacts c ON m.key_remote_jid = c.jid
WHERE m.timestamp BETWEEN
strftime('%s', '2024-01-01') * 1000 AND
strftime('%s', '2024-01-31') * 1000
ORDER BY m.timestamp;
```
## Kommunikations-Analyse: Soziale Netzwerke aufdecken
### Häufigste Kontakte identifizieren
```sql
-- Top-Kommunikationspartner nach Nachrichtenvolumen
SELECT
c.display_name,
m.key_remote_jid,
COUNT(*) as nachrichten_gesamt,
SUM(CASE WHEN m.key_from_me = 1 THEN 1 ELSE 0 END) as gesendet,
SUM(CASE WHEN m.key_from_me = 0 THEN 1 ELSE 0 END) as empfangen,
MIN(datetime(m.timestamp/1000, 'unixepoch', 'localtime')) as erster_kontakt,
MAX(datetime(m.timestamp/1000, 'unixepoch', 'localtime')) as letzter_kontakt
FROM messages m
LEFT JOIN wa_contacts c ON m.key_remote_jid = c.jid
GROUP BY m.key_remote_jid
HAVING nachrichten_gesamt > 10
ORDER BY nachrichten_gesamt DESC;
```
### Kommunikationsmuster-Analyse
```sql
-- Tägliche Aktivitätsmuster
SELECT
strftime('%H', timestamp/1000, 'unixepoch', 'localtime') as stunde,
COUNT(*) as nachrichten_anzahl,
AVG(length(data)) as durchschnittliche_laenge
FROM messages
WHERE timestamp > 0 AND data IS NOT NULL
GROUP BY stunde
ORDER BY stunde;
```
```sql
-- Verdächtige Aktivitätsspitzen identifizieren
WITH hourly_stats AS (
SELECT
date(timestamp/1000, 'unixepoch', 'localtime') as tag,
strftime('%H', timestamp/1000, 'unixepoch', 'localtime') as stunde,
COUNT(*) as nachrichten_pro_stunde
FROM messages
WHERE timestamp > 0
GROUP BY tag, stunde
),
avg_per_hour AS (
SELECT stunde, AVG(nachrichten_pro_stunde) as durchschnitt
FROM hourly_stats
GROUP BY stunde
)
SELECT
h.tag,
h.stunde,
h.nachrichten_pro_stunde,
a.durchschnitt,
ROUND((h.nachrichten_pro_stunde - a.durchschnitt) / a.durchschnitt * 100, 2) as abweichung_prozent
FROM hourly_stats h
JOIN avg_per_hour a ON h.stunde = a.stunde
WHERE h.nachrichten_pro_stunde > a.durchschnitt * 2
ORDER BY abweichung_prozent DESC;
```
## Browser-Forensik: Digitale Spuren verfolgen
### Chrome/Chromium History-Analyse
```sql
-- Browser-History mit Besuchshäufigkeit
SELECT
url,
title,
visit_count,
datetime(last_visit_time/1000000-11644473600, 'unixepoch', 'localtime') as letzter_besuch,
CASE
WHEN typed_count > 0 THEN 'Direkt eingegeben'
ELSE 'Über Link/Verlauf'
END as zugriff_art
FROM urls
WHERE last_visit_time > 0
ORDER BY last_visit_time DESC
LIMIT 100;
```
### Such-Verlauf analysieren
```sql
-- Google-Suchen aus Browser-History extrahieren
SELECT
datetime(last_visit_time/1000000-11644473600, 'unixepoch', 'localtime') as suchzeit,
CASE
WHEN url LIKE '%google.com/search%' THEN
replace(substr(url, instr(url, 'q=') + 2,
case when instr(substr(url, instr(url, 'q=') + 2), '&') > 0
then instr(substr(url, instr(url, 'q=') + 2), '&') - 1
else length(url) end), '+', ' ')
ELSE 'Andere Suchmaschine'
END as suchbegriff,
url
FROM urls
WHERE url LIKE '%search%' OR url LIKE '%q=%'
ORDER BY last_visit_time DESC;
```
## Anomalie-Erkennung mit SQL
### Ungewöhnliche Datei-Zugriffe identifizieren
```sql
-- Dateizugriffe außerhalb der Arbeitszeiten
WITH file_access AS (
SELECT
datetime(timestamp, 'unixepoch', 'localtime') as zugriffszeit,
strftime('%H', timestamp, 'unixepoch', 'localtime') as stunde,
strftime('%w', timestamp, 'unixepoch', 'localtime') as wochentag,
file_path,
action_type
FROM file_access_logs
)
SELECT *
FROM file_access
WHERE (
stunde < '08' OR stunde > '18' OR -- Außerhalb 8-18 Uhr
wochentag IN ('0', '6') -- Wochenende
) AND action_type IN ('read', 'write', 'delete')
ORDER BY zugriffszeit DESC;
```
### Datenexfiltration-Indikatoren
```sql
-- Große Dateiübertragungen in kurzen Zeiträumen
SELECT
datetime(transfer_start, 'unixepoch', 'localtime') as start_zeit,
SUM(file_size) as gesamt_bytes,
COUNT(*) as anzahl_dateien,
destination_ip,
GROUP_CONCAT(DISTINCT file_extension) as dateitypen
FROM network_transfers
WHERE transfer_start BETWEEN
strftime('%s', 'now', '-7 days') AND strftime('%s', 'now')
GROUP BY
date(transfer_start, 'unixepoch', 'localtime'),
strftime('%H', transfer_start, 'unixepoch', 'localtime'),
destination_ip
HAVING gesamt_bytes > 100000000 -- > 100MB
ORDER BY gesamt_bytes DESC;
```
## Erweiterte Techniken: Window Functions und CTEs
### Sliding Window-Analyse für Ereigniskorrelation
```sql
-- Ereignisse in 5-Minuten-Fenstern korrelieren
WITH event_windows AS (
SELECT
datetime(timestamp, 'unixepoch', 'localtime') as ereigniszeit,
event_type,
user_id,
LAG(timestamp, 1) OVER (PARTITION BY user_id ORDER BY timestamp) as prev_timestamp,
LEAD(timestamp, 1) OVER (PARTITION BY user_id ORDER BY timestamp) as next_timestamp
FROM security_events
ORDER BY timestamp
)
SELECT
ereigniszeit,
event_type,
user_id,
CASE
WHEN (timestamp - prev_timestamp) < 300 THEN 'Schnelle Aufeinanderfolge'
WHEN (next_timestamp - timestamp) < 300 THEN 'Vor schnellem Event'
ELSE 'Isoliert'
END as ereignis_kontext
FROM event_windows;
```
### Temporäre Anomalie-Scores
```sql
-- Anomalie-Score basierend auf Abweichung vom Normalverhalten
WITH user_baseline AS (
SELECT
user_id,
AVG(daily_logins) as avg_logins,
STDEV(daily_logins) as stddev_logins
FROM (
SELECT
user_id,
date(login_time, 'unixepoch', 'localtime') as login_date,
COUNT(*) as daily_logins
FROM user_logins
WHERE login_time > strftime('%s', 'now', '-30 days')
GROUP BY user_id, login_date
)
GROUP BY user_id
HAVING COUNT(*) > 7 -- Mindestens 7 Tage Daten
),
current_behavior AS (
SELECT
user_id,
date(login_time, 'unixepoch', 'localtime') as login_date,
COUNT(*) as daily_logins
FROM user_logins
WHERE login_time > strftime('%s', 'now', '-7 days')
GROUP BY user_id, login_date
)
SELECT
c.user_id,
c.login_date,
c.daily_logins,
b.avg_logins,
ROUND(ABS(c.daily_logins - b.avg_logins) / b.stddev_logins, 2) as anomalie_score
FROM current_behavior c
JOIN user_baseline b ON c.user_id = b.user_id
WHERE anomalie_score > 2.0 -- Mehr als 2 Standardabweichungen
ORDER BY anomalie_score DESC;
```
## Python-Integration für Automatisierung
### SQLite-Forensik mit Python
```python
import sqlite3
import pandas as pd
from datetime import datetime
import matplotlib.pyplot as plt
class ForensicSQLAnalyzer:
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path)
self.conn.row_factory = sqlite3.Row
def extract_timeline(self, start_date=None, end_date=None):
"""Timeline-Extraktion mit Datumsfilterung"""
query = """
SELECT
datetime(timestamp/1000, 'unixepoch', 'localtime') as timestamp,
event_type,
details,
user_context
FROM events
WHERE 1=1
"""
params = []
if start_date:
query += " AND timestamp >= ?"
params.append(int(start_date.timestamp() * 1000))
if end_date:
query += " AND timestamp <= ?"
params.append(int(end_date.timestamp() * 1000))
query += " ORDER BY timestamp"
return pd.read_sql_query(query, self.conn, params=params)
def communication_analysis(self):
"""Kommunikationsmuster analysieren"""
query = """
SELECT
contact_id,
COUNT(*) as message_count,
AVG(message_length) as avg_length,
MIN(timestamp) as first_contact,
MAX(timestamp) as last_contact
FROM messages
GROUP BY contact_id
HAVING message_count > 5
ORDER BY message_count DESC
"""
return pd.read_sql_query(query, self.conn)
def detect_anomalies(self, threshold=2.0):
"""Statistische Anomalie-Erkennung"""
query = """
WITH daily_stats AS (
SELECT
date(timestamp, 'unixepoch', 'localtime') as day,
COUNT(*) as daily_events
FROM events
GROUP BY day
),
stats AS (
SELECT
AVG(daily_events) as mean_events,
STDEV(daily_events) as stddev_events
FROM daily_stats
)
SELECT
d.day,
d.daily_events,
s.mean_events,
ABS(d.daily_events - s.mean_events) / s.stddev_events as z_score
FROM daily_stats d, stats s
WHERE z_score > ?
ORDER BY z_score DESC
"""
return pd.read_sql_query(query, self.conn, params=[threshold])
def export_findings(self, filename):
"""Ermittlungsergebnisse exportieren"""
timeline = self.extract_timeline()
comms = self.communication_analysis()
anomalies = self.detect_anomalies()
with pd.ExcelWriter(filename) as writer:
timeline.to_excel(writer, sheet_name='Timeline', index=False)
comms.to_excel(writer, sheet_name='Communications', index=False)
anomalies.to_excel(writer, sheet_name='Anomalies', index=False)
# Verwendung
analyzer = ForensicSQLAnalyzer('/path/to/evidence.db')
findings = analyzer.export_findings('investigation_findings.xlsx')
```
## Häufige Fallstricke und Best Practices
### Datenintegrität sicherstellen
```sql
-- Konsistenz-Checks vor Analyse
SELECT
'Null Timestamps' as issue_type,
COUNT(*) as count
FROM messages
WHERE timestamp IS NULL OR timestamp = 0
UNION ALL
SELECT
'Missing Contact Info' as issue_type,
COUNT(*) as count
FROM messages m
LEFT JOIN wa_contacts c ON m.key_remote_jid = c.jid
WHERE c.jid IS NULL;
```
### Performance-Optimierung
```sql
-- Index für häufige Abfragen erstellen
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
ON messages(timestamp);
CREATE INDEX IF NOT EXISTS idx_messages_contact_timestamp
ON messages(key_remote_jid, timestamp);
-- Query-Performance analysieren
EXPLAIN QUERY PLAN
SELECT * FROM messages
WHERE timestamp BETWEEN ? AND ?
ORDER BY timestamp;
```
### Forensische Dokumentation
```sql
-- Metadaten für Gerichtsverwertbarkeit dokumentieren
SELECT
'Database Schema Version' as info_type,
user_version as value
FROM pragma_user_version
UNION ALL
SELECT
'Last Modified',
datetime(mtime, 'unixepoch', 'localtime')
FROM pragma_file_control;
```
## Spezialisierte Forensik-Szenarien
### Mobile App-Forensik: Instagram-Datenbank
```sql
-- Instagram-Nachrichten mit Medien-Metadaten
SELECT
datetime(m.timestamp/1000, 'unixepoch', 'localtime') as nachricht_zeit,
u.username as absender,
CASE
WHEN m.item_type = 1 THEN 'Text: ' || m.text
WHEN m.item_type = 2 THEN 'Bild: ' || mi.media_url
WHEN m.item_type = 3 THEN 'Video: ' || mi.media_url
ELSE 'Anderer Typ: ' || m.item_type
END as inhalt,
m.thread_key as chat_id
FROM direct_messages m
LEFT JOIN users u ON m.user_id = u.pk
LEFT JOIN media_items mi ON m.media_id = mi.id
WHERE m.timestamp > 0
ORDER BY m.timestamp DESC;
```
### Incident Response: Systemprotokoll-Korrelation
```sql
-- Korrelation zwischen Login-Events und Netzwerk-Aktivität
WITH suspicious_logins AS (
SELECT
login_time,
user_id,
source_ip,
login_time + 3600 as investigation_window -- 1 Stunde nach Login
FROM login_events
WHERE source_ip NOT LIKE '192.168.%' -- Externe IPs
AND login_time > strftime('%s', 'now', '-7 days')
),
network_activity AS (
SELECT
connection_time,
source_ip,
destination_ip,
bytes_transferred,
protocol
FROM network_connections
)
SELECT
datetime(sl.login_time, 'unixepoch', 'localtime') as verdaechtiger_login,
sl.user_id,
sl.source_ip as login_ip,
COUNT(na.connection_time) as netzwerk_aktivitaeten,
SUM(na.bytes_transferred) as gesamt_daten_bytes,
GROUP_CONCAT(DISTINCT na.destination_ip) as ziel_ips
FROM suspicious_logins sl
LEFT JOIN network_activity na ON
na.connection_time BETWEEN sl.login_time AND sl.investigation_window
AND na.source_ip = sl.source_ip
GROUP BY sl.login_time, sl.user_id, sl.source_ip
HAVING netzwerk_aktivitaeten > 0
ORDER BY gesamt_daten_bytes DESC;
```
## Erweiterte WAL-Analyse und Recovery
### WAL-Datei Untersuchung
```sql
-- WAL-Mode Status prüfen
PRAGMA journal_mode;
PRAGMA wal_checkpoint;
-- Uncommitted transactions in WAL identifizieren
-- Hinweis: Erfordert spezielle Tools oder Hex-Editor
-- Zeigt Konzept für manuelle Analyse
SELECT
name,
rootpage,
sql
FROM sqlite_master
WHERE type = 'table'
ORDER BY name;
```
### Gelöschte Daten-Recovery
```python
# Python-Script für erweiterte SQLite-Recovery
import sqlite3
import struct
import os
class SQLiteForensics:
def __init__(self, db_path):
self.db_path = db_path
self.page_size = self.get_page_size()
def get_page_size(self):
"""SQLite Page-Size ermitteln"""
with open(self.db_path, 'rb') as f:
f.seek(16) # Page size offset
return struct.unpack('>H', f.read(2))[0]
def analyze_freespace(self):
"""Freespace auf gelöschte Records analysieren"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Freespace-Informationen sammeln
cursor.execute("PRAGMA freelist_count;")
free_pages = cursor.fetchone()[0]
cursor.execute("PRAGMA page_count;")
total_pages = cursor.fetchone()[0]
recovery_potential = {
'total_pages': total_pages,
'free_pages': free_pages,
'recovery_potential': f"{(free_pages/total_pages)*100:.2f}%"
}
conn.close()
return recovery_potential
def extract_unallocated(self):
"""Unallocated Space für Recovery extrahieren"""
# Vereinfachtes Beispiel - echte Implementation erfordert
# detaillierte SQLite-Interna-Kenntnisse
unallocated_data = []
with open(self.db_path, 'rb') as f:
file_size = os.path.getsize(self.db_path)
pages = file_size // self.page_size
for page_num in range(1, pages + 1):
f.seek((page_num - 1) * self.page_size)
page_data = f.read(self.page_size)
# Suche nach Text-Patterns in Freespace
# (Vereinfacht - echte Recovery ist komplexer)
if b'WhatsApp' in page_data or b'@' in page_data:
unallocated_data.append({
'page': page_num,
'potential_data': page_data[:100] # Erste 100 Bytes
})
return unallocated_data
# Verwendung für Recovery-Assessment
forensics = SQLiteForensics('/path/to/damaged.db')
recovery_info = forensics.analyze_freespace()
print(f"Recovery-Potenzial: {recovery_info['recovery_potential']}")
```
## Compliance und Rechtssicherheit
### Audit-Trail erstellen
```sql
-- Forensische Dokumentation aller durchgeführten Abfragen
CREATE TABLE IF NOT EXISTS forensic_audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
investigator TEXT,
query_type TEXT,
sql_query TEXT,
affected_rows INTEGER,
case_number TEXT,
notes TEXT
);
-- Beispiel-Eintrag
INSERT INTO forensic_audit_log
(investigator, query_type, sql_query, affected_rows, case_number, notes)
VALUES
('Max Mustermann', 'TIMELINE_EXTRACTION',
'SELECT * FROM messages WHERE timestamp BETWEEN ? AND ?',
1247, 'CASE-2024-001',
'Timeline-Extraktion für Zeitraum 01.01.2024 - 31.01.2024');
```
### Hash-Verifikation implementieren
```python
import hashlib
import sqlite3
def verify_database_integrity(db_path, expected_hash=None):
"""Datenbank-Integrität durch Hash-Verifikation prüfen"""
# SHA-256 Hash der Datenbankdatei
sha256_hash = hashlib.sha256()
with open(db_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
current_hash = sha256_hash.hexdigest()
# Zusätzlich: Struktureller Integritäts-Check
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
cursor.execute("PRAGMA integrity_check;")
integrity_result = cursor.fetchall()
is_structurally_intact = integrity_result == [('ok',)]
except Exception as e:
is_structurally_intact = False
integrity_result = [f"Error: {str(e)}"]
finally:
conn.close()
return {
'file_hash': current_hash,
'hash_matches': current_hash == expected_hash if expected_hash else None,
'structurally_intact': is_structurally_intact,
'integrity_details': integrity_result,
'verified_at': datetime.now().isoformat()
}
# Chain of Custody dokumentieren
def log_database_access(db_path, investigator, purpose):
"""Datenbankzugriff für Chain of Custody protokollieren"""
verification = verify_database_integrity(db_path)
log_entry = {
'timestamp': datetime.now().isoformat(),
'investigator': investigator,
'database_path': db_path,
'access_purpose': purpose,
'pre_access_hash': verification['file_hash'],
'database_integrity': verification['structurally_intact']
}
# Log in separater Audit-Datei speichern
with open('forensic_access_log.json', 'a') as log_file:
json.dump(log_entry, log_file)
log_file.write('\n')
return log_entry
```
## Fazit und Weiterführende Ressourcen
SQL in der digitalen Forensik ist mehr als nur Datenbankabfragen - es ist ein mächtiges Werkzeug für:
- **Timeline-Rekonstruktion** mit präziser zeitlicher Korrelation
- **Kommunikationsanalyse** für soziale Netzwerk-Aufklärung
- **Anomalie-Erkennung** durch statistische Analyse
- **Automatisierung** wiederkehrender Untersuchungsschritte
- **Tiefe Datenextraktion** jenseits GUI-Limitationen
### Nächste Schritte
1. **Praktische Übung**: Beginnen Sie mit einfachen WhatsApp-Datenbank-Analysen
2. **Tool-Integration**: Kombinieren Sie SQL mit Python für erweiterte Analysen
3. **Spezialisierung**: Vertiefen Sie mobile-spezifische oder Browser-Forensik
4. **Automation**: Entwickeln Sie wiederverwendbare SQL-Scripts für häufige Szenarien
5. **Rechtssicherheit**: Implementieren Sie Audit-Trails und Hash-Verifikation
### Empfohlene Tools
- **DB Browser for SQLite**: GUI für interaktive Exploration
- **SQLiteStudio**: Erweiterte SQLite-Verwaltung
- **Python sqlite3**: Programmbasierte Automatisierung
- **Autopsy**: Integration in forensische Workflows
- **Cellebrite UFED**: Mobile Forensik mit SQL-Export
Die Kombination aus SQL-Kenntnissen und forensischem Verständnis macht moderne Ermittler zu hocheffizienten Datenanalytikern. In einer Welt zunehmender Datenmengen wird diese Fähigkeit zum entscheidenden Wettbewerbsvorteil.

View File

@ -0,0 +1,601 @@
---
title: "Timeline-Analyse & Event-Korrelation: Methodische Rekonstruktion forensischer Ereignisse"
description: "Umfassende Anleitung zur systematischen Timeline-Erstellung aus heterogenen Datenquellen, Super-Timeline-Processing und Advanced-Correlation-Techniken für komplexe Incident-Response-Szenarien."
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
last_updated: 2025-08-10
difficulty: advanced
categories: ["analysis", "methodology", "incident-response"]
tags: ["timeline-correlation", "event-sequencing", "temporal-analysis", "super-timeline", "pivot-points", "behavioral-patterns", "anomaly-detection", "anti-forensics-detection", "incident-response", "log2timeline", "plaso"]
tool_name: "Timeline Analysis & Event Correlation"
related_tools: ["Autopsy", "Volatility", "Wireshark", "SIFT Workstation"]
published: true
---
# Timeline-Analyse & Event-Korrelation: Methodische Rekonstruktion forensischer Ereignisse
Timeline-Analyse bildet das Rückgrat moderner forensischer Untersuchungen und ermöglicht die chronologische Rekonstruktion von Ereignissen aus heterogenen digitalen Artefakten. Diese methodische Herangehensweise korreliert zeitbasierte Evidenz für präzise Incident-Response und belastbare Beweisführung.
## Grundlagen der forensischen Timeline-Analyse
### Was ist Timeline-Analyse?
Timeline-Analyse ist die systematische Korrelation zeitbasierter Artefakte aus verschiedenen digitalen Quellen zur Rekonstruktion von Ereignissequenzen. Sie ermöglicht Forensikern, das "Was", "Wann", "Wo" und "Wie" von Sicherheitsvorfällen zu verstehen.
**Kernprinzipien:**
- **Chronologische Ordnung**: Alle Ereignisse werden in temporaler Reihenfolge arrangiert
- **Multi-Source-Integration**: Daten aus verschiedenen Systemen werden vereint
- **Zeitstempel-Normalisierung**: UTC-Konvertierung für einheitliche Referenz
- **Korrelationsbasierte Analyse**: Zusammenhänge zwischen scheinbar unabhängigen Events
### Typologie forensischer Zeitstempel
**MAC-Times (Modified, Accessed, Created)**
```
Filesystem-Timestamps:
- $STANDARD_INFORMATION (SI) - NTFS-Metadaten
- $FILE_NAME (FN) - Directory-Entry-Timestamps
- Born Date - Erste Erstellung im Filesystem
- $USNJrnl - Change Journal Entries
```
**Registry-Timestamps**
```
Windows Registry:
- Key Last Write Time - Letzte Modifikation
- Value Creation Time - Wert-Erstellung
- Hive Load Time - Registry-Hive-Mounting
```
**Event-Log-Timestamps**
```
Windows Event Logs:
- TimeCreated - Event-Generierung
- TimeWritten - Log-Persistierung
- CorrelationActivityID - Cross-System-Tracking
```
## Super-Timeline-Erstellung: Methodisches Vorgehen
### Phase 1: Artefakt-Akquisition und Preprocessing
**Datenquellen-Inventar erstellen:**
```bash
# Filesystem-Timeline mit fls
fls -r -p -m /mnt/evidence/image.dd > filesystem_timeline.body
# Registry-Timeline mit regtime
regtime.py -r /mnt/evidence/registry/ > registry_timeline.csv
# Event-Log-Extraktion mit python-evtx
evtx_dump.py Security.evtx > security_events.xml
```
**Memory-Artefakte integrieren:**
```bash
# Volatility Timeline-Generierung
vol.py -f memory.vmem --profile=Win10x64 timeliner > memory_timeline.csv
# Process-Timeline mit detailed Metadata
vol.py -f memory.vmem --profile=Win10x64 pslist -v > process_details.txt
```
### Phase 2: Zeitstempel-Normalisierung und UTC-Konvertierung
**Timezone-Handling:**
```python
# Python-Script für Timezone-Normalisierung
import datetime
import pytz
def normalize_timestamp(timestamp_str, source_timezone):
"""
Konvertiert lokale Timestamps zu UTC für einheitliche Timeline
"""
local_tz = pytz.timezone(source_timezone)
dt = datetime.datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
localized_dt = local_tz.localize(dt)
utc_dt = localized_dt.astimezone(pytz.utc)
return utc_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
```
**Anti-Timestomp-Detection:**
```bash
# Timestomp-Anomalien identifizieren
analyzeMFT.py -f $MFT -o mft_analysis.csv
# Suche nach: SI-Time < FN-Time (Timestomp-Indikator)
```
### Phase 3: Log2timeline/PLASO Super-Timeline-Processing
**PLASO-basierte Timeline-Generierung:**
```bash
# Multi-Source-Timeline mit log2timeline
log2timeline.py --storage-file evidence.plaso \
--parsers "win7,chrome,firefox,skype" \
--timezone "Europe/Berlin" \
/mnt/evidence/
# CSV-Export für Analysis
psort.py -w timeline_super.csv evidence.plaso
```
**Advanced PLASO-Filtering:**
```bash
# Zeitfenster-spezifische Extraktion
psort.py -w incident_window.csv \
--date-filter "2024-01-10,2024-01-12" \
evidence.plaso
# Ereignis-spezifisches Filtering
psort.py -w web_activity.csv \
--filter "parser contains 'chrome'" \
evidence.plaso
```
## Advanced Correlation-Techniken
### Pivot-Point-Identifikation
**Initial Compromise Detection:**
```sql
-- SQL-basierte Timeline-Analyse (bei CSV-Import in DB)
SELECT timestamp, source, event_type, description
FROM timeline
WHERE description LIKE '%powershell%'
OR description LIKE '%cmd.exe%'
OR description LIKE '%rundll32%'
ORDER BY timestamp;
```
**Lateral Movement Patterns:**
```python
# Python-Script für Lateral-Movement-Detection
def detect_lateral_movement(timeline_data):
"""
Identifiziert suspicious Login-Patterns über Zeitfenster
"""
login_events = timeline_data[
timeline_data['event_type'].str.contains('4624|4625', na=False)
]
# Gruppierung nach Source-IP und Zeitfenster-Analyse
suspicious_logins = login_events.groupby(['source_ip']).apply(
lambda x: len(x[x['timestamp'].diff().dt.seconds < 300]) > 5
)
return suspicious_logins[suspicious_logins == True]
```
### Behavioral Pattern Recognition
**User Activity Profiling:**
```bash
# Regelmäßige Aktivitätsmuster extrahieren
grep -E "(explorer\.exe|chrome\.exe|outlook\.exe)" timeline.csv | \
awk -F',' '{print substr($1,1,10), $3}' | \
sort | uniq -c | sort -nr
```
**Anomalie-Detection durch Statistical Analysis:**
```python
import pandas as pd
from scipy import stats
def detect_activity_anomalies(timeline_df):
"""
Identifiziert ungewöhnliche Aktivitätsmuster via Z-Score
"""
# Aktivität pro Stunde aggregieren
timeline_df['hour'] = pd.to_datetime(timeline_df['timestamp']).dt.hour
hourly_activity = timeline_df.groupby('hour').size()
# Z-Score Berechnung für Anomalie-Detection
z_scores = stats.zscore(hourly_activity)
anomalous_hours = hourly_activity[abs(z_scores) > 2]
return anomalous_hours
```
## Network-Event-Korrelation
### Cross-System Timeline Correlation
**SIEM-Integration für Multi-Host-Korrelation:**
```bash
# Splunk-Query für korrelierte Events
index=windows EventCode=4624 OR EventCode=4625 OR EventCode=4648
| eval login_time=strftime(_time, "%Y-%m-%d %H:%M:%S")
| stats values(EventCode) as event_codes by src_ip, login_time
| where mvcount(event_codes) > 1
```
**Network Flow Timeline Integration:**
```python
# Zeek/Bro-Logs mit Filesystem-Timeline korrelieren
def correlate_network_filesystem(conn_logs, file_timeline):
"""
Korreliert Netzwerk-Connections mit File-Access-Patterns
"""
# Zeitfenster-basierte Korrelation (±30 Sekunden)
correlations = []
for _, conn in conn_logs.iterrows():
conn_time = pd.to_datetime(conn['ts'])
time_window = pd.Timedelta(seconds=30)
related_files = file_timeline[
(pd.to_datetime(file_timeline['timestamp']) >= conn_time - time_window) &
(pd.to_datetime(file_timeline['timestamp']) <= conn_time + time_window)
]
if not related_files.empty:
correlations.append({
'connection': conn,
'related_files': related_files,
'correlation_strength': len(related_files)
})
return correlations
```
## Anti-Forensik-Detection durch Timeline-Inkonsistenzen
### Timestamp Manipulation Detection
**Timestomp-Pattern-Analyse:**
```bash
# MFT-Analyse für Timestomp-Detection
analyzeMFT.py -f \$MFT -o mft_full.csv
# Suspekte Timestamp-Patterns identifizieren
python3 << EOF
import pandas as pd
import numpy as np
mft_data = pd.read_csv('mft_full.csv')
# Pattern 1: SI-Time vor FN-Time (klassischer Timestomp)
timestomp_candidates = mft_data[
pd.to_datetime(mft_data['SI_Modified']) < pd.to_datetime(mft_data['FN_Modified'])
]
# Pattern 2: Unrealistische Timestamps (z.B. 1980-01-01)
epoch_anomalies = mft_data[
pd.to_datetime(mft_data['SI_Created']).dt.year < 1990
]
print(f"Potential Timestomp: {len(timestomp_candidates)} files")
print(f"Epoch Anomalies: {len(epoch_anomalies)} files")
EOF
```
### Event Log Manipulation Detection
**Windows Event Log Gap Analysis:**
```python
def detect_log_gaps(event_log_df):
"""
Identifiziert verdächtige Lücken in Event-Log-Sequenzen
"""
# Event-Record-IDs sollten sequenziell sein
event_log_df['RecordNumber'] = pd.to_numeric(event_log_df['RecordNumber'])
event_log_df = event_log_df.sort_values('RecordNumber')
# Gaps in Record-Sequenz finden
record_diffs = event_log_df['RecordNumber'].diff()
large_gaps = record_diffs[record_diffs > 100] # Threshold anpassbar
return large_gaps
```
## Automated Timeline Processing & ML-basierte Anomalie-Erkennung
### Machine Learning für Pattern Recognition
**Unsupervised Clustering für Event-Gruppierung:**
```python
from sklearn.cluster import DBSCAN
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
def cluster_timeline_events(timeline_df):
"""
Gruppiert ähnliche Events via DBSCAN-Clustering
"""
# TF-IDF für Event-Descriptions
vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')
event_vectors = vectorizer.fit_transform(timeline_df['description'])
# DBSCAN-Clustering
clustering = DBSCAN(eps=0.5, min_samples=5).fit(event_vectors.toarray())
timeline_df['cluster'] = clustering.labels_
# Anomalie-Events (Cluster -1)
anomalous_events = timeline_df[timeline_df['cluster'] == -1]
return timeline_df, anomalous_events
```
**Time-Series-Anomalie-Detection:**
```python
from sklearn.ensemble import IsolationForest
import matplotlib.pyplot as plt
def detect_temporal_anomalies(timeline_df):
"""
Isolation Forest für zeitbasierte Anomalie-Detection
"""
# Stündliche Aktivität aggregieren
timeline_df['timestamp'] = pd.to_datetime(timeline_df['timestamp'])
hourly_activity = timeline_df.groupby(
timeline_df['timestamp'].dt.floor('H')
).size().reset_index(name='event_count')
# Isolation Forest Training
iso_forest = IsolationForest(contamination=0.1)
anomaly_labels = iso_forest.fit_predict(
hourly_activity[['event_count']]
)
# Anomale Zeitfenster identifizieren
hourly_activity['anomaly'] = anomaly_labels
anomalous_periods = hourly_activity[hourly_activity['anomaly'] == -1]
return anomalous_periods
```
## Enterprise-Scale Timeline Processing
### Distributed Processing für große Datasets
**Apache Spark für Big-Data-Timeline-Analyse:**
```python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
def process_enterprise_timeline(spark_session, timeline_path):
"""
Spark-basierte Verarbeitung für TB-große Timeline-Daten
"""
# Timeline-Daten laden
timeline_df = spark_session.read.csv(
timeline_path,
header=True,
inferSchema=True
)
# Zeitfenster-basierte Aggregation
windowed_activity = timeline_df \
.withColumn("timestamp", to_timestamp("timestamp")) \
.withColumn("hour_window", window("timestamp", "1 hour")) \
.groupBy("hour_window", "source_system") \
.agg(
count("*").alias("event_count"),
countDistinct("user").alias("unique_users"),
collect_set("event_type").alias("event_types")
)
return windowed_activity
```
### Cloud-Forensics Timeline Integration
**AWS CloudTrail Timeline Correlation:**
```bash
# CloudTrail-Events mit lokaler Timeline korrelieren
aws logs filter-log-events \
--log-group-name CloudTrail \
--start-time 1642636800000 \
--end-time 1642723200000 \
--filter-pattern "{ $.eventName = \"AssumeRole\" }" \
--output json > cloudtrail_events.json
# JSON zu CSV für Timeline-Integration
jq -r '.events[] | [.eventTime, .sourceIPAddress, .eventName, .userIdentity.type] | @csv' \
cloudtrail_events.json > cloudtrail_timeline.csv
```
## Praktische Anwendungsszenarien
### Szenario 1: Advanced Persistent Threat (APT) Investigation
**Mehrstufige Timeline-Analyse:**
1. **Initial Compromise Detection:**
```bash
# Web-Browser-Downloads mit Malware-Signaturen korrelieren
grep -E "(\.exe|\.zip|\.pdf)" browser_downloads.csv | \
while read line; do
timestamp=$(echo $line | cut -d',' -f1)
filename=$(echo $line | cut -d',' -f3)
# Hash-Verification gegen IOC-Liste
sha256=$(sha256sum "/mnt/evidence/$filename" 2>/dev/null | cut -d' ' -f1)
grep -q "$sha256" ioc_hashes.txt && echo "IOC Match: $timestamp - $filename"
done
```
2. **Lateral Movement Tracking:**
```sql
-- Cross-System-Bewegung via RDP/SMB
SELECT t1.timestamp, t1.source_ip, t2.timestamp, t2.dest_ip
FROM network_timeline t1
JOIN filesystem_timeline t2 ON
t2.timestamp BETWEEN t1.timestamp AND t1.timestamp + INTERVAL 5 MINUTE
WHERE t1.protocol = 'RDP' AND t2.activity_type = 'file_creation'
ORDER BY t1.timestamp;
```
### Szenario 2: Insider-Threat-Analyse
**Behavioral Baseline vs. Anomalie-Detection:**
```python
def analyze_insider_threat(user_timeline, baseline_days=30):
"""
Vergleicht User-Aktivität mit historischer Baseline
"""
# Baseline-Zeitraum definieren
baseline_end = pd.to_datetime('2024-01-01')
baseline_start = baseline_end - pd.Timedelta(days=baseline_days)
baseline_activity = user_timeline[
(user_timeline['timestamp'] >= baseline_start) &
(user_timeline['timestamp'] <= baseline_end)
]
# Anomale Aktivitätsmuster
analysis_period = user_timeline[
user_timeline['timestamp'] > baseline_end
]
# Metriken: Off-Hours-Activity, Data-Volume, Access-Patterns
baseline_metrics = calculate_user_metrics(baseline_activity)
current_metrics = calculate_user_metrics(analysis_period)
anomaly_score = compare_metrics(baseline_metrics, current_metrics)
return anomaly_score
```
## Herausforderungen und Lösungsansätze
### Challenge 1: Timezone-Komplexität in Multi-Domain-Umgebungen
**Problem:** Inkonsistente Timezones zwischen Systemen führen zu falschen Korrelationen.
**Lösung:**
```python
def unified_timezone_conversion(timeline_entries):
"""
Intelligente Timezone-Detection und UTC-Normalisierung
"""
timezone_mapping = {
'windows_local': 'Europe/Berlin',
'unix_utc': 'UTC',
'web_browser': 'client_timezone' # Aus Browser-Metadaten
}
for entry in timeline_entries:
source_tz = detect_timezone_from_source(entry['source'])
entry['timestamp_utc'] = convert_to_utc(
entry['timestamp'],
timezone_mapping.get(source_tz, 'UTC')
)
return timeline_entries
```
### Challenge 2: Volume-Skalierung bei Enterprise-Investigations
**Problem:** TB-große Timeline-Daten überschreiten Memory-Kapazitäten.
**Lösung - Streaming-basierte Verarbeitung:**
```python
def stream_process_timeline(file_path, chunk_size=10000):
"""
Memory-effiziente Timeline-Processing via Chunks
"""
for chunk in pd.read_csv(file_path, chunksize=chunk_size):
# Chunk-weise Verarbeitung
processed_chunk = apply_timeline_analysis(chunk)
# Streaming-Output zu aggregated Results
yield processed_chunk
```
### Challenge 3: Anti-Forensik und Timeline-Manipulation
**Problem:** Adversaries manipulieren Timestamps zur Evidence-Destruction.
**Lösung - Multi-Source-Validation:**
```bash
# Cross-Reference-Validation zwischen verschiedenen Timestamp-Quellen
python3 << EOF
# $MFT vs. $UsnJrnl vs. Event-Logs vs. Registry
def validate_timestamp_integrity(file_path):
sources = {
'mft_si': get_mft_si_time(file_path),
'mft_fn': get_mft_fn_time(file_path),
'usnjrnl': get_usnjrnl_time(file_path),
'prefetch': get_prefetch_time(file_path),
'eventlog': get_eventlog_time(file_path)
}
# Timestamp-Inkonsistenzen identifizieren
inconsistencies = detect_timestamp_discrepancies(sources)
confidence_score = calculate_integrity_confidence(sources)
return inconsistencies, confidence_score
EOF
```
## Tool-Integration und Workflow-Optimierung
### Timeline-Tool-Ecosystem
**Core-Tools-Integration:**
```bash
#!/bin/bash
# Comprehensive Timeline-Workflow-Automation
# 1. Multi-Source-Acquisition
log2timeline.py --storage-file case.plaso \
--parsers "win7,chrome,firefox,apache,nginx" \
--hashers "sha256" \
/mnt/evidence/
# 2. Memory-Timeline-Integration
volatility -f memory.vmem --profile=Win10x64 timeliner \
--output=csv --output-file=memory_timeline.csv
# 3. Network-Timeline-Addition
zeek -r network.pcap Log::default_path=/tmp/zeek_logs/
python3 zeek_to_timeline.py /tmp/zeek_logs/ > network_timeline.csv
# 4. Timeline-Merge und Analysis
psort.py -w comprehensive_timeline.csv case.plaso
python3 merge_timelines.py comprehensive_timeline.csv \
memory_timeline.csv network_timeline.csv > unified_timeline.csv
# 5. Advanced-Analysis-Pipeline
python3 timeline_analyzer.py unified_timeline.csv \
--detect-anomalies --pivot-analysis --correlation-strength=0.7
```
### Autopsy Timeline-Viewer Integration
**Autopsy-Import für Visual Timeline Analysis:**
```python
def export_autopsy_timeline(timeline_df, case_name):
"""
Konvertiert Timeline zu Autopsy-kompatiblem Format
"""
autopsy_format = timeline_df[['timestamp', 'source', 'event_type', 'description']].copy()
autopsy_format['timestamp'] = pd.to_datetime(autopsy_format['timestamp']).astype(int) // 10**9
# Autopsy-CSV-Format
autopsy_format.to_csv(f"{case_name}_autopsy_timeline.csv",
columns=['timestamp', 'source', 'event_type', 'description'],
index=False)
```
## Fazit und Best Practices
Timeline-Analyse repräsentiert eine fundamentale Investigationstechnik, die bei korrekter Anwendung präzise Incident-Rekonstruktion ermöglicht. Die Kombination aus methodischer Multi-Source-Integration, Advanced-Correlation-Techniken und ML-basierter Anomalie-Detection bildet die Basis für moderne forensische Untersuchungen.
**Key Success Factors:**
1. **Systematic Approach**: Strukturierte Herangehensweise von Akquisition bis Analysis
2. **Multi-Source-Validation**: Cross-Reference zwischen verschiedenen Artefakt-Typen
3. **Timezone-Awareness**: Konsistente UTC-Normalisierung für akkurate Korrelation
4. **Anti-Forensik-Resistenz**: Detection von Timestamp-Manipulation und Evidence-Destruction
5. **Scalability-Design**: Enterprise-fähige Processing-Pipelines für Big-Data-Szenarien
Die kontinuierliche Weiterentwicklung von Adversary-Techniken erfordert adaptive Timeline-Methoden, die sowohl traditionelle Artefakte als auch moderne Cloud- und Container-Umgebungen erfassen. Die Integration von Machine Learning in Timeline-Workflows eröffnet neue Möglichkeiten für automatisierte Anomalie-Detection und Pattern-Recognition bei gleichzeitiger Reduktion des manuellen Aufwands.
**Nächste Schritte:**
- Vertiefung spezifischer Tool-Implementierungen (Autopsy, SIFT, etc.)
- Cloud-native Timeline-Techniken für AWS/Azure-Umgebungen
- Advanced Correlation-Algorithmen für Zero-Day-Detection
- Integration von Threat-Intelligence in Timeline-Workflows

View File

@ -3,7 +3,7 @@ title: "Extraktion logischer Dateisysteme alter Android-Smartphones - eine KI-Re
tool_name: "Android Logical Imaging"
description: "Wie man alte Android-Handys aufbekommen könnte - eine Recherche von Claude"
last_updated: 2025-07-21
author: "Claude 4 Sonnet (Research)"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "advanced"
categories: ["data-collection"]
tags: ["imaging", "filesystem", "hardware-interface"]

View File

@ -1,110 +0,0 @@
---
title: "Regular Expressions (Regex) Musterbasierte Textanalyse"
tool_name: "Regular Expressions (Regex)"
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
last_updated: 2025-07-20
author: "Claude 4 Sonnet"
difficulty: "intermediate"
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"]
sections:
overview: true
installation: false
configuration: false
usage_examples: true
best_practices: true
troubleshooting: false
advanced_topics: true
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
**Regular Expressions (Regex)** sind ein leistungsfähiges Werkzeug zur Erkennung, Extraktion und Transformation von Zeichenfolgen anhand vordefinierter Muster. In der digitalen Forensik sind Regex-Ausdrücke unverzichtbar: Sie helfen beim Auffinden von IP-Adressen, Hash-Werten, Dateipfaden, Malware-Signaturen oder Kreditkartennummern in großen Mengen unstrukturierter Daten wie Logdateien, Netzwerktraces oder Memory Dumps.
Regex ist nicht auf eine bestimmte Plattform oder Software beschränkt es wird in nahezu allen gängigen Programmiersprachen, Texteditoren und forensischen Tools unterstützt.
## Verwendungsbeispiele
### 1. IP-Adressen extrahieren
```regex
\b(?:\d{1,3}\.){3}\d{1,3}\b
````
Verwendung:
* Finden von IP-Adressen in Firewall-Logs oder Packet Captures.
* Beispiel-Zeile:
```
Connection from 192.168.1.101 to port 443 established
```
### 2. E-Mail-Adressen identifizieren
```regex
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
```
Verwendung:
* Erkennung von kompromittierten Accounts in Phishing-E-Mails.
* Analyse von Useraktivitäten oder Kommunikationsverläufen.
### 3. Hash-Werte erkennen (z.B. SHA-256)
```regex
\b[A-Fa-f0-9]{64}\b
```
Verwendung:
* Extraktion von Malware-Hashes aus Memory Dumps oder YARA-Logs.
### 4. Zeitstempel in Logdateien extrahieren
```regex
\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}
```
Verwendung:
* Zeitsensitive Korrelationsanalysen (z.B. bei Intrusion Detection oder Timeline-Rekonstruktionen).
## Best Practices
* **Regex testen**: Nutze Plattformen wie [regexr.com](https://regexr.com/) oder [regex101.com](https://regex101.com/) zur Validierung.
* **Performance beachten**: Komplexe Ausdrücke können ineffizient sein und Systeme verlangsamen verwende Lazy Quantifiers (`*?`, `+?`) bei Bedarf.
* **Escape-Zeichen korrekt anwenden**: Spezielle Zeichen wie `.` oder `\` müssen bei Bedarf mit `\\` oder `\.` maskiert werden.
* **Portabilität prüfen**: Unterschiedliche Regex-Engines (z.B. Python `re`, PCRE, JavaScript) interpretieren manche Syntax leicht unterschiedlich.
* **Lesbarkeit fördern**: Verwende benannte Gruppen (`(?P<name>...)`) und Kommentare (`(?x)`), um reguläre Ausdrücke besser wartbar zu machen.
## Weiterführende Themen
### Lookaheads und Lookbehinds
Mit **Lookaheads** (`(?=...)`) und **Lookbehinds** (`(?<=...)`) können Bedingungen formuliert werden, ohne dass der Text Teil des Matchs wird.
Beispiel: Alle `.exe`-Dateinamen **ohne** das Wort `safe` davor matchen:
```regex
(?<!safe\s)[\w-]+\.exe
```
### Regex in Forensik-Tools
* **YARA**: Unterstützt Regex zur Erstellung von Malware-Signaturen.
* **Wireshark**: Filtert Payloads anhand von Regex-ähnlicher Syntax.
* **Splunk & ELK**: Verwenden Regex für Logparsing und Visualisierung.
* **Volatility Plugins**: Extrahieren Artefakte mit Regex-basierten Scans.
---
> 🔤 **Regex ist ein universelles Werkzeug für Analysten, Ermittler und Entwickler, um versteckte Informationen schnell und flexibel aufzuspüren.**
>
> Nutze es überall dort, wo Textdaten eine Rolle spielen.

View File

@ -2,8 +2,8 @@
title: "Kali Linux - Die Hacker-Distribution für Forensik & Penetration Testing"
tool_name: "Kali Linux"
description: "Leitfaden zur Installation, Nutzung und Best Practices für Kali Linux die All-in-One-Plattform für Security-Profis."
last_updated: 2025-07-20
author: "Claude 4 Sonnet"
last_updated: 2025-08-10
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "intermediate"
categories: ["incident-response", "forensics", "penetration-testing"]
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]

View File

@ -3,7 +3,7 @@ title: "MISP - Plattform für Threat Intelligence Sharing"
tool_name: "MISP"
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
last_updated: 2025-07-20
author: "Claude 4 Sonnet"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "intermediate"
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]

View File

@ -3,7 +3,7 @@ title: "Nextcloud - Sichere Kollaborationsplattform"
tool_name: "Nextcloud"
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
last_updated: 2025-07-20
author: "Claude 4 Sonnet"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "novice"
categories: ["collaboration-general"]
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]

View File

@ -3,7 +3,7 @@ title: "Velociraptor Skalierbare Endpoint-Forensik mit VQL"
tool_name: "Velociraptor"
description: "Detaillierte Anleitung und Best Practices für Velociraptor Remote-Forensik der nächsten Generation"
last_updated: 2025-07-20
author: "Claude 4 Sonnet"
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "advanced"
categories: ["incident-response", "malware-analysis", "network-forensics"]
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]

File diff suppressed because it is too large Load Diff

6
src/env.d.ts vendored
View File

@ -11,6 +11,9 @@ declare global {
showToolDetails: (toolName: string, modalType?: string) => void;
hideToolDetails: (modalType?: string) => void;
hideAllToolDetails: () => void;
matrixShowToolDetails?: (toolName: string, modalType?: string) => void;
matrixHideToolDetails?: (modalType?: string) => void;
toggleKbEntry: (entryId: string) => void;
toggleDomainAgnosticSection: (sectionId: string) => void;
restoreAIResults?: () => void;
@ -39,6 +42,9 @@ declare global {
toggleAllScenarios?: () => void;
showShareDialog?: (shareButton: Element) => void;
modalHideInProgress?: boolean;
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
shareCurrentArticle: (button: HTMLElement) => Promise<void>;
}
}

View File

@ -2,6 +2,10 @@
import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
import '../styles/auditTrail.css';
import '../styles/knowledgebase.css';
import '../styles/palette.css';
import '../styles/autocomplete.css';
export interface Props {
title: string;
@ -21,36 +25,44 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<script>
function createToolSlug(toolName) {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
async function loadUtilityFunctions() {
try {
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
(window as any).createToolSlug = createToolSlug;
(window as any).findToolByIdentifier = findToolByIdentifier;
(window as any).isToolHosted = isToolHosted;
console.log('[UTILS] Utility functions loaded successfully');
} catch (error) {
console.error('Failed to load utility functions:', error);
// Provide fallback implementations
(window as any).createToolSlug = (toolName: string) => {
if (!toolName || typeof toolName !== 'string') return '';
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
};
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find((tool: any) =>
tool.name === identifier ||
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
);
};
(window as any).isToolHosted = (tool: any) => {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
};
console.log('[UTILS] Fallback utility functions registered');
}
}
return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
function findToolByIdentifier(tools, identifier) {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
function scrollToElement(element, options = {}) {
function scrollToElement(element: Element | null, options = {}) {
if (!element) return;
setTimeout(() => {
@ -66,17 +78,21 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}, 100);
}
function scrollToElementById(elementId, options = {}) {
function scrollToElementById(elementId: string, options = {}) {
const element = document.getElementById(elementId);
scrollToElement(element, options);
if (element) {
scrollToElement(element, options);
}
}
function scrollToElementBySelector(selector, options = {}) {
function scrollToElementBySelector(selector: string, options = {}) {
const element = document.querySelector(selector);
scrollToElement(element, options);
if (element) {
scrollToElement(element, options);
}
}
function prioritizeSearchResults(tools, searchTerm) {
function prioritizeSearchResults(tools: any[], searchTerm: string) {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
@ -84,8 +100,8 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map(tag => tag.toLowerCase());
const bTagsLower = (b.tags || []).map(tag => tag.toLowerCase());
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
@ -97,15 +113,15 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
});
}
(window as any).createToolSlug = createToolSlug;
(window as any).findToolByIdentifier = findToolByIdentifier;
(window as any).isToolHosted = isToolHosted;
(window as any).scrollToElement = scrollToElement;
(window as any).scrollToElementById = scrollToElementById;
(window as any).scrollToElementBySelector = scrollToElementBySelector;
(window as any).prioritizeSearchResults = prioritizeSearchResults;
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
// CRITICAL: Load utility functions FIRST before any URL handling
await loadUtilityFunctions();
const THEME_KEY = 'dfir-theme';
function getSystemTheme() {
@ -116,12 +132,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
return localStorage.getItem(THEME_KEY) || 'auto';
}
function applyTheme(theme) {
function applyTheme(theme: string) {
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme);
}
function updateThemeToggle(theme) {
function updateThemeToggle(theme: string) {
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
button.setAttribute('data-current-theme', theme);
});
@ -157,6 +173,44 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
getStoredTheme
};
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
let attempts = 0;
const maxAttempts = 50;
const tryDelegate = () => {
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
return matrixShowToolDetails(toolName, modalType);
}
const directShowToolDetails = (window as any).directShowToolDetails;
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
return directShowToolDetails(toolName, modalType);
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(tryDelegate, 100);
} else {
}
};
tryDelegate();
};
(window as any).hideToolDetails = function(modalType: string = 'both') {
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
return matrixHideToolDetails(modalType);
}
};
(window as any).hideAllToolDetails = function() {
(window as any).hideToolDetails('both');
};
async function checkClientAuth(context = 'general') {
try {
const response = await fetch('/api/auth/status');
@ -191,7 +245,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}
}
async function requireClientAuth(callback, returnUrl, context = 'general') {
async function requireClientAuth(callback: () => void, returnUrl: string, context = 'general') {
const authStatus = await checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) {
@ -206,12 +260,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}
}
async function showIfAuthenticated(selector, context = 'general') {
async function showIfAuthenticated(selector: string, context = 'general') {
const authStatus = await checkClientAuth(context);
const element = document.querySelector(selector);
if (element) {
element.style.display = (!authStatus.authRequired || authStatus.authenticated)
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex'
: 'none';
}
@ -240,6 +294,51 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
(window as any).showIfAuthenticated = showIfAuthenticated;
(window as any).setupAuthButtons = setupAuthButtons;
async function copyUrlToClipboard(url: string, button: HTMLElement) {
try {
await navigator.clipboard.writeText(url);
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<polyline points="20,6 9,17 4,12"/>
</svg>
Kopiert!
`;
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
async function shareArticle(button: HTMLElement, url: string, title: string) {
const fullUrl = window.location.origin + url;
await copyUrlToClipboard(fullUrl, button);
}
async function shareCurrentArticle(button: HTMLElement) {
await copyUrlToClipboard(window.location.href, button);
}
(window as any).shareArticle = shareArticle;
(window as any).shareCurrentArticle = shareCurrentArticle;
initTheme();
setupAuthButtons('[data-contribute-button]');
@ -247,8 +346,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
await showIfAuthenticated('#ai-view-toggle', 'ai');
};
initAIButton();
console.log('[CONSOLIDATED] All utilities loaded and initialized');
});
</script>
</head>

View File

@ -0,0 +1,37 @@
// src/pages/api/ai/embeddings-status.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = async () => {
try {
const { embeddingsService } = await import('../../../utils/embeddings.js');
await embeddingsService.waitForInitialization();
const stats = embeddingsService.getStats();
const status = stats.enabled && stats.initialized ? 'ready' :
stats.enabled && !stats.initialized ? 'initializing' : 'disabled';
return new Response(JSON.stringify({
success: true,
embeddings: stats,
timestamp: new Date().toISOString(),
status: status
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
embeddings: { enabled: false, initialized: false, count: 0 },
timestamp: new Date().toISOString(),
status: 'disabled',
error: error.message
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
};

View File

@ -1,22 +0,0 @@
// src/pages/api/ai/embeddings-status.ts
import type { APIRoute } from 'astro';
import { embeddingsService } from '../../../utils/embeddings.js';
import { apiResponse, apiServerError } from '../../../utils/api.js';
export const prerender = false;
export const GET: APIRoute = async () => {
try {
const stats = embeddingsService.getStats();
return apiResponse.success({
embeddings: stats,
timestamp: new Date().toISOString(),
status: stats.enabled && stats.initialized ? 'ready' :
stats.enabled && !stats.initialized ? 'initializing' : 'disabled'
});
} catch (error) {
console.error('Embeddings status error:', error);
return apiServerError.internal('Failed to get embeddings status');
}
};

View File

@ -20,7 +20,7 @@ const AI_ANALYZER_API_KEY = getEnv('AI_ANALYZER_API_KEY');
const AI_ANALYZER_MODEL = getEnv('AI_ANALYZER_MODEL');
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_WINDOW = 60 * 1000;
const RATE_LIMIT_MAX = 5;
function sanitizeInput(input: string): string {
@ -81,12 +81,13 @@ QUALITÄTSKRITERIEN FÜR FRAGEN:
- Forensisch spezifisch, nicht allgemein ( "Mehr Details?" "Welche forensischen Artefakte (RAM-Dumps, Disk-Images, Logs) stehen zur Verfügung?")
- Methodisch relevant ( "Wann passierte das?" "Liegen Log-Dateien aus dem Incident-Zeitraum vor, und welche Retention-Policy gilt?")
- Priorisiert nach Auswirkung auf die forensische Untersuchungsqualität
- Die Frage soll maximal 30 Wörter umfassen
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
[
"Forensisch spezifische Frage 1?",
"Forensisch spezifische Frage 2?",
"Forensisch spezifische Frage 3?"
"spezifische Frage 1?",
"spezifische Frage 2?",
"spezifische Frage 3?"
]
NUTZER-EINGABE:

View File

@ -1,4 +1,4 @@
// src/pages/api/ai/query.ts - FIXED: Rate limiting for micro-task pipeline
// src/pages/api/ai/query.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';

View File

@ -1,4 +1,4 @@
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@ -27,6 +27,7 @@ const ContributionToolSchema = z.object({
knowledgebase: z.boolean().optional().nullable(),
'domain-agnostic-software': z.array(z.string()).optional().nullable(),
related_concepts: z.array(z.string()).optional().nullable(),
related_software: z.array(z.string()).optional().nullable(),
tags: z.array(z.string()).default([]),
statusUrl: z.string().url('Must be a valid URL').optional().nullable()
});
@ -80,6 +81,38 @@ function sanitizeInput(obj: any): any {
return obj;
}
function preprocessFormData(body: any): any {
// Handle comma-separated strings from autocomplete inputs
if (body.tool) {
// Handle tags
if (typeof body.tool.tags === 'string') {
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
}
// Handle related concepts
if (body.tool.relatedConcepts) {
if (typeof body.tool.relatedConcepts === 'string') {
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_concepts = body.tool.relatedConcepts;
}
delete body.tool.relatedConcepts; // Remove the original key
}
// Handle related software
if (body.tool.relatedSoftware) {
if (typeof body.tool.relatedSoftware === 'string') {
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_software = body.tool.relatedSoftware;
}
delete body.tool.relatedSoftware; // Remove the original key
}
}
return body;
}
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
@ -109,6 +142,17 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
}
}
// Validate related items exist (optional validation - could be enhanced)
if (tool.related_concepts && tool.related_concepts.length > 0) {
// Could validate that referenced concepts actually exist
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
}
if (tool.related_software && tool.related_software.length > 0) {
// Could validate that referenced software actually exists
console.log('[VALIDATION] Related software provided:', tool.related_software);
}
return { valid: errors.length === 0, errors };
} catch (error) {
@ -143,6 +187,9 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.invalidJSON();
}
// Preprocess form data to handle autocomplete inputs
body = preprocessFormData(body);
const sanitizedBody = sanitizeInput(body);
let validatedData;
@ -153,6 +200,7 @@ export const POST: APIRoute = async ({ request }) => {
const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}`
);
console.log('[VALIDATION] Zod validation errors:', errorMessages);
return apiError.validation('Validation failed', errorMessages);
}
@ -174,6 +222,16 @@ export const POST: APIRoute = async ({ request }) => {
}
};
console.log('[CONTRIBUTION] Processing contribution:', {
type: contributionData.type,
toolName: contributionData.tool.name,
toolType: contributionData.tool.type,
submitter: userEmail,
hasRelatedConcepts: !!(contributionData.tool.related_concepts?.length),
hasRelatedSoftware: !!(contributionData.tool.related_software?.length),
tagsCount: contributionData.tool.tags?.length || 0
});
try {
const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData);

View File

@ -0,0 +1,83 @@
// src/pages/api/search/semantic.ts
import type { APIRoute } from 'astro';
import { getToolsData } from '../../../utils/dataService.js';
import { configDotenv } from 'dotenv';
configDotenv();
const DEFAULT_MAX_RESULTS = (() => {
const raw = process.env.AI_EMBEDDING_CANDIDATES;
const n = Number.parseInt(raw ?? '', 10);
return Number.isFinite(n) && n > 0 ? n : 50;
})();
const DEFAULT_THRESHOLD = (() => {
const raw = process.env.AI_SIMILARITY_THRESHOLD;
const n = Number.parseFloat(raw ?? '');
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : 0.45;
})();
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
try {
const {
query,
maxResults = DEFAULT_MAX_RESULTS,
threshold = DEFAULT_THRESHOLD
} = await request.json();
if (!query || typeof query !== 'string') {
return new Response(
JSON.stringify({ success: false, error: 'Query is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
/* --- (rest of the handler unchanged) -------------------------- */
const { embeddingsService } = await import('../../../utils/embeddings.js');
if (!embeddingsService.isEnabled()) {
return new Response(
JSON.stringify({ success: false, error: 'Semantic search not available' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
await embeddingsService.waitForInitialization();
const similarItems = await embeddingsService.findSimilar(
query.trim(),
maxResults,
threshold
);
const toolsData = await getToolsData();
const rankedTools = similarItems
.map((s, i) => {
const tool = toolsData.tools.find(t => t.name === s.name);
return tool ? { ...tool, _semanticSimilarity: s.similarity, _semanticRank: i + 1 } : null;
})
.filter(Boolean);
return new Response(
JSON.stringify({
success: true,
query: query.trim(),
results: rankedTools,
totalFound: rankedTools.length,
semanticSearch: true,
threshold,
maxSimilarity: rankedTools[0]?._semanticSimilarity ?? 0
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Semantic search error:', error);
return new Response(
JSON.stringify({ success: false, error: 'Semantic search failed' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};

View File

@ -1,4 +1,3 @@
// src/pages/api/upload/media.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@ -22,7 +21,9 @@ const UPLOAD_CONFIG = {
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedTypes: new Set([
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@ -30,7 +31,29 @@ const UPLOAD_CONFIG = {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain', 'text/csv', 'application/json'
'text/plain',
'text/csv',
'text/markdown',
'text/x-markdown',
'application/json',
'application/xml',
'text/xml',
'text/html',
'application/zip',
'application/x-tar',
'application/gzip',
'application/x-gzip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'application/x-7z-compressed',
'application/rtf',
'text/richtext',
'application/x-yaml',
'text/yaml',
'application/yaml'
]),
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
@ -50,6 +73,7 @@ function checkUploadRateLimit(userEmail: string): boolean {
}
if (userLimit.count >= RATE_LIMIT_MAX) {
console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
return false;
}
@ -58,27 +82,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
}
function validateFile(file: File): { valid: boolean; error?: string } {
console.log(`[UPLOAD] Validating file: ${file.name}, size: ${file.size}, type: ${file.type}`);
if (file.size > UPLOAD_CONFIG.maxFileSize) {
return {
valid: false,
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
};
const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
return { valid: false, error: errorMsg };
}
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
return {
valid: false,
error: `File type ${file.type} not allowed`
};
const errorMsg = `File type ${file.type} not allowed`;
console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
return { valid: false, error: errorMsg };
}
console.log(`[UPLOAD] File validation passed for: ${file.name}`);
return { valid: true };
}
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
try {
const uploader = new NextcloudUploader();
const result = await uploader.uploadFile(file, userEmail);
console.log(`[UPLOAD] Nextcloud upload successful:`, {
filename: result.filename,
url: result.url,
size: file.size
});
return {
success: true,
url: result.url,
@ -87,7 +121,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
storage: 'nextcloud'
};
} catch (error) {
console.error('Nextcloud upload failed:', error);
console.error('[UPLOAD] Nextcloud upload failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
@ -97,7 +131,10 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
}
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
try {
console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
@ -106,11 +143,20 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
const filename = `${timestamp}-${randomString}${extension}`;
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
console.log(`[UPLOAD] Writing file to: ${filepath}`);
const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer);
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
console.log(`[UPLOAD] Local upload successful:`, {
filename,
filepath,
publicUrl,
size: file.size
});
return {
success: true,
url: publicUrl,
@ -119,7 +165,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
storage: 'local'
};
} catch (error) {
console.error('Local upload failed:', error);
console.error('[UPLOAD] Local upload failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Local upload failed',
@ -130,12 +176,22 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
console.log('[UPLOAD] Processing upload request');
const authResult = await withAPIAuth(request, 'contributions');
console.log('[UPLOAD] Auth result:', {
authenticated: authResult.authenticated,
authRequired: authResult.authRequired,
userId: authResult.userId
});
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
return apiError.unauthorized('Authentication required for file uploads');
}
const userEmail = authResult.session?.email || 'anon@anon.anon';
console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
if (!checkUploadRateLimit(userEmail)) {
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
@ -143,38 +199,59 @@ export const POST: APIRoute = async ({ request }) => {
let formData;
try {
console.log('[UPLOAD] Parsing form data');
formData = await request.formData();
console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
} catch (error) {
return apiError.badRequest('Invalid form data');
console.error('[UPLOAD] Failed to parse form data:', error);
return apiError.badRequest('Invalid form data - could not parse request');
}
const file = formData.get('file') as File;
const type = formData.get('type') as string;
if (!file) {
console.warn('[UPLOAD] No file provided in request');
return apiSpecial.missingRequired(['file']);
}
console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
const validation = validateFile(file);
if (!validation.valid) {
return apiError.badRequest(validation.error!);
}
const nextcloudConfigured = isNextcloudConfigured();
console.log('[UPLOAD] Environment check:', {
nextcloudConfigured,
localUploadPath: UPLOAD_CONFIG.localUploadPath,
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl,
nodeEnv: process.env.NODE_ENV
});
let result: UploadResult;
if (isNextcloudConfigured()) {
if (nextcloudConfigured) {
console.log('[UPLOAD] Using Nextcloud as primary storage');
result = await uploadToNextcloud(file, userEmail);
if (!result.success) {
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
result = await uploadToLocal(file, type);
}
} else {
console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
result = await uploadToLocal(file, type);
}
if (result.success) {
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
console.log(`[UPLOAD] Upload completed successfully:`, {
filename: result.filename,
storage: result.storage,
url: result.url,
user: userEmail
});
return apiSpecial.uploadSuccess({
url: result.url!,
@ -183,7 +260,12 @@ export const POST: APIRoute = async ({ request }) => {
storage: result.storage!
});
} else {
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
console.error(`[UPLOAD] Upload failed completely:`, {
filename: file.name,
error: result.error,
storage: result.storage,
user: userEmail
});
return apiSpecial.uploadFailed(result.error!);
}
@ -193,6 +275,8 @@ export const POST: APIRoute = async ({ request }) => {
export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
console.log('[UPLOAD] Getting upload status');
const authResult = await withAPIAuth(request);
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
@ -204,12 +288,14 @@ export const GET: APIRoute = async ({ request }) => {
try {
await fs.access(UPLOAD_CONFIG.localUploadPath);
localStorageAvailable = true;
console.log('[UPLOAD] Local storage accessible');
} catch {
try {
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
localStorageAvailable = true;
console.log('[UPLOAD] Local storage created');
} catch (error) {
console.warn('Local upload directory not accessible:', error);
console.warn('[UPLOAD] Local upload directory not accessible:', error);
}
}
@ -237,9 +323,14 @@ export const GET: APIRoute = async ({ request }) => {
paths: {
uploadEndpoint: '/api/upload/media',
localPath: localStorageAvailable ? '/uploads' : null
},
environment: {
nodeEnv: process.env.NODE_ENV,
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl
}
};
console.log('[UPLOAD] Status check completed:', status);
return apiResponse.success(status);
}, 'Upload status retrieval failed');

View File

@ -1,5 +1,5 @@
---
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
// src/pages/contribute/knowledgebase.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { withAuth } from '../../utils/auth.js';
import { getToolsData } from '../../utils/dataService.js';
@ -114,8 +114,13 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
<div class="form-group">
<label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
<div class="upload-area" id="upload-area">
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
<div class="upload-placeholder">
<input
type="file"
id="file-input"
multiple
accept=".pdf,.doc,.docx,.txt,.md,.markdown,.csv,.json,.xml,.html,.rtf,.yaml,.yml,.zip,.tar,.gz,.rar,.7z,.png,.jpg,.jpeg,.gif,.webp,.svg,.mp4,.webm,.mov,.avi"
class="hidden"
> <div class="upload-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
@ -304,6 +309,12 @@ class KnowledgebaseForm {
private handleFiles(files: File[]) {
files.forEach(file => {
const validation = this.validateFileBeforeUpload(file);
if (!validation.valid) {
console.log('[UPLOAD]Cannot upload ', file.name, ' Error: ', validation.error);
return;
}
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
const newFile: UploadedFile = {
id: fileId,
@ -317,30 +328,98 @@ class KnowledgebaseForm {
this.renderFileList();
}
private validateFileBeforeUpload(file: File): { valid: boolean; error?: string } {
const maxSizeBytes = 50 * 1024 * 1024; // 50MB
if (file.size > maxSizeBytes) {
const sizeMB = (file.size / 1024 / 1024).toFixed(1);
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(0);
return {
valid: false,
error: `File too large (${sizeMB}MB). Maximum size: ${maxMB}MB`
};
}
const allowedExtensions = [
'.pdf', '.doc', '.docx', '.txt', '.md', '.markdown', '.csv', '.json',
'.xml', '.html', '.rtf', '.yaml', '.yml', '.zip', '.tar', '.gz',
'.rar', '.7z', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg',
'.mp4', '.webm', '.mov', '.avi'
];
const fileName = file.name.toLowerCase();
const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) {
return {
valid: false,
error: `File type not allowed. Allowed: ${allowedExtensions.join(', ')}`
};
}
return { valid: true };
}
private async uploadFile(fileId: string) {
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
if (!fileItem) return;
if (!fileItem) {
console.error('[UPLOAD] File item not found for ID:', fileId);
return;
}
console.log('[UPLOAD] Starting upload for:', fileItem.name, 'Size:', fileItem.file.size, 'Type:', fileItem.file.type);
const formData = new FormData();
formData.append('file', fileItem.file);
formData.append('type', 'knowledgebase');
try {
console.log('[UPLOAD] Sending request to /api/upload/media');
const response = await fetch('/api/upload/media', {
method: 'POST',
body: formData
});
console.log('[UPLOAD] Response status:', response.status);
let responseText: string;
let responseData: any;
try {
responseText = await response.text();
console.log('[UPLOAD] Raw response:', responseText.substring(0, 200));
try {
responseData = JSON.parse(responseText);
} catch (parseError) {
responseData = { error: responseText };
}
} catch (readError) {
console.error('[UPLOAD] Failed to read response:', readError);
throw new Error('Failed to read server response');
}
if (response.ok) {
const result = await response.json();
console.log('[UPLOAD] Success result:', responseData);
fileItem.uploaded = true;
fileItem.url = result.url;
fileItem.url = responseData.url;
this.renderFileList();
} else {
throw new Error('Upload failed');
if (responseData && responseData.details) {
console.error('[UPLOAD] Error details:', responseData.details);
}
}
} catch (error) {
this.showMessage('error', `Failed to upload ${fileItem.name}`);
console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
const errorMessage = error instanceof Error
? error.message
: 'Unknown upload error';
this.removeFile(fileId);
}
}
@ -412,7 +491,6 @@ class KnowledgebaseForm {
} catch (error) {
console.error('[KB FORM] Submission error:', error);
this.showMessage('error', 'Submission failed. Please try again.');
} finally {
this.isSubmitting = false;
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
@ -441,18 +519,6 @@ class KnowledgebaseForm {
this.renderFileList();
}
private showMessage(type: 'success' | 'error' | 'warning', message: string) {
const container = document.getElementById('message-container');
if (!container) return;
const messageEl = document.createElement('div');
messageEl.className = `message message-${type}`;
messageEl.textContent = message;
container.appendChild(messageEl);
setTimeout(() => messageEl.remove(), 5000);
}
public removeFileById(fileId: string) {
this.removeFile(fileId);
}

View File

@ -22,6 +22,17 @@ const existingTools = data.tools;
const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool;
// Extract data for autocomplete
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
const allSoftwareAndMethods = existingTools
.filter(tool => tool.type === 'software' || tool.type === 'method')
.map(tool => tool.name)
.sort();
const allConcepts = existingTools
.filter(tool => tool.type === 'concept')
.map(tool => tool.name)
.sort();
---
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
@ -194,16 +205,27 @@ const isEdit = !!editTool;
</div>
</div>
<div id="concepts-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Konzepte im Zusammenhang</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
{existingTools.filter(tool => tool.type === 'concept').map(concept => (
<label class="checkbox-wrapper">
<input type="checkbox" name="relatedConcepts" value={concept.name}
checked={editTool?.related_concepts?.includes(concept.name)} />
<span>{concept.name}</span>
</label>
))}
<div id="relations-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Verwandte Tools & Konzepte</h3>
<div style="display: grid; gap: 1.5rem;">
<div id="related-concepts-section">
<label for="related-concepts-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Konzepte</label>
<input type="text" id="related-concepts-input" placeholder="Beginne zu tippen, um Konzepte zu finden..." />
<input type="hidden" id="related-concepts-hidden" name="relatedConcepts" value={editTool?.related_concepts?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Konzepte, die mit diesem Tool/Methode in Verbindung stehen
</small>
</div>
<div id="related-software-section">
<label for="related-software-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Software & Methoden</label>
<input type="text" id="related-software-input" placeholder="Beginne zu tippen, um Software/Methoden zu finden..." />
<input type="hidden" id="related-software-hidden" name="relatedSoftware" value={editTool?.related_software?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Software oder Methoden, die oft zusammen mit diesem Tool verwendet werden
</small>
</div>
</div>
</div>
@ -211,9 +233,12 @@ const isEdit = !!editTool;
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Zusatzinfos</h3>
<div style="margin-bottom: 1.5rem;">
<label for="tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags" name="tags" value={editTool?.tags?.join(', ') || ''}
placeholder="Komma-getrennt: Passende Begriffe, nach denen ihr suchen würdet." />
<label for="tags-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags-input" placeholder="Beginne zu tippen, um Tags hinzuzufügen..." />
<input type="hidden" id="tags-hidden" name="tags" value={editTool?.tags?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Passende Begriffe, nach denen ihr suchen würdet
</small>
</div>
<div style="margin-bottom: 1.5rem;">
@ -274,7 +299,275 @@ const isEdit = !!editTool;
</div>
</BaseLayout>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
class AutocompleteManager {
constructor(inputElement, dataSource, options = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.isOpen = false;
this.selectedIndex = -1;
this.filteredData = [];
this.selectedItems = new Set();
this.init();
}
init() {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown() {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
// Insert dropdown after input
this.input.parentNode.style.position = 'relative';
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents() {
this.input.addEventListener('input', (e) => {
this.handleInput(e.target.value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= this.options.minLength) {
this.showDropdown();
}
});
this.input.addEventListener('blur', (e) => {
// Delay to allow click events on dropdown items
setTimeout(() => {
if (!this.dropdown.contains(document.activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) {
this.hideDropdown();
}
});
}
initMultipleMode() {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.input.parentNode.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value) {
if (value.length >= this.options.minLength) {
this.filteredData = this.options.filterFunction(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e) {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query) {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults);
}
defaultRender(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown() {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction(item);
return `<div class="autocomplete-option" data-index="${index}">${content}</div>`;
})
.join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight() {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text) {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay() {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}">×</button>
</span>
`)
.join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem(btn.getAttribute('data-item'));
});
});
}
updateInputValue() {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator);
}
}
showDropdown() {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown() {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource) {
this.dataSource = newDataSource;
}
getSelectedItems() {
return Array.from(this.selectedItems);
}
setSelectedItems(items) {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy() {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}
console.log('[FORM] Script loaded, initializing...');
class ContributionForm {
@ -283,6 +576,7 @@ class ContributionForm {
this.editTool = editTool;
this.elements = {};
this.isSubmitting = false;
this.autocompleteManagers = new Map();
this.init();
}
@ -303,14 +597,20 @@ class ContributionForm {
yamlPreview: document.getElementById('yaml-preview'),
successModal: document.getElementById('success-modal'),
softwareFields: document.getElementById('software-fields'),
conceptsFields: document.getElementById('concepts-fields'),
relationsFields: document.getElementById('relations-fields'),
descriptionCount: document.getElementById('description-count'),
reasonCount: document.getElementById('reason-count'),
validationErrors: document.getElementById('validation-errors'),
errorList: document.getElementById('error-list'),
platformsRequired: document.getElementById('platforms-required'),
licenseRequired: document.getElementById('license-required'),
licenseInput: document.getElementById('license')
licenseInput: document.getElementById('license'),
tagsInput: document.getElementById('tags-input'),
tagsHidden: document.getElementById('tags-hidden'),
relatedConceptsInput: document.getElementById('related-concepts-input'),
relatedConceptsHidden: document.getElementById('related-concepts-hidden'),
relatedSoftwareInput: document.getElementById('related-software-input'),
relatedSoftwareHidden: document.getElementById('related-software-hidden')
};
if (!this.elements.form || !this.elements.submitBtn) {
@ -327,6 +627,7 @@ class ContributionForm {
console.log('[FORM] Setting up handlers...');
this.setupEventListeners();
this.setupAutocomplete();
this.updateFieldVisibility();
this.setupCharacterCounters();
this.updateYAMLPreview();
@ -334,6 +635,65 @@ class ContributionForm {
console.log('[FORM] Initialization complete!');
}
setupAutocomplete() {
// Tags autocomplete
if (this.elements.tagsInput && this.elements.tagsHidden) {
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
allowMultiple: true,
hiddenInput: this.elements.tagsHidden,
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
});
// Set initial values if editing
if (this.editTool?.tags) {
tagsManager.setSelectedItems(this.editTool.tags);
}
this.autocompleteManagers.set('tags', tagsManager);
}
// Related concepts autocomplete
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
allowMultiple: true,
hiddenInput: this.elements.relatedConceptsHidden,
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
});
// Set initial values if editing
if (this.editTool?.related_concepts) {
conceptsManager.setSelectedItems(this.editTool.related_concepts);
}
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
}
// Related software autocomplete
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
allowMultiple: true,
hiddenInput: this.elements.relatedSoftwareHidden,
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
});
// Set initial values if editing
if (this.editTool?.related_software) {
softwareManager.setSelectedItems(this.editTool.related_software);
}
this.autocompleteManagers.set('relatedSoftware', softwareManager);
}
// Listen for autocomplete changes to update YAML preview
Object.values(this.autocompleteManagers).forEach(manager => {
if (manager.input) {
manager.input.addEventListener('autocomplete:select', () => {
this.updateYAMLPreview();
});
}
});
}
setupEventListeners() {
this.elements.typeSelect.addEventListener('change', () => {
this.updateFieldVisibility();
@ -363,203 +723,221 @@ class ContributionForm {
console.log('[FORM] Event listeners attached');
}
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
this.elements.softwareFields.style.display = 'none';
this.elements.conceptsFields.style.display = 'none';
// Only hide/show software-specific fields (platforms, license)
// Relations should always be visible since all tool types can have relationships
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
// Always show relations - all tool types can have relationships
this.elements.relationsFields.style.display = 'block';
if (type === 'software') {
this.elements.softwareFields.style.display = 'block';
this.elements.conceptsFields.style.display = 'block';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
} else if (type === 'method') {
this.elements.conceptsFields.style.display = 'block';
// Only mark platform/license as required for software
if (this.elements.platformsRequired) {
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
}
if (this.elements.licenseRequired) {
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
}
// Always show both relation sections - let users decide what's relevant
const conceptsSection = document.getElementById('related-concepts-section');
const softwareSection = document.getElementById('related-software-section');
if (conceptsSection) conceptsSection.style.display = 'block';
if (softwareSection) softwareSection.style.display = 'block';
console.log('[FORM] Updated visibility for type:', type || '(no type selected)');
}
console.log('[FORM] Field visibility updated for type:', type);
}
setupCharacterCounters() {
const counters = [
{ element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 },
{ element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 }
];
setupCharacterCounters() {
const counters = [
{ element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 },
{ element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 }
];
counters.forEach(({ element, counter, max }) => {
if (element && counter) {
const updateCounter = () => {
const count = element.value.length;
counter.textContent = count;
counter.style.color = count > max * 0.9 ? 'var(--color-warning)' : 'var(--color-text-secondary)';
};
counters.forEach(({ element, counter, max }) => {
if (element && counter) {
const updateCounter = () => {
const count = element.value.length;
counter.textContent = count;
counter.style.color = count > max * 0.9 ? 'var(--color-warning)' : 'var(--color-text-secondary)';
element.addEventListener('input', updateCounter);
updateCounter();
}
});
}
updateYAMLPreview() {
if (!this.elements.yamlPreview) return;
try {
const formData = new FormData(this.elements.form);
const tool = {
name: formData.get('name') || 'Tool Name',
type: formData.get('type') || 'software',
description: formData.get('description') || 'Tool description',
domains: formData.getAll('domains'),
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel') || 'intermediate',
url: formData.get('url') || 'https://example.com'
};
element.addEventListener('input', updateCounter);
updateCounter();
if (formData.get('icon')) {
tool.icon = formData.get('icon');
}
if (tool.type === 'software') {
tool.platforms = formData.getAll('platforms');
tool.license = formData.get('license') || 'Unknown';
if (formData.get('accessType')) {
tool.accessType = formData.get('accessType');
}
const domainAgnostic = formData.getAll('domainAgnostic');
if (domainAgnostic.length > 0) {
tool['domain-agnostic-software'] = domainAgnostic;
}
}
if (formData.has('knowledgebase')) {
tool.knowledgebase = true;
}
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
const yaml = this.generateYAML(tool);
this.elements.yamlPreview.textContent = yaml;
} catch (error) {
console.error('[FORM] YAML preview error:', error);
this.elements.yamlPreview.textContent = '# Error generating preview';
}
});
}
}
updateYAMLPreview() {
if (!this.elements.yamlPreview) return;
generateYAML(tool) {
const lines = [];
try {
lines.push(`name: "${tool.name}"`);
if (tool.icon) lines.push(`icon: "${tool.icon}"`);
lines.push(`type: ${tool.type}`);
lines.push(`description: "${tool.description}"`);
lines.push(`domains: [${tool.domains.map(d => `"${d}"`).join(', ')}]`);
lines.push(`phases: [${tool.phases.map(p => `"${p}"`).join(', ')}]`);
lines.push(`skillLevel: ${tool.skillLevel}`);
lines.push(`url: "${tool.url}"`);
if (tool.platforms && tool.platforms.length > 0) {
lines.push(`platforms: [${tool.platforms.map(p => `"${p}"`).join(', ')}]`);
}
if (tool.license) lines.push(`license: "${tool.license}"`);
if (tool.accessType) lines.push(`accessType: ${tool.accessType}`);
if (tool['domain-agnostic-software']) {
lines.push(`domain-agnostic-software: [${tool['domain-agnostic-software'].map(c => `"${c}"`).join(', ')}]`);
}
if (tool.knowledgebase) lines.push(`knowledgebase: true`);
if (tool.tags && tool.tags.length > 0) {
lines.push(`tags: [${tool.tags.map(t => `"${t}"`).join(', ')}]`);
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
}
if (tool.related_software && tool.related_software.length > 0) {
lines.push(`related_software: [${tool.related_software.map(s => `"${s}"`).join(', ')}]`);
}
return lines.join('\n');
}
validateForm() {
const errors = [];
const formData = new FormData(this.elements.form);
const tool = {
name: formData.get('name') || 'Tool Name',
type: formData.get('type') || 'software',
description: formData.get('description') || 'Tool description',
domains: formData.getAll('domains'),
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel') || 'intermediate',
url: formData.get('url') || 'https://example.com'
};
if (formData.get('icon')) {
tool.icon = formData.get('icon');
const name = formData.get('name')?.trim();
if (!name) {
errors.push('Tool name is required');
}
if (tool.type === 'software') {
tool.platforms = formData.getAll('platforms');
tool.license = formData.get('license') || 'Unknown';
if (formData.get('accessType')) {
tool.accessType = formData.get('accessType');
}
const domainAgnostic = formData.getAll('domainAgnostic');
if (domainAgnostic.length > 0) {
tool['domain-agnostic-software'] = domainAgnostic;
const description = formData.get('description')?.trim();
if (!description) {
errors.push('Description is required');
} else if (description.length < 10) {
errors.push('Description must be at least 10 characters long');
}
const skillLevel = formData.get('skillLevel');
if (!skillLevel) {
errors.push('Skill level is required');
}
const type = formData.get('type');
if (!type) {
errors.push('Type is required');
}
const url = formData.get('url')?.trim();
if (!url) {
errors.push('Primary URL is required');
} else {
try {
new URL(url);
} catch {
errors.push('Primary URL must be a valid URL');
}
}
if (formData.has('knowledgebase')) {
tool.knowledgebase = true;
if (type === 'software') {
const platforms = formData.getAll('platforms');
if (platforms.length === 0) {
errors.push('At least one platform is required for software');
}
const license = formData.get('license')?.trim();
if (!license) {
errors.push('License is required for software');
}
}
const tags = formData.get('tags');
if (tags) {
tool.tags = tags.split(',').map(t => t.trim()).filter(Boolean);
return errors;
}
showValidationErrors(errors) {
if (errors.length === 0) {
this.elements.validationErrors.style.display = 'none';
return;
}
const relatedConcepts = formData.getAll('relatedConcepts');
if (relatedConcepts.length > 0) {
tool.related_concepts = relatedConcepts;
}
this.elements.errorList.innerHTML = '';
const yaml = this.generateYAML(tool);
this.elements.yamlPreview.textContent = yaml;
errors.forEach(error => {
const li = document.createElement('li');
li.textContent = error;
this.elements.errorList.appendChild(li);
});
} catch (error) {
console.error('[FORM] YAML preview error:', error);
this.elements.yamlPreview.textContent = '# Error generating preview';
}
}
this.elements.validationErrors.style.display = 'block';
generateYAML(tool) {
const lines = [];
lines.push(`name: "${tool.name}"`);
if (tool.icon) lines.push(`icon: "${tool.icon}"`);
lines.push(`type: ${tool.type}`);
lines.push(`description: "${tool.description}"`);
lines.push(`domains: [${tool.domains.map(d => `"${d}"`).join(', ')}]`);
lines.push(`phases: [${tool.phases.map(p => `"${p}"`).join(', ')}]`);
lines.push(`skillLevel: ${tool.skillLevel}`);
lines.push(`url: "${tool.url}"`);
if (tool.platforms && tool.platforms.length > 0) {
lines.push(`platforms: [${tool.platforms.map(p => `"${p}"`).join(', ')}]`);
}
if (tool.license) lines.push(`license: "${tool.license}"`);
if (tool.accessType) lines.push(`accessType: ${tool.accessType}`);
if (tool['domain-agnostic-software']) {
lines.push(`domain-agnostic-software: [${tool['domain-agnostic-software'].map(c => `"${c}"`).join(', ')}]`);
}
if (tool.knowledgebase) lines.push(`knowledgebase: true`);
if (tool.tags && tool.tags.length > 0) {
lines.push(`tags: [${tool.tags.map(t => `"${t}"`).join(', ')}]`);
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return lines.join('\n');
}
validateForm() {
const errors = [];
const formData = new FormData(this.elements.form);
const name = formData.get('name')?.trim();
if (!name) {
errors.push('Tool name is required');
}
const description = formData.get('description')?.trim();
if (!description) {
errors.push('Description is required');
} else if (description.length < 10) {
errors.push('Description must be at least 10 characters long');
}
const skillLevel = formData.get('skillLevel');
if (!skillLevel) {
errors.push('Skill level is required');
}
const type = formData.get('type');
if (!type) {
errors.push('Type is required');
}
const url = formData.get('url')?.trim();
if (!url) {
errors.push('Primary URL is required');
} else {
try {
new URL(url);
} catch {
errors.push('Primary URL must be a valid URL');
}
}
if (type === 'software') {
const platforms = formData.getAll('platforms');
if (platforms.length === 0) {
errors.push('At least one platform is required for software');
}
const license = formData.get('license')?.trim();
if (!license) {
errors.push('License is required for software');
}
}
return errors;
}
showValidationErrors(errors) {
if (errors.length === 0) {
this.elements.validationErrors.style.display = 'none';
return;
}
this.elements.errorList.innerHTML = '';
errors.forEach(error => {
const li = document.createElement('li');
li.textContent = error;
this.elements.errorList.appendChild(li);
});
this.elements.validationErrors.style.display = 'block';
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async handleSubmit() {
console.log('[FORM] Submit handler called!');
@ -597,14 +975,32 @@ showValidationErrors(errors) {
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel'),
url: formData.get('url'),
tags: formData.get('tags') ?
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
tags: []
},
metadata: {
reason: formData.get('reason') || ''
reason: formData.get('reason') || '',
contact: formData.get('contact') || ''
}
};
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
if (formData.get('icon')) submission.tool.icon = formData.get('icon');
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true;
@ -620,13 +1016,6 @@ showValidationErrors(errors) {
}
}
if (submission.tool.type !== 'concept') {
const related = formData.getAll('relatedConcepts');
if (related.length > 0) {
submission.tool.related_concepts = related;
}
}
console.log('[FORM] Sending submission:', submission);
const response = await fetch('/api/contribute/tool', {
@ -681,6 +1070,14 @@ showValidationErrors(errors) {
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
destroy() {
// Clean up autocomplete managers
this.autocompleteManagers.forEach(manager => {
manager.destroy();
});
this.autocompleteManagers.clear();
}
}
function initializeForm() {
@ -707,3 +1104,4 @@ if (document.readyState === 'loading') {
console.log('[FORM] Script loaded successfully');
</script>
</BaseLayout>

View File

@ -6,10 +6,18 @@ import ToolMatrix from '../components/ToolMatrix.astro';
import AIQueryInterface from '../components/AIQueryInterface.astro';
import TargetedScenarios from '../components/TargetedScenarios.astro';
import { getToolsData } from '../utils/dataService.js';
import { withAPIAuth, getAuthRequirementForContext } from '../utils/auth.js';
const data = await getToolsData();
const tools = data.tools;
const phases = data.phases;
const aiAuthRequired = getAuthRequirementForContext('ai');
let aiAuthContext: { authenticated: boolean; userId: string; session?: any; authRequired: boolean; } | null = null;
if (aiAuthRequired) {
aiAuthContext = await withAPIAuth(Astro.request, 'ai');
}
---
<BaseLayout title="~/">
@ -36,17 +44,33 @@ const phases = data.phases;
</div>
</div>
<button id="ai-query-btn" class="btn btn-accent btn-lg ai-primary-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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-Beratung starten
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-2">
<line x1="7" y1="17" x2="17" y2="7"/>
<polyline points="7,7 17,7 17,17"/>
</svg>
</button>
{aiAuthRequired && !aiAuthContext?.authenticated ? (
<div class="ai-auth-required">
<button id="ai-login-btn" class="btn btn-accent btn-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Anmelden für KI-Beratung
</button>
<p style="margin-top: 0.75rem; font-size: 0.875rem; color: var(--color-text-secondary); text-align: center;">
Authentifizierung erforderlich für KI-Features
</p>
</div>
) : (
<button id="ai-query-btn" class="btn btn-accent btn-lg ai-primary-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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-Beratung starten
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-2">
<line x1="7" y1="17" x2="17" y2="7"/>
<polyline points="7,7 17,7 17,17"/>
</svg>
</button>
)}
<div class="ai-features-mini">
<span class="badge badge-secondary">Workflow-Empfehlungen</span>
@ -178,7 +202,39 @@ const phases = data.phases;
<ToolFilters data={data} />
</section>
<AIQueryInterface />
{aiAuthRequired && !aiAuthContext?.authenticated ? (
<section id="ai-interface" class="ai-interface hidden">
<div class="ai-query-section">
<div class="content-center-lg">
<div class="card" style="text-align: center; padding: 3rem; border-left: 4px solid var(--color-accent);">
<div style="margin-bottom: 2rem;">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="1.5" style="margin: 0 auto;">
<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"/>
<circle cx="12" cy="12" r="2"/>
</svg>
</div>
<h2 style="margin-bottom: 1rem; color: var(--color-primary);">Anmeldung erforderlich</h2>
<p style="margin-bottom: 2rem; color: var(--color-text-secondary); line-height: 1.6;">
Für die Nutzung der KI-Beratung ist eine Authentifizierung erforderlich.
Melden Sie sich an, um personalisierten Workflow-Empfehlungen und Tool-Analysen zu erhalten.
</p>
<a href={`/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`}
class="btn btn-accent btn-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Anmelden
</a>
</div>
</div>
</div>
</section>
) : (
<AIQueryInterface />
)}
<section id="tools-grid" style="padding-bottom: 2rem;">
<div class="grid-auto-fit" id="tools-container">
@ -195,7 +251,7 @@ const phases = data.phases;
<ToolMatrix data={data} />
</BaseLayout>
<script define:vars={{ toolsData: data.tools, phases: data.phases }}>
<script define:vars={{ toolsData: data.tools, phases: data.phases, aiAuthRequired: aiAuthRequired, aiAuthenticated: aiAuthContext?.authenticated }}>
window.toolsData = toolsData;
window.selectApproach = function(approach) {
@ -211,14 +267,18 @@ const phases = data.phases;
const selectedCard = document.querySelector(`.approach-card.${approach}`);
if (selectedCard) selectedCard.classList.add('selected');
const methodologySection = document.getElementById('methodology-section');
const targetedSection = document.getElementById('targeted-section');
if (methodologySection) methodologySection.classList.remove('active');
if (targetedSection) targetedSection.classList.remove('active');
if (approach === 'methodology') {
const methodologySection = document.getElementById('methodology-section');
if (methodologySection) {
methodologySection.classList.add('active');
window.scrollToElementById('methodology-section');
}
} else if (approach === 'targeted') {
const targetedSection = document.getElementById('targeted-section');
if (targetedSection) {
targetedSection.classList.add('active');
window.scrollToElementById('targeted-section');
@ -238,9 +298,12 @@ const phases = data.phases;
selectedCard.classList.add('active');
}
const existingPhaseButton = document.querySelector(`[data-phase="${phase}"]`);
if (existingPhaseButton && !existingPhaseButton.classList.contains('active')) {
existingPhaseButton.click();
const phaseSelect = document.getElementById('phase-select');
if (phaseSelect) {
phaseSelect.value = phase;
const changeEvent = new Event('change', { bubbles: true });
phaseSelect.dispatchEvent(changeEvent);
}
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
@ -248,7 +311,9 @@ const phases = data.phases;
gridToggle.click();
}
window.scrollToElementById('tools-grid');
setTimeout(() => {
window.scrollToElementById('tools-grid');
}, 200);
};
document.addEventListener('DOMContentLoaded', () => {
@ -259,12 +324,20 @@ const phases = data.phases;
const filtersSection = document.getElementById('filters-section');
const noResults = document.getElementById('no-results');
const aiQueryBtn = document.getElementById('ai-query-btn');
const aiLoginBtn = document.getElementById('ai-login-btn');
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
console.error('Required DOM elements not found');
return;
}
if (aiLoginBtn) {
aiLoginBtn.addEventListener('click', () => {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = `/api/auth/login?returnTo=${currentUrl}`;
});
}
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', () => {
aiQueryBtn.classList.add('activated');
@ -282,59 +355,80 @@ const phases = data.phases;
});
}
function switchToView(view) {
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');
function switchToView(view) {
console.log('[VIEW] Switching to view:', view);
if (toolsGrid) toolsGrid.style.display = 'none';
if (matrixContainer) matrixContainer.style.display = 'none';
if (aiInterface) aiInterface.style.display = 'none';
if (filtersSection) filtersSection.style.display = 'none';
if (noResults) noResults.style.display = 'none';
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');
switch (view) {
case 'grid':
if (toolsGrid) toolsGrid.style.display = 'block';
if (filtersSection) filtersSection.style.display = 'block';
break;
case 'matrix':
if (matrixContainer) matrixContainer.style.display = 'block';
if (filtersSection) filtersSection.style.display = 'block';
break;
case 'ai':
if (aiInterface) aiInterface.style.display = 'block';
break;
}
}
const methodologySection = document.getElementById('methodology-section');
const targetedSection = document.getElementById('targeted-section');
function hideFilterControls() {
const filterSections = document.querySelectorAll('.filter-section');
filterSections.forEach((section, index) => {
if (index < filterSections.length - 1) {
section.style.display = 'none';
}
});
}
if (toolsGrid) toolsGrid.style.display = 'none';
if (matrixContainer) {
matrixContainer.style.display = 'none';
matrixContainer.classList.add('hidden');
}
if (aiInterface) aiInterface.style.display = 'none';
if (noResults) noResults.style.display = 'none';
function showFilterControls() {
const filterSections = document.querySelectorAll('.filter-section');
const searchInput = document.getElementById('search-input');
const tagCloud = document.querySelector('.tag-cloud');
const tagControls = document.querySelector('.tag-controls');
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
if (methodologySection) methodologySection.classList.remove('active');
if (targetedSection) targetedSection.classList.remove('active');
filterSections.forEach(section => section.style.display = 'block');
switch (view) {
case 'grid':
console.log('[VIEW] Showing grid view');
if (toolsGrid) toolsGrid.style.display = 'block';
if (filtersSection) filtersSection.style.display = 'block';
break;
if (searchInput) searchInput.style.display = 'block';
if (tagCloud) tagCloud.style.display = 'flex';
if (tagControls) tagControls.style.display = 'flex';
case 'matrix':
console.log('[VIEW] Showing matrix view');
if (matrixContainer) {
matrixContainer.style.display = 'block';
matrixContainer.classList.remove('hidden');
}
if (filtersSection) filtersSection.style.display = 'block';
break;
allInputs.forEach(input => input.style.display = 'block');
checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex');
case 'ai':
console.log('[VIEW] Showing AI view');
if (aiAuthRequired && !aiAuthenticated) {
console.log('[AUTH] AI access denied, redirecting to login');
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = `/api/auth/login?returnTo=${currentUrl}`;
return;
}
if (aiInterface) aiInterface.style.display = 'block';
if (filtersSection) {
filtersSection.style.display = 'block';
const filterSections = filtersSection.querySelectorAll('.filter-section');
filterSections.forEach((section, index) => {
if (index === filterSections.length - 1) {
section.style.display = 'block';
} else {
section.style.display = 'none';
}
});
}
break;
default:
console.warn('[VIEW] Unknown view:', view);
}
if (view !== 'ai' && filtersSection) {
const filterSections = filtersSection.querySelectorAll('.filter-section');
filterSections.forEach(section => {
section.style.display = 'block';
});
}
}
window.navigateToGrid = function(toolName) {
@ -416,6 +510,8 @@ const phases = data.phases;
};
function handleSharedURL() {
console.log('[SHARE] Handling shared URL:', window.location.search);
const urlParams = new URLSearchParams(window.location.search);
const toolParam = urlParams.get('tool');
const viewParam = urlParams.get('view');
@ -428,12 +524,18 @@ const phases = data.phases;
return;
}
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
if (!tool) {
console.warn('Shared tool not found:', toolParam);
if (!window.findToolByIdentifier) {
console.error('[SHARE] findToolByIdentifier not available, retrying...');
setTimeout(() => handleSharedURL(), 200);
return;
}
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
if (!tool) {
return;
}
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
@ -455,11 +557,11 @@ const phases = data.phases;
default:
window.navigateToGrid(tool.name);
}
}, 100);
}, 300);
}
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const { tools: filtered, semanticSearch } = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix' || currentView === 'ai') {
@ -468,33 +570,127 @@ const phases = data.phases;
const allToolCards = document.querySelectorAll('.tool-card');
const filteredNames = new Set(filtered.map(tool => tool.name.toLowerCase()));
const toolsContainer = document.getElementById('tools-container');
let visibleCount = 0;
allToolCards.forEach(card => {
const toolName = card.getAttribute('data-tool-name');
if (filteredNames.has(toolName)) {
card.style.display = 'block';
visibleCount++;
} else {
card.style.display = 'none';
if (semanticSearch && filtered.length > 0) {
console.log('[SEMANTIC] Reordering tools by semantic similarity');
const orderedCards = [];
const remainingCards = [];
filtered.forEach(tool => {
const toolName = tool.name.toLowerCase();
const matchingCard = Array.from(allToolCards).find(card =>
card.getAttribute('data-tool-name') === toolName
);
if (matchingCard) {
matchingCard.style.display = 'block';
orderedCards.push(matchingCard);
visibleCount++;
if (tool._semanticSimilarity) {
matchingCard.setAttribute('data-semantic-similarity', tool._semanticSimilarity.toFixed(3));
matchingCard.setAttribute('data-semantic-rank', tool._semanticRank || '');
const header = matchingCard.querySelector('.tool-card-header h3');
if (header && tool._semanticRank <= 3) {
const existingIndicator = header.querySelector('.semantic-rank-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
const indicator = document.createElement('span');
indicator.className = 'semantic-rank-indicator';
indicator.style.cssText = `
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--color-accent);
border-radius: 50%;
margin-left: 0.5rem;
opacity: ${1 - (tool._semanticRank - 1) * 0.3};
`;
indicator.title = `Semantische Relevanz: ${tool._semanticSimilarity.toFixed(3)}`;
header.appendChild(indicator);
}
}
}
});
allToolCards.forEach(card => {
const toolName = card.getAttribute('data-tool-name');
if (!filteredNames.has(toolName)) {
card.style.display = 'none';
remainingCards.push(card);
}
});
const allCards = [...orderedCards, ...remainingCards];
allCards.forEach(card => {
toolsContainer.appendChild(card);
});
} else {
allToolCards.forEach(card => {
const toolName = card.getAttribute('data-tool-name');
card.removeAttribute('data-semantic-similarity');
card.removeAttribute('data-semantic-rank');
const semanticIndicator = card.querySelector('.semantic-rank-indicator');
if (semanticIndicator) {
semanticIndicator.remove();
}
if (filteredNames.has(toolName)) {
card.style.display = 'block';
visibleCount++;
} else {
card.style.display = 'none';
}
});
if (!semanticSearch) {
const originalOrder = Array.from(allToolCards).sort((a, b) => {
const aIndex = Array.from(allToolCards).indexOf(a);
const bIndex = Array.from(allToolCards).indexOf(b);
return aIndex - bIndex;
});
originalOrder.forEach(card => {
toolsContainer.appendChild(card);
});
}
});
}
if (visibleCount === 0) {
noResults.style.display = 'block';
} else {
noResults.style.display = 'none';
}
if (semanticSearch) {
console.log(`[SEMANTIC] Displayed ${visibleCount} tools in semantic order`);
}
});
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
switchToView(view);
if (!event.triggeredByButton) {
switchToView(view);
}
});
window.switchToAIView = () => switchToView('ai');
window.switchToView = switchToView;
handleSharedURL();
// CRITICAL: Handle shared URLs AFTER everything is set up
// Increased timeout to ensure all components and utility functions are loaded
setTimeout(() => {
handleSharedURL();
}, 1000);
});
</script>
</BaseLayout>

View File

@ -108,13 +108,15 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
const isStandalone = !hasAssociatedTool;
const articleUrl = `/knowledgebase/${entry.slug}`;
return (
<article
class="kb-entry card cursor-pointer"
id={`kb-${entry.slug}`}
data-tool-name={entry.title.toLowerCase()}
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
onclick={`window.location.href='/knowledgebase/${entry.slug}'`}
onclick={`window.location.href='${articleUrl}'`}
>
<!-- Card Header -->
<div class="flex-between mb-3">
@ -137,7 +139,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</div>
<div class="flex gap-2 flex-shrink-0" onclick="event.stopPropagation();">
<a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary btn-sm">
<a href={articleUrl} class="btn btn-primary btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
@ -145,7 +147,21 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</svg>
Öffnen
</a>
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
<button
class="btn btn-secondary btn-sm"
onclick="window.shareArticle(this, '${articleUrl}', '${entry.title}')"
title="Artikel teilen"
style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<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>
Teilen
</button>
</div>
</div>
@ -283,5 +299,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
lastScrollY = window.scrollY;
}
});
});
</script>

View File

@ -47,192 +47,435 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
displayTool.projectUrl !== null &&
displayTool.projectUrl !== "" &&
displayTool.projectUrl.trim() !== "";
const currentUrl = Astro.url.href;
---
<BaseLayout title={entry.data.title} description={entry.data.description}>
<article style="max-width: 900px; margin: 0 auto;">
<header style="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);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1;">
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
{displayTool?.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{displayTool.icon}</span>}
<div class="article-layout">
<!-- Article Header -->
<header class="article-header">
<div class="article-header-content">
<div class="article-meta">
<nav class="breadcrumb">
<a href="/knowledgebase" class="breadcrumb-link">Knowledgebase</a>
<span class="breadcrumb-separator">→</span>
<span class="breadcrumb-current">{entry.data.title}</span>
</nav>
<div class="article-tags">
{entry.data.categories?.map((cat: string) => (
<span class="article-tag article-tag-category">{cat}</span>
))}
{entry.data.tags?.map((tag: string) => (
<span class="article-tag">{tag}</span>
))}
</div>
</div>
<div class="article-title-section">
<h1 class="article-title">
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
{entry.data.title}
</h1>
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
{entry.data.description}
</p>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
{isStandalone ? (
<span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>
) : (
<>
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
{!isMethod && !isConcept && !isStandalone && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{!isMethod && !isConcept && !isStandalone && displayTool?.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
</>
)}
<span class="badge badge-error">📖</span>
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
{entry.data.difficulty && (
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
</div>
)}
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
<p class="article-description">{entry.data.description}</p>
</div>
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
</div>
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Typ</strong>
<p style="margin: 0; font-size: 0.9375rem;">
{isStandalone ? 'Allgemeiner Artikel' :
isConcept ? 'Konzept-Artikel' :
isMethod ? 'Methoden-Artikel' :
'Software-Artikel'}
</p>
</div>
{entry.data.categories && entry.data.categories.length > 0 && (
<div style="grid-column: 1 / -1;">
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
{entry.data.categories.map((cat: string) => (
<span class="tag" style="font-size: 0.75rem;">{cat}</span>
))}
<div class="article-metadata-grid">
<div class="metadata-item">
<span class="metadata-label">Typ</span>
<div class="metadata-badges">
{isStandalone ? (
<span class="badge badge-accent">Artikel</span>
) : (
<>
{isConcept && <span class="badge badge-concept">Konzept</span>}
{isMethod && <span class="badge badge-method">Methode</span>}
{!isMethod && !isConcept && <span class="badge badge-primary">Software</span>}
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
</>
)}
<span class="badge badge-error">📖</span>
</div>
</div>
)}
{entry.data.difficulty && (
<div class="metadata-item">
<span class="metadata-label">Schwierigkeit</span>
<span class="metadata-value">{entry.data.difficulty}</span>
</div>
)}
<div class="metadata-item">
<span class="metadata-label">Aktualisiert</span>
<span class="metadata-value">{entry.data.last_updated.toLocaleDateString('de-DE')}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Autor</span>
<span class="metadata-value">{entry.data.author}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Lesezeit</span>
<span class="metadata-value" id="reading-time">~5 min</span>
</div>
</div>
<div class="article-actions">
<button
id="share-article-btn"
class="btn btn-secondary"
onclick="window.shareCurrentArticle(this)"
title="Artikel teilen"
>
<svg width="16" height="16" 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>
Teilen
</button>
<a href="/knowledgebase" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15,18 9,12 15,6"></polyline>
</svg>
Zurück
</a>
</div>
</div>
</header>
<nav style="margin-bottom: 2rem; position: relative; z-index: 50;">
<a href="/knowledgebase" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<polyline points="15,18 9,12 15,6"></polyline>
</svg>
Zurück zur Knowledgebase
</a>
</nav>
<!-- Main Content Area -->
<div class="article-content-wrapper">
<!-- Sidebar Navigation (will be populated by JS) -->
<aside class="article-sidebar">
<!-- TOC will be inserted here by JavaScript -->
</aside>
<div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;">
<Content />
</div>
</div>
<!-- Article Content -->
<main class="article-main">
<article class="article-content">
<div class="markdown-content">
<Content />
</div>
</article>
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
</h3>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
{isStandalone ? (
<a href="/knowledgebase" class="btn btn-primary">
<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 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Weitere Artikel
</a>
) : (
<>
{isConcept ? (
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Mehr erfahren
</a>
) : isMethod ? (
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Zur Methode
</a>
) : (
<>
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
<!-- Article Footer -->
<footer class="article-footer">
<div class="article-footer-actions">
<h3>Tool-Aktionen</h3>
<div class="footer-actions-grid">
{isStandalone ? (
<a href="/knowledgebase" class="btn btn-primary">
<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"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
Software-Homepage
Weitere Artikel
</a>
{hasValidProjectUrl && (
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<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"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
</svg>
Zugreifen
</a>
)}
</>
)}
</>
)}
) : (
<>
{isConcept ? (
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-concept">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Mehr erfahren
</a>
) : isMethod ? (
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-method">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Zur Methode
</a>
) : (
<>
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Homepage
</a>
{hasValidProjectUrl && (
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
</svg>
Zugreifen
</a>
)}
</>
)}
</>
)}
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
<div style="margin-left: auto;">
<details style="position: relative;">
<summary class="btn btn-secondary" style="cursor: pointer; list-style: none;">
<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"/>
<a href="/" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
Verwandte Tools ({relatedTools.length})
</summary>
<div style="position: absolute; top: 100%; left: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 200px; z-index: 100; box-shadow: var(--shadow-lg);">
Zur Hauptseite
</a>
</div>
</div>
{relatedTools.length > 0 && (
<div class="related-tools">
<h3>Verwandte Tools</h3>
<div class="related-tools-grid">
{relatedTools.map((tool: any) => (
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer"
style="display: block; padding: 0.5rem; border-radius: 0.25rem; text-decoration: none; color: var(--color-text); margin-bottom: 0.25rem;"
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
onmouseout="this.style.backgroundColor='transparent'">
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
{tool.name}
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="related-tool-card">
{tool.icon && <span class="tool-icon">{tool.icon}</span>}
<span class="tool-name">{tool.name}</span>
</a>
))}
</div>
</details>
</div>
)}
<a href="/" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
Zur Hauptseite
</a>
</div>
</div>
)}
</footer>
</main>
</div>
</article>
</div>
<script>
/** @template {Element} T
* @param {string} sel
* @param {Document|Element} [root=document]
* @returns {T|null} */
const qs = (sel, root = document) => root.querySelector(sel);
/** @template {Element} T
* @param {string} sel
* @param {Document|Element} [root=document]
* @returns {T[]} */
const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
function calculateReadingTime() {
/** @type {HTMLElement|null} */
const content = qs('.markdown-content');
if (!content) return;
const text = (content.textContent || content.innerText || '').trim();
if (!text) return;
const wordsPerMinute = 200;
const words = text.split(/\s+/).length;
const readingTime = Math.ceil(words / wordsPerMinute);
const readingTimeElement = document.getElementById('reading-time');
if (readingTimeElement) {
readingTimeElement.textContent = `~${readingTime} min`;
}
}
function generateSidebarTOC() {
/** @type {HTMLElement|null} */
const article = qs('.markdown-content');
/** @type {HTMLElement|null} */
const sidebar = qs('.article-sidebar');
/** @type {HTMLElement|null} */
const main = qs('.article-main');
if (!article || !sidebar || !main) return;
/** @type {HTMLHeadingElement[]} */
const headings = qsa('h1, h2, h3, h4, h5, h6', article);
if (headings.length < 2) {
sidebar.style.display = 'none';
main.style.maxWidth = '100%';
return;
}
headings.forEach((h, i) => {
if (!h.id) h.id = `heading-${i}`;
});
const tocHTML = `
<div class="sidebar-toc">
<h3 class="toc-title">Inhaltsverzeichnis</h3>
<nav class="toc-navigation">
${headings.map((h) => {
const level = parseInt(h.tagName.slice(1), 10);
const text = (h.textContent || '').trim();
const id = h.id;
return `
<a href="#${id}" class="toc-item toc-level-${level}" data-heading="${id}">
${text}
</a>
`;
}).join('')}
</nav>
</div>
`;
sidebar.innerHTML = tocHTML;
/** @type {HTMLAnchorElement[]} */
const tocItems = qsa('.toc-item', sidebar);
tocItems.forEach((item) => {
item.addEventListener('click', (e) => {
e.preventDefault();
const href = item.getAttribute('href');
if (!href || !href.startsWith('#')) return;
const target = document.getElementById(href.slice(1));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
tocItems.forEach((i) => i.classList.remove('active'));
item.classList.add('active');
}
});
});
const updateActiveSection = () => {
const scrollY = window.scrollY + 100;
let currentId = null;
for (let i = 0; i < headings.length; i++) {
const h = headings[i];
const rect = h.getBoundingClientRect();
const absTop = rect.top + window.pageYOffset;
if (absTop <= scrollY) currentId = h.id;
}
if (currentId) {
tocItems.forEach((i) => i.classList.remove('active'));
/** @type {HTMLAnchorElement|null} */
const active = qs(`.toc-item[data-heading="${currentId}"]`, sidebar);
if (active) active.classList.add('active');
}
};
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
updateActiveSection();
ticking = false;
});
});
updateActiveSection();
}
function enhanceCodeCopy() {
/** @type {HTMLPreElement[]} */
const pres = qsa('.markdown-content pre');
pres.forEach((pre) => {
if (pre.dataset.copyEnhanced === 'true') return;
pre.dataset.copyEnhanced = 'true';
pre.style.position ||= 'relative';
// Try to find an existing copy button we can reuse
let btn =
pre.querySelector('.copy-btn') || // our class
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
// If there is an "old" button that is NOT ours, prefer to reuse it by giving it our class.
if (btn && !btn.classList.contains('copy-btn')) {
btn.classList.add('copy-btn');
}
// If no button at all, create one
if (!btn) {
btn = document.createElement('button');
btn.type = 'button';
btn.className = 'copy-btn';
btn.setAttribute('aria-label', 'Code kopieren');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span>Copy</span>
`;
pre.appendChild(btn);
}
// If there is a SECOND old button lingering (top-left in your case), hide it
const possibleOldButtons = pre.querySelectorAll(
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
);
possibleOldButtons.forEach((b) => {
if (b !== btn) b.style.display = 'none';
});
// Success pill
if (!pre.querySelector('.copied-pill')) {
const pill = document.createElement('div');
pill.className = 'copied-pill';
pill.textContent = '✓ Kopiert';
pre.appendChild(pill);
}
// Screen reader live region
if (!pre.querySelector('.sr-live')) {
const live = document.createElement('div');
live.className = 'sr-live';
live.setAttribute('aria-live', 'polite');
Object.assign(live.style, {
position: 'absolute', width: '1px', height: '1px', padding: '0', margin: '-1px',
overflow: 'hidden', clip: 'rect(0,0,0,0)', border: '0'
});
pre.appendChild(live);
}
btn.addEventListener('click', async () => {
const code = pre.querySelector('code');
const text = code ? code.innerText : pre.innerText;
const live = pre.querySelector('.sr-live');
const copyText = async (t) => {
try {
await navigator.clipboard.writeText(t);
return true;
} catch {
const ta = document.createElement('textarea');
ta.value = t;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
}
};
const ok = await copyText(text);
pre.dataset.copied = ok ? 'true' : 'false';
if (live) live.textContent = ok ? 'Code in die Zwischenablage kopiert' : 'Kopieren fehlgeschlagen';
window.setTimeout(() => { pre.dataset.copied = 'false'; }, 1200);
});
});
}
// keep your existing DOMContentLoaded; just ensure this is called
document.addEventListener('DOMContentLoaded', () => {
// existing:
calculateReadingTime();
generateSidebarTOC();
// new/updated:
enhanceCodeCopy();
});
</script>
</BaseLayout>

407
src/styles/auditTrail.css Normal file
View File

@ -0,0 +1,407 @@
/* src/styles/auditTrail.css - Reusable Audit Trail Styles */
.audit-trail-container {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-left: 4px solid var(--color-accent);
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
transition: var(--transition-fast);
}
.audit-trail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.audit-trail-header.clickable {
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
margin: -0.25rem;
transition: var(--transition-fast);
}
.audit-trail-header.clickable:hover {
background-color: var(--color-bg-tertiary);
}
.audit-trail-title {
display: flex;
align-items: center;
gap: 1.5rem;
flex: 1;
}
.audit-icon {
display: flex;
align-items: center;
gap: 0.75rem;
}
.audit-icon-gradient {
width: 24px;
height: 24px;
background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: bold;
}
.audit-icon h4 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-accent);
}
.audit-stats {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.stat-item {
display: flex;
align-items: center;
gap: 0.375rem;
}
.stat-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.stat-time {
background-color: var(--color-accent);
}
.toggle-icon {
transition: transform var(--transition-medium);
color: var(--color-text-secondary);
}
.audit-trail-details {
display: block;
transition: all var(--transition-medium);
overflow: hidden;
}
.audit-trail-details.collapsed {
display: none;
}
.audit-summary {
background-color: var(--color-bg-tertiary);
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
}
.summary-header {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-accent);
margin-bottom: 0.75rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
.summary-stat {
text-align: center;
}
.summary-value {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.summary-value.success {
color: var(--color-accent);
}
.summary-value.warning {
color: var(--color-warning);
}
.summary-label {
font-size: 0.6875rem;
color: var(--color-text-secondary);
}
.insights-section {
margin-top: 0.75rem;
}
.insights-header {
font-size: 0.6875rem;
font-weight: 600;
margin-bottom: 0.375rem;
}
.insights-header.success {
color: var(--color-accent);
}
.insights-header.warning {
color: var(--color-warning);
}
.insights-list {
margin: 0;
padding-left: 1rem;
font-size: 0.625rem;
line-height: 1.4;
}
.insights-list li {
margin-bottom: 0.25rem;
}
.audit-process-flow {
display: grid;
gap: 1rem;
}
.phase-group {
position: relative;
}
.phase-group:not(.last-phase)::after {
content: '';
position: absolute;
left: 13px;
bottom: -8px;
width: 2px;
height: 16px;
background: linear-gradient(to bottom, var(--color-border) 0%, transparent 100%);
}
.phase-icon {
font-size: 1rem;
}
.phase-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.phase-divider {
flex: 1;
height: 1px;
background-color: var(--color-border);
}
.phase-stats {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.confidence-bar {
width: 48px;
height: 8px;
background-color: var(--color-bg-tertiary);
border-radius: 4px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
border-radius: 4px;
transition: var(--transition-fast);
}
.confidence-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
min-width: 28px;
}
.phase-entries {
margin-left: 1.5rem;
display: grid;
gap: 0.5rem;
}
.audit-entry {
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
padding: 0.75rem;
transition: var(--transition-fast);
}
.audit-entry:hover {
background-color: var(--color-bg-secondary);
}
.entry-main {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.entry-action {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.entry-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.confidence-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.confidence-value {
min-width: 28px;
text-align: right;
}
.processing-time {
min-width: 40px;
text-align: right;
}
.entry-details {
font-size: 0.75rem;
color: var(--color-text-secondary);
padding-top: 0.5rem;
border-top: 1px solid var(--color-border);
}
.detail-item {
margin-bottom: 0.25rem;
word-break: break-word;
}
.detail-item:last-child {
margin-bottom: 0;
}
.technical-toggle {
text-align: center;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
}
.technical-toggle-btn {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 0.75rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
transition: var(--transition-fast);
}
.technical-toggle-btn:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text);
}
.technical-details {
margin-top: 1rem;
display: grid;
gap: 0.5rem;
transition: all var(--transition-medium);
}
.technical-details.collapsed {
display: none;
}
.technical-entry {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
padding: 0.75rem;
font-size: 0.75rem;
}
.technical-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
}
.technical-phase {
font-weight: 600;
color: var(--color-primary);
}
.technical-time {
color: var(--color-text-secondary);
}
.technical-content {
display: grid;
gap: 0.375rem;
}
.technical-row {
color: var(--color-text-secondary);
word-break: break-word;
}
/* Responsive Design */
@media (width <= 768px) {
.audit-stats {
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
.summary-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.phase-divider {
display: none;
}
.entry-main {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.technical-header {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
}

121
src/styles/autocomplete.css Normal file
View File

@ -0,0 +1,121 @@
/* ============================================================================
AUTOCOMPLETE COMPONENT STYLES
============================================================================ */
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-option {
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
}
.autocomplete-option:last-child {
border-bottom: none;
}
.autocomplete-option:hover {
background-color: var(--color-bg-secondary);
}
.autocomplete-item {
font-size: 0.875rem;
color: var(--color-text);
}
.autocomplete-no-results {
padding: 0.75rem;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.875rem;
font-style: italic;
}
.autocomplete-selected {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
}
.autocomplete-tag {
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
animation: fadeInScale 0.2s ease-out;
}
.autocomplete-remove {
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.15s ease;
}
.autocomplete-remove:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* Animation for tag appearance */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Ensure autocomplete container has relative positioning */
.autocomplete-container {
position: relative;
}
/* Custom scrollbar for autocomplete dropdown */
.autocomplete-dropdown::-webkit-scrollbar {
width: 6px;
}
.autocomplete-dropdown::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.autocomplete-dropdown::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.autocomplete-dropdown::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}

View File

@ -14,72 +14,7 @@
padding: 0;
}
/* ===================================================================
2. CSS VARIABLES AND THEMING
================================================================= */
:root {
/* Light Theme Colors */
--color-bg: #fff;
--color-bg-secondary: #f8fafc;
--color-bg-tertiary: #e2e8f0;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-border: #cbd5e1;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-accent: #059669;
--color-accent-hover: #047857;
--color-warning: #d97706;
--color-error: #dc2626;
/* Enhanced card type colors */
--color-hosted: #7c3aed;
--color-hosted-bg: #f3f0ff;
--color-oss: #059669;
--color-oss-bg: #ecfdf5;
--color-method: #0891b2;
--color-method-bg: #f0f9ff;
--color-concept: #ea580c;
--color-concept-bg: #fff7ed;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 5%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 10%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 10%);
/* Transitions */
--transition-fast: all 0.2s ease;
--transition-medium: all 0.3s ease;
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-secondary: #1e293b;
--color-bg-tertiary: #334155;
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #475569;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-accent: #10b981;
--color-accent-hover: #34d399;
--color-warning: #f59e0b;
--color-error: #f87171;
--color-hosted: #a855f7;
--color-hosted-bg: #2e1065;
--color-oss: #10b981;
--color-oss-bg: #064e3b;
--color-method: #0891b2;
--color-method-bg: #164e63;
--color-concept: #f97316;
--color-concept-bg: #7c2d12;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 30%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 40%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 50%);
}
/* ===================================================================
3. BASE HTML ELEMENTS
@ -747,11 +682,13 @@ input[type="checkbox"] {
================================================================= */
.tool-card {
height: 300px;
min-height: 300px;
max-height: 350px;
display: flex;
flex-direction: column;
padding: 1.25rem;
cursor: pointer;
overflow: hidden;
}
.tool-card-header {
@ -760,6 +697,7 @@ input[type="checkbox"] {
align-items: flex-start;
min-height: 2.5rem;
margin-bottom: 0.75rem;
flex-shrink: 0;
}
.tool-card-header h3 {
@ -788,6 +726,7 @@ input[type="checkbox"] {
font-size: 0.875rem;
margin-bottom: 0.5rem;
word-break: break-word;
flex-shrink: 0;
}
.tool-card-metadata {
@ -796,6 +735,7 @@ input[type="checkbox"] {
gap: 1rem;
margin-bottom: 0.75rem;
line-height: 1;
flex-shrink: 0;
}
.metadata-item {
@ -824,10 +764,11 @@ input[type="checkbox"] {
flex-wrap: wrap;
gap: 0.25rem;
max-height: 3.5rem;
min-height: 0;
overflow: hidden;
position: relative;
margin-bottom: 1rem;
flex-shrink: 0;
flex: 0 0 3.5rem;
}
.tool-tags-container::after {
@ -861,6 +802,8 @@ input[type="checkbox"] {
.tool-card-buttons {
margin-top: auto;
flex-shrink: 0;
flex-basis: auto;
min-height: 0;
}
.button-row {
@ -1689,6 +1632,156 @@ input[type="checkbox"] {
font-size: 0.75rem;
}
/* ===================================================================
SEMANTIC SEARCH STYLES - INLINE VERSION (REPLACE EXISTING)
================================================================= */
/* Search row with inline semantic toggle */
.search-row {
display: flex;
align-items: center;
gap: 1rem;
}
.search-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
/* Inline semantic search toggle */
.semantic-search-inline {
flex-shrink: 0;
}
.semantic-toggle-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
transition: var(--transition-fast);
user-select: none;
white-space: nowrap;
}
.semantic-toggle-wrapper:hover {
background-color: var(--color-bg-tertiary);
border-color: var(--color-accent);
}
.semantic-toggle-wrapper input[type="checkbox"] {
display: none;
}
.semantic-checkbox-custom {
width: 16px;
height: 16px;
border: 2px solid var(--color-border);
border-radius: 0.25rem;
background-color: var(--color-bg);
transition: var(--transition-fast);
position: relative;
flex-shrink: 0;
}
.semantic-checkbox-custom::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 8px;
height: 8px;
background-color: white;
border-radius: 0.125rem;
transition: var(--transition-fast);
}
.semantic-toggle-wrapper input:checked + .semantic-checkbox-custom {
background-color: var(--color-accent);
border-color: var(--color-accent);
}
.semantic-toggle-wrapper input:checked + .semantic-checkbox-custom::after {
transform: translate(-50%, -50%) scale(1);
}
.semantic-toggle-label {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text);
}
.semantic-toggle-label svg {
width: 14px;
height: 14px;
color: var(--color-accent);
flex-shrink: 0;
}
/* Semantic Status Display */
.semantic-status {
margin-top: 0.75rem;
padding: 0.375rem 0.75rem;
background-color: var(--color-accent);
color: white;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
text-align: center;
opacity: 0.9;
}
.semantic-results-count {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
}
.semantic-results-count::before {
content: '🧠';
font-size: 0.875rem;
}
/* Responsive adjustments */
@media (width <= 768px) {
.search-row {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.semantic-toggle-wrapper {
justify-content: center;
padding: 0.625rem;
}
.semantic-toggle-label {
font-size: 0.875rem;
}
}
@media (width <= 480px) {
.semantic-toggle-label span {
display: none; /* Hide "Semantisch" text on very small screens */
}
.semantic-toggle-wrapper {
padding: 0.5rem;
min-width: 40px;
justify-content: center;
}
}
/* ===================================================================
16. AI INTERFACE (CONSOLIDATED)
================================================================= */
@ -1971,6 +2064,9 @@ input[type="checkbox"] {
}
.phase-info {
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
@ -3434,6 +3530,16 @@ footer {
width: 14px;
height: 14px;
}
.tool-card {
min-height: 280px;
max-height: 320px;
}
.tool-tags-container {
max-height: 3rem;
flex: 0 0 3rem;
}
}
@media (width <= 640px) {
@ -3593,179 +3699,30 @@ footer {
}
}
/* ===================================================================
MIGRATION UTILITIES - Additional classes for inline style migration
================================================================= */
/* Alignment utilities */
.align-middle { vertical-align: middle; }
/* Font style utilities */
.italic { font-style: italic; }
/* Border width utilities */
.border-2 { border-width: 2px; }
/* Border color utilities */
.border-accent { border-color: var(--color-accent); }
/* Card variants for complex backgrounds */
.card-warning {
background: linear-gradient(135deg, var(--color-warning) 0%, var(--color-accent) 100%);
color: white;
}
/* Header variants for complex gradient combinations */
.header-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
color: white;
}
/* Ensure we have all text size variants */
.text-2xl { font-size: 1.5rem; }
/* Additional spacing utilities that might be missing */
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.my-6 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
/* Flex utilities that might be missing */
.flex-1 { flex: 1; }
/* Additional rounded variants */
.rounded-xl { border-radius: 0.75rem; }
/* ===================================================================
23. MARKDOWN CONTENT
================================================================= */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 2rem;
margin-bottom: 1rem;
}
.markdown-content h1:first-child,
.markdown-content h2:first-child,
.markdown-content h3:first-child {
margin-top: 0;
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content li {
margin-bottom: 0.5rem;
}
.markdown-content pre {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin: 1.5rem 0;
overflow-x: auto;
font-size: 0.875rem;
}
.markdown-content code:not(pre code) {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
padding: 0.125rem 0.375rem;
font-size: 0.875rem;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background-color: var(--color-bg);
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid var(--color-border);
}
.markdown-content th,
.markdown-content td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
.markdown-content th {
background-color: var(--color-bg-secondary);
font-weight: 600;
}
.markdown-content blockquote {
border-left: 4px solid var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 0.5rem 0.5rem 0;
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}
/* ===================================================================
26. ENHANCED AUDIT TRAIL STYLES
================================================================= */
.audit-process-flow {
position: relative;
}
.phase-group {
position: relative;
}
.phase-group:not(:last-child)::after {
content: '';
position: absolute;
left: 13px;
bottom: -8px;
width: 2px;
height: 16px;
background: linear-gradient(to bottom, var(--color-border) 0%, transparent 100%);
}
.toggle-icon {
transition: transform 0.2s ease;
}
/* Hover effects for audit entries */
.audit-trail-details .hover\\:bg-secondary:hover {
background-color: var(--color-bg-secondary);
}
/* Responsive adjustments for audit trail */
@media (width <= 768px) {
.audit-process-flow .grid-cols-3 {
grid-template-columns: 1fr;
gap: 1rem;
}
.phase-group .flex {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}

View File

@ -0,0 +1,661 @@
/* ==========================================================================
0) FOUNDATIONS
- Typography, rhythm, utility tokens, base elements
========================================================================== */
:root {
/* Optional: gentle defaults if not already defined upstream */
--radius-xs: 0.25rem;
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--shadow-xs: 0 1px 2px rgba(0,0,0,0.06);
--shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
--shadow-md: 0 6px 18px rgba(0,0,0,0.10);
--shadow-lg: 0 12px 30px rgba(0,0,0,0.16);
--ease-quick: 0.2s ease;
}
/* Base text container for articles */
:where(.markdown-content) {
max-width: 70ch;
margin: 0 auto;
font-size: 1.125rem;
line-height: 1.8;
color: var(--color-text);
}
:where(.markdown-content) * {
scroll-margin-top: 100px;
}
/* ==========================================================================
1) HEADINGS & TEXT
========================================================================== */
:where(.markdown-content) h1,
:where(.markdown-content) h2,
:where(.markdown-content) h3,
:where(.markdown-content) h4,
:where(.markdown-content) h5,
:where(.markdown-content) h6 {
font-weight: 700;
line-height: 1.25;
color: var(--color-text);
margin: 0;
}
:where(.markdown-content) h1 {
font-size: 2.5rem;
margin: 3rem 0 1.5rem 0;
border-bottom: 3px solid var(--color-primary);
padding-bottom: 0.5rem;
}
:where(.markdown-content) h2 {
font-size: 2rem;
margin: 2.5rem 0 1rem 0;
border-bottom: 2px solid var(--color-border);
padding-bottom: 0.375rem;
}
:where(.markdown-content) h3 {
font-size: 1.5rem;
margin: 2rem 0 0.75rem 0;
color: var(--color-primary);
}
:where(.markdown-content) h4 {
font-size: 1.25rem;
margin: 1.5rem 0 0.5rem 0;
}
:where(.markdown-content) h5,
:where(.markdown-content) h6 {
font-size: 1.125rem;
margin: 1rem 0 0.5rem 0;
font-weight: 600;
}
:where(.markdown-content) p {
margin: 0 0 1.5rem 0;
text-align: justify;
hyphens: auto;
}
:where(.markdown-content) strong { font-weight: 700; color: var(--color-text); }
:where(.markdown-content) em { font-style: italic; color: var(--color-text-secondary); }
/* Links with refined hover state */
:where(.markdown-content) a {
color: var(--color-primary);
text-decoration: underline;
text-decoration-color: transparent;
transition: var(--ease-quick);
}
:where(.markdown-content) a:hover {
text-decoration-color: var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 0.125rem 0.25rem;
border-radius: var(--radius-xs);
}
/* External link indicator */
:where(.markdown-content) a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"])::after {
content: " ↗";
font-size: 0.875rem;
color: var(--color-text-secondary);
}
/* ==========================================================================
2) LISTS, DEFINITIONS, TABLES, QUOTES
========================================================================== */
:where(.markdown-content) ul,
:where(.markdown-content) ol {
margin: 1.5rem 0;
padding-left: 2rem;
}
:where(.markdown-content) li {
margin-bottom: 0.75rem;
line-height: 1.2;
}
:where(.markdown-content) li > ul,
:where(.markdown-content) li > ol {
margin: 0.5rem 0;
}
:where(.markdown-content) dl { margin: 1.5rem 0; }
:where(.markdown-content) dt {
font-weight: 700;
margin: 1rem 0 0.5rem 0;
color: var(--color-primary);
}
:where(.markdown-content) dd {
margin-left: 1.5rem;
margin-bottom: 0.75rem;
padding-left: 1rem;
border-left: 2px solid var(--color-border);
}
:where(.markdown-content) table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background-color: var(--color-bg);
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
}
:where(.markdown-content) th,
:where(.markdown-content) td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
:where(.markdown-content) th {
background-color: var(--color-bg-secondary);
font-weight: 600;
}
:where(.markdown-content) blockquote {
border-left: 4px solid var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
:where(.markdown-content) hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}
/* ==========================================================================
3) CODE & INLINE CODE
========================================================================== */
:where(.markdown-content) pre {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.5rem;
margin: 2rem 0;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
position: relative;
box-shadow: var(--shadow-sm);
padding-left: 1.5rem;
}
:where(.markdown-content) pre::before {
content: attr(data-language);
position: absolute;
top: 0.5rem;
left: 0.75rem;
right: auto;
z-index: 1;
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
font-size: 0.75rem;
}
:where(.markdown-content) pre[data-language]::before {
top: 0.5rem;
left: 0.75rem;
position: absolute;
z-index: 1;
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
line-height: 1;
}
:where(.markdown-content) code:not(pre code) {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
font-family: 'SF Mono','Monaco','Menlo','Consolas',monospace;
color: var(--color-text);
}
/* Language badges (keep if your renderer sets data-language) */
:where(.markdown-content) pre[data-language="bash"]::before { content: "BASH"; color: #4EAA25; }
:where(.markdown-content) pre[data-language="javascript"]::before { content: "JS"; color: #F7DF1E; }
:where(.markdown-content) pre[data-language="python"]::before { content: "PYTHON"; color: #3776AB; }
:where(.markdown-content) pre[data-language="sql"]::before { content: "SQL"; color: #336791; }
:where(.markdown-content) pre[data-language="yaml"]::before { content: "YAML"; color: #CB171E; }
:where(.markdown-content) pre[data-language="json"]::before { content: "JSON"; color: #000000; }
/* Keyboard UI hints */
:where(.markdown-content) kbd {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-xs);
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
font-family: monospace;
box-shadow: var(--shadow-xs);
margin: 0 0.125rem;
}
:where(.markdown-content) .copy-btn {
position: absolute;
top: 0.5rem; right: 0.5rem;
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.35rem 0.6rem;
border: 1px solid var(--color-border);
background: var(--color-bg);
border-radius: var(--radius-sm);
font-size: 0.75rem;
cursor: pointer;
box-shadow: var(--shadow-sm);
line-height: 1;
z-index: 2;
transition: var(--transition-fast);
color: var(--color-text);
}
:where(.markdown-content) .copy-btn:hover {
background: var(--color-bg-secondary);
}
:where(.markdown-content) .copy-btn:focus-visible {
outline: 2px solid color-mix(in srgb, var(--color-primary) 60%, transparent);
outline-offset: 2px;
}
:where(.markdown-content) .copy-btn svg {
width: 14px; height: 14px;
}
[data-theme="dark"] :where(.markdown-content) .copy-btn {
background: var(--color-bg-secondary);
border-color: color-mix(in srgb, var(--color-border) 80%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-bg-tertiary) 40%, transparent), var(--shadow-sm);
}
:where(.markdown-content) pre .copied-pill {
position: absolute;
top: 0.5rem; right: 0.5rem;
padding: 0.35rem 0.6rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
background: var(--color-primary);
color: #fff;
opacity: 0; transform: translateY(-6px) scale(0.98);
pointer-events: none;
z-index: 2;
}
/* Success state driven by [data-copied="true"] on <pre> */
:where(.markdown-content) pre[data-copied="true"] {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 30%, transparent) inset, var(--shadow-sm);
}
:where(.markdown-content) pre[data-copied="true"] .copy-btn { opacity: 0; }
:where(.markdown-content) pre[data-copied="true"] .copied-pill {
opacity: 1; transform: translateY(0) scale(1);
}
/* Smooth but gentle motion */
@media (prefers-reduced-motion: no-preference) {
:where(.markdown-content) pre,
:where(.markdown-content) .copy-btn,
:where(.markdown-content) pre .copied-pill {
transition: 180ms ease;
}
}
/* ==========================================================================
4) CALLOUTS, STEPS, HIGHLIGHT BOX
========================================================================== */
:where(.markdown-content) .callout {
border-left: 4px solid;
border-radius: 0.5rem;
margin: 2rem 0;
padding: 1.25rem 1.5rem;
background-color: var(--color-bg-secondary);
border-color: var(--color-border);
box-shadow: var(--shadow-sm);
}
:where(.markdown-content) .callout-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: 0.9375rem;
}
:where(.markdown-content) .callout-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
/* Callout variants */
:where(.markdown-content) .callout-note {
border-left-color: var(--color-primary);
background-color: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-note .callout-header { color: var(--color-primary); }
:where(.markdown-content) .callout-tip {
border-left-color: var(--color-accent);
background-color: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-tip .callout-header { color: var(--color-accent); }
:where(.markdown-content) .callout-warning {
border-left-color: var(--color-warning);
background-color: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-warning .callout-header { color: var(--color-warning); }
:where(.markdown-content) .callout-danger {
border-left-color: var(--color-error);
background-color: color-mix(in srgb, var(--color-error) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-danger .callout-header { color: var(--color-error); }
/* Highlight feature box */
:where(.markdown-content) .highlight-box {
background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%);
color: white;
padding: 2rem;
border-radius: 1rem;
margin: 2rem 0;
text-align: center;
box-shadow: var(--shadow-md);
}
:where(.markdown-content) .highlight-box h3 { margin: 0; color: white; }
/* Steps */
:where(.markdown-content) .steps { counter-reset: step-counter; margin: 2rem 0; }
:where(.markdown-content) .step {
counter-increment: step-counter;
margin: 1.25rem 0;
padding: 1.25rem 1.5rem;
background-color: var(--color-bg-secondary);
border-radius: 0.75rem;
border-left: 4px solid var(--color-primary);
position: relative;
}
:where(.markdown-content) .step::before {
content: counter(step-counter);
position: absolute;
left: -0.75rem;
top: 1rem;
background-color: var(--color-primary);
color: white;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 0.875rem;
}
:where(.markdown-content) .step h4 { margin-top: 0; color: var(--color-primary); }
/* ==========================================================================
5) ARTICLE LAYOUT (Header, Meta, Sidebar, Footer)
========================================================================== */
.article-layout {
max-width: 1400px;
margin: 0 auto;
padding: 0 1rem;
}
/* Header */
.article-header {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%);
border: 1px solid var(--color-border);
border-radius: 1rem;
margin-bottom: 2rem;
overflow: hidden;
}
.article-header-content { padding: 2rem; }
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.breadcrumb-link {
color: var(--color-primary);
text-decoration: none;
transition: var(--ease-quick);
}
.breadcrumb-link:hover { text-decoration: underline; }
.breadcrumb-separator { color: var(--color-text-secondary); }
.breadcrumb-current { color: var(--color-text-secondary); font-weight: 500; }
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.article-tag {
padding: 0.25rem 0.75rem;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-secondary);
}
.article-tag-category {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.article-title-section { margin-bottom: 2rem; }
.article-title {
font-size: 2.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
color: var(--color-primary);
line-height: 1.2;
}
.article-icon { margin-right: 1rem; font-size: 2rem; }
.article-description { font-size: 1.25rem; color: var(--color-text-secondary); margin: 0; line-height: 1.5; }
.article-metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
padding: 1.5rem;
background-color: var(--color-bg);
border-radius: 0.75rem;
border: 1px solid var(--color-border);
}
.metadata-item { display: flex; flex-direction: column; gap: 0.5rem; }
.metadata-label {
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--color-text-secondary);
}
.metadata-value { font-size: 0.9375rem; font-weight: 500; color: var(--color-text); }
.metadata-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.article-actions { display: flex; gap: 1rem; justify-content: center; }
/* Content wrapper with sidebar */
.article-content-wrapper {
display: grid;
grid-template-columns: 280px 1fr;
gap: 3rem;
align-items: start;
}
.article-sidebar {
position: sticky;
top: 6rem;
max-height: calc(100vh - 8rem);
overflow-y: auto;
}
/* Sidebar TOC */
.sidebar-toc {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.5rem;
}
.toc-title {
margin: 0 0 1rem 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-primary);
}
.toc-navigation { display: flex; flex-direction: column; gap: 0.25rem; }
.toc-item {
display: block;
padding: 0.5rem 0.75rem;
color: var(--color-text);
text-decoration: none;
border-radius: var(--radius-sm);
font-size: 0.875rem;
line-height: 1.4;
transition: var(--ease-quick);
border-left: 3px solid transparent;
}
.toc-item:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
.toc-item.active {
background-color: var(--color-primary);
color: white;
border-left-color: var(--color-accent);
}
/* Indentation by level (JS sets .toc-level-X) */
.toc-level-1 { padding-left: 0.75rem; font-weight: 600; }
.toc-level-2 { padding-left: 1rem; font-weight: 500; }
.toc-level-3 { padding-left: 1.5rem; }
.toc-level-4 { padding-left: 2rem; font-size: 0.8125rem; }
.toc-level-5 { padding-left: 2.5rem; font-size: 0.8125rem; opacity: 0.85; }
.toc-level-6 { padding-left: 3rem; font-size: 0.8125rem; opacity: 0.8; }
/* Main article column */
.article-main { min-width: 0; max-width: 95ch; }
.article-content { margin-bottom: 3rem; }
/* Footer */
.article-footer {
border-top: 2px solid var(--color-border);
padding-top: 2rem;
margin-top: 3rem;
}
.article-footer h3 { margin: 0 0 1.5rem 0; color: var(--color-primary); }
.footer-actions-grid { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem; }
.footer-actions-grid .btn { flex: 1; min-width: 200px; }
/* Tool button variants (keep since template uses them) */
.btn-concept { background-color: var(--color-concept); color: white; border-color: var(--color-concept); }
.btn-concept:hover { opacity: 0.9; }
.btn-method { background-color: var(--color-method); color: white; border-color: var(--color-method); }
.btn-method:hover { opacity: 0.9; }
.related-tools { margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--color-border); }
.related-tools h3 { margin: 0 0 1rem 0; color: var(--color-text); }
.related-tools-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;
}
.related-tool-card {
display: flex; align-items: center; gap: 0.75rem; padding: 1rem;
background-color: var(--color-bg-secondary); border: 1px solid var(--color-border);
border-radius: 0.5rem; color: var(--color-text); text-decoration: none;
transition: var(--ease-quick);
}
.related-tool-card:hover {
background-color: var(--color-bg-tertiary); border-color: var(--color-primary);
transform: translateY(-1px); box-shadow: var(--shadow-sm);
}
.tool-icon { font-size: 1.25rem; flex-shrink: 0; }
.tool-name { font-weight: 500; font-size: 0.9375rem; }
/* ==========================================================================
6) RESPONSIVE
========================================================================== */
@media (max-width: 1200px) {
.article-content-wrapper { grid-template-columns: 240px 1fr; gap: 2rem; }
}
@media (max-width: 1024px) {
.article-content-wrapper { grid-template-columns: 1fr; gap: 0; }
.article-sidebar { position: static; max-height: none; margin-bottom: 2rem; }
.sidebar-toc { max-width: 500px; margin: 0 auto; }
.article-main { max-width: 100%; }
}
@media (max-width: 768px) {
.article-layout { padding: 0 0.5rem; }
.article-header-content { padding: 1.5rem; }
.article-title { font-size: 2rem; }
.article-description { font-size: 1.125rem; }
.article-metadata-grid { grid-template-columns: 1fr; gap: 1rem; padding: 1rem; }
.article-actions { flex-direction: column; }
.footer-actions-grid { flex-direction: column; }
.footer-actions-grid .btn { min-width: auto; }
:where(.markdown-content) {
font-size: 1rem;
line-height: 1.65;
max-width: 100%;
}
:where(.markdown-content) pre { padding: 1rem; margin: 1.5rem 0; font-size: 0.8125rem; }
:where(.markdown-content) .callout { padding: 1rem; margin: 1.5rem 0; }
:where(.markdown-content) .step { padding: 1rem; margin: 1rem 0; }
:where(.markdown-content) .step::before { left: -0.5rem; top: 0.75rem; }
}
@media (max-width: 480px) {
.article-title { font-size: 1.75rem; }
.article-icon { font-size: 1.5rem; margin-right: 0.75rem; }
.breadcrumb { font-size: 0.8125rem; }
}
/* ==========================================================================
7) PRINT
========================================================================== */
@media print {
/* Hide interactive chrome not used in print */
.article-sidebar { display: none !important; }
.article-actions,
.article-footer .footer-actions-grid { display: none !important; }
/* Expand content */
.article-main { max-width: 100% !important; }
}

62
src/styles/palette.css Normal file
View File

@ -0,0 +1,62 @@
:root {
/* Light Theme Colors */
--color-bg: #fff;
--color-bg-secondary: #f8fafc;
--color-bg-tertiary: #e2e8f0;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-border: #cbd5e1;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-accent: #059669;
--color-accent-hover: #047857;
--color-warning: #d97706;
--color-error: #dc2626;
/* Enhanced card type colors */
--color-hosted: #7c3aed;
--color-hosted-bg: #f3f0ff;
--color-oss: #059669;
--color-oss-bg: #ecfdf5;
--color-method: #0891b2;
--color-method-bg: #f0f9ff;
--color-concept: #ea580c;
--color-concept-bg: #fff7ed;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 5%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 10%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 10%);
/* Transitions */
--transition-fast: all 0.2s ease;
--transition-medium: all 0.3s ease;
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-secondary: #1e293b;
--color-bg-tertiary: #334155;
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #475569;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-accent: #10b981;
--color-accent-hover: #34d399;
--color-warning: #f59e0b;
--color-error: #f87171;
--color-hosted: #a855f7;
--color-hosted-bg: #2e1065;
--color-oss: #10b981;
--color-oss-bg: #064e3b;
--color-method: #0891b2;
--color-method-bg: #164e63;
--color-concept: #f97316;
--color-concept-bg: #7c2d12;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 30%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 40%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 50%);
}

File diff suppressed because it is too large Load Diff

411
src/utils/auditService.ts Normal file
View File

@ -0,0 +1,411 @@
// src/utils/auditService.ts
import 'dotenv/config';
function env(key: string, fallback: string | undefined = undefined): string | undefined {
if (typeof process !== 'undefined' && process.env?.[key] !== undefined) {
return process.env[key];
}
if (typeof import.meta !== 'undefined' && (import.meta as any).env?.[key] !== undefined) {
return (import.meta as any).env[key];
}
return fallback;
}
interface AuditEntry {
timestamp: number;
phase: string;
action: string;
input: any;
output: any;
confidence: number;
processingTimeMs: number;
metadata: Record<string, any>;
}
interface AuditConfig {
enabled: boolean;
detailLevel: 'minimal' | 'standard' | 'verbose';
retentionHours: number;
maxEntries: number;
}
interface CompressedAuditEntry {
timestamp: number;
phase: string;
action: string;
inputSummary: string;
outputSummary: string;
confidence: number;
processingTimeMs: number;
metadata: Record<string, any>;
}
interface ProcessedAuditTrail {
totalTime: number;
avgConfidence: number;
stepCount: number;
highConfidenceSteps: number;
lowConfidenceSteps: number;
phases: Array<{
name: string;
icon: string;
displayName: string;
avgConfidence: number;
totalTime: number;
entries: CompressedAuditEntry[];
}>;
summary: {
analysisQuality: 'excellent' | 'good' | 'fair' | 'poor';
keyInsights: string[];
potentialIssues: string[];
};
}
class AuditService {
private config: AuditConfig;
private tempEntries: AuditEntry[] = [];
private readonly phaseConfig = {
'initialization': { icon: '🚀', displayName: 'Initialisierung' },
'retrieval': { icon: '🔍', displayName: 'Datensuche' },
'selection': { icon: '🎯', displayName: 'Tool-Auswahl' },
'micro-task': { icon: '⚡', displayName: 'Detail-Analyse' },
'validation': { icon: '✓', displayName: 'Validierung' },
'completion': { icon: '✅', displayName: 'Finalisierung' }
};
private readonly actionTranslations = {
'pipeline-start': 'Analyse gestartet',
'embeddings-search': 'Ähnliche Tools gesucht',
'ai-tool-selection': 'Tools automatisch ausgewählt',
'ai-analysis': 'KI-Analyse durchgeführt',
'phase-tool-selection': 'Phasen-Tools evaluiert',
'tool-evaluation': 'Tool-Bewertung erstellt',
'background-knowledge-selection': 'Hintergrundwissen ausgewählt',
'confidence-scoring': 'Vertrauenswertung berechnet',
'pipeline-end': 'Analyse abgeschlossen'
};
constructor() {
this.config = this.loadConfig();
}
private loadConfig(): AuditConfig {
const enabledFlag = env('FORENSIC_AUDIT_ENABLED', 'false');
const detailLevel = env('FORENSIC_AUDIT_DETAIL_LEVEL', 'standard') as 'minimal' | 'standard' | 'verbose';
const retentionHours = parseInt(env('FORENSIC_AUDIT_RETENTION_HOURS', '72') || '72', 10);
const maxEntries = parseInt(env('FORENSIC_AUDIT_MAX_ENTRIES', '50') || '50', 10);
console.log('[AUDIT SERVICE] Configuration loaded:', {
enabled: enabledFlag === 'true',
detailLevel,
retentionHours,
maxEntries,
context: typeof process !== 'undefined' ? 'server' : 'client'
});
return {
enabled: enabledFlag === 'true',
detailLevel,
retentionHours,
maxEntries
};
}
getDebugInfo(): {
config: AuditConfig;
environment: Record<string, any>;
context: string;
} {
const context = typeof process !== 'undefined' ? 'server' : 'client';
return {
config: this.config,
environment: {
FORENSIC_AUDIT_ENABLED: env('FORENSIC_AUDIT_ENABLED'),
FORENSIC_AUDIT_DETAIL_LEVEL: env('FORENSIC_AUDIT_DETAIL_LEVEL'),
FORENSIC_AUDIT_RETENTION_HOURS: env('FORENSIC_AUDIT_RETENTION_HOURS'),
FORENSIC_AUDIT_MAX_ENTRIES: env('FORENSIC_AUDIT_MAX_ENTRIES'),
processEnvKeys: typeof process !== 'undefined' ? Object.keys(process.env).filter(k => k.includes('AUDIT')) : [],
importMetaEnvAvailable: typeof import.meta !== 'undefined' && !!(import.meta as any).env
},
context
};
}
addEntry(
phase: string,
action: string,
input: any,
output: any,
confidence: number,
startTime: number,
metadata: Record<string, any> = {}
): void {
if (!this.config.enabled) return;
const entry: AuditEntry = {
timestamp: Date.now(),
phase,
action,
input: this.compressData(input),
output: this.compressData(output),
confidence: Math.round(confidence),
processingTimeMs: Date.now() - startTime,
metadata
};
this.tempEntries.push(entry);
console.log(`[AUDIT] ${phase}/${action}: ${confidence}% confidence, ${entry.processingTimeMs}ms`);
}
mergeAndClear(auditTrail: AuditEntry[]): void {
if (!this.config.enabled || this.tempEntries.length === 0) return;
auditTrail.unshift(...this.tempEntries);
const entryCount = this.tempEntries.length;
this.tempEntries = [];
console.log(`[AUDIT] Merged ${entryCount} entries into audit trail`);
}
processAuditTrail(rawAuditTrail: AuditEntry[]): ProcessedAuditTrail | null {
if (!this.config.enabled) {
console.log('[AUDIT] Service disabled, returning null');
return null;
}
if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) {
console.log('[AUDIT] No audit trail data provided');
return null;
}
try {
console.log('[AUDIT] Processing', rawAuditTrail.length, 'audit entries');
const totalTime = rawAuditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0);
const validConfidenceEntries = rawAuditTrail.filter(entry => typeof entry.confidence === 'number');
const avgConfidence = validConfidenceEntries.length > 0
? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length)
: 0;
const highConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) >= 80).length;
const lowConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) < 60).length;
const groupedEntries = rawAuditTrail.reduce((groups, entry) => {
const phase = entry.phase || 'unknown';
if (!groups[phase]) groups[phase] = [];
groups[phase].push(entry);
return groups;
}, {} as Record<string, AuditEntry[]>);
const phases = Object.entries(groupedEntries).map(([phase, entries]) => {
const phaseConfig = this.phaseConfig[phase] || { icon: '📋', displayName: phase };
const validEntries = entries.filter(entry => entry && typeof entry === 'object');
const phaseAvgConfidence = validEntries.length > 0
? Math.round(validEntries.reduce((sum, entry) => sum + (entry.confidence || 0), 0) / validEntries.length)
: 0;
const phaseTotalTime = validEntries.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0);
return {
name: phase,
icon: phaseConfig.icon,
displayName: phaseConfig.displayName,
avgConfidence: phaseAvgConfidence,
totalTime: phaseTotalTime,
entries: validEntries
.map(e => this.compressEntry(e))
.filter((e): e is CompressedAuditEntry => e !== null)
};
}).filter(phase => phase.entries.length > 0);
const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps);
const result: ProcessedAuditTrail = {
totalTime,
avgConfidence,
stepCount: rawAuditTrail.length,
highConfidenceSteps,
lowConfidenceSteps,
phases,
summary
};
console.log('[AUDIT] Successfully processed audit trail:', result);
return result;
} catch (error) {
console.error('[AUDIT] Error processing audit trail:', error);
return null;
}
}
private compressEntry(entry: AuditEntry): CompressedAuditEntry | null {
if (!entry || typeof entry !== 'object') {
console.warn('[AUDIT] Invalid audit entry:', entry);
return null;
}
try {
return {
timestamp: entry.timestamp || Date.now(),
phase: entry.phase || 'unknown',
action: entry.action || 'unknown',
inputSummary: this.summarizeData(entry.input),
outputSummary: this.summarizeData(entry.output),
confidence: entry.confidence || 0,
processingTimeMs: entry.processingTimeMs || 0,
metadata: entry.metadata || {}
};
} catch (error) {
console.error('[AUDIT] Error compressing entry:', error);
return null;
}
}
private compressData(data: any): any {
if (this.config.detailLevel === 'verbose') {
return data;
} else if (this.config.detailLevel === 'standard') {
return this.summarizeForStorage(data);
} else {
return this.minimalSummary(data);
}
}
private summarizeData(data: any): string {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') {
return data.length > 100 ? data.slice(0, 100) + '...' : data;
}
if (typeof data === 'number' || typeof data === 'boolean') {
return data.toString();
}
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
if (data.length <= 3) return JSON.stringify(data);
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
}
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
}
return String(data);
}
private summarizeForStorage(data: any): any {
if (typeof data === 'string' && data.length > 500) {
return data.slice(0, 500) + '...[truncated]';
}
if (Array.isArray(data) && data.length > 10) {
return [...data.slice(0, 10), `...[${data.length - 10} more items]`];
}
return data;
}
private minimalSummary(data: any): any {
if (typeof data === 'string' && data.length > 100) {
return data.slice(0, 100) + '...[truncated]';
}
if (Array.isArray(data) && data.length > 3) {
return [...data.slice(0, 3), `...[${data.length - 3} more items]`];
}
return data;
}
private generateSummary(entries: AuditEntry[], avgConfidence: number, lowConfidenceSteps: number): {
analysisQuality: 'excellent' | 'good' | 'fair' | 'poor';
keyInsights: string[];
potentialIssues: string[];
} {
let analysisQuality: 'excellent' | 'good' | 'fair' | 'poor';
if (avgConfidence >= 85 && lowConfidenceSteps === 0) {
analysisQuality = 'excellent';
} else if (avgConfidence >= 70 && lowConfidenceSteps <= 1) {
analysisQuality = 'good';
} else if (avgConfidence >= 60 && lowConfidenceSteps <= 3) {
analysisQuality = 'fair';
} else {
analysisQuality = 'poor';
}
const keyInsights: string[] = [];
const embeddingsUsed = entries.some(e => e.action === 'embeddings-search');
if (embeddingsUsed) {
keyInsights.push('Semantische Suche wurde erfolgreich eingesetzt');
}
const toolSelectionEntries = entries.filter(e => e.action === 'ai-tool-selection');
if (toolSelectionEntries.length > 0) {
const avgSelectionConfidence = toolSelectionEntries.reduce((sum, e) => sum + e.confidence, 0) / toolSelectionEntries.length;
if (avgSelectionConfidence >= 80) {
keyInsights.push('Hohe Konfidenz bei der Tool-Auswahl');
}
}
const potentialIssues: string[] = [];
if (lowConfidenceSteps > 2) {
potentialIssues.push(`${lowConfidenceSteps} Analyseschritte mit niedriger Konfidenz`);
}
const longSteps = entries.filter(e => e.processingTimeMs > 5000);
if (longSteps.length > 0) {
potentialIssues.push(`${longSteps.length} Schritte benötigten mehr als 5 Sekunden`);
}
return {
analysisQuality,
keyInsights,
potentialIssues
};
}
getActionDisplayName(action: string): string {
return this.actionTranslations[action] || action;
}
formatDuration(ms: number): string {
if (ms < 1000) return '< 1s';
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.ceil((ms % 60000) / 1000);
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
getConfidenceColor(confidence: number): string {
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
}
isEnabled(): boolean {
return this.config.enabled;
}
getConfig(): AuditConfig {
return { ...this.config };
}
}
export const auditService = new AuditService();
export type { ProcessedAuditTrail, CompressedAuditEntry };
export const debugAuditService = {
getDebugInfo() {
return auditService.getDebugInfo();
},
isEnabled() {
return auditService.isEnabled();
},
getConfig() {
return auditService.getConfig();
}
};

View File

@ -260,8 +260,6 @@ function getAuthRequirement(context: AuthContextType): boolean {
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
case 'ai':
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
case 'general':
return process.env.AUTHENTICATION_NECESSARY !== 'false';
default:
return true;
}

366
src/utils/clientUtils.ts Normal file
View File

@ -0,0 +1,366 @@
// src/utils/clientUtils.ts
// Client-side utilities that mirror server-side toolHelpers.ts
export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// Consolidated Autocomplete Functionality
interface AutocompleteOptions {
minLength?: number;
maxResults?: number;
placeholder?: string;
allowMultiple?: boolean;
separator?: string;
filterFunction?: (query: string) => any[];
renderFunction?: (item: any) => string;
hiddenInput?: HTMLInputElement;
}
export class AutocompleteManager {
public input: HTMLInputElement;
public dataSource: any[];
public options: AutocompleteOptions;
public isOpen: boolean = false;
public selectedIndex: number = -1;
public filteredData: any[] = [];
public selectedItems: Set<string> = new Set();
public dropdown!: HTMLElement;
public selectedContainer!: HTMLElement;
constructor(inputElement: HTMLInputElement, dataSource: any[], options: AutocompleteOptions = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.init();
}
init(): void {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown(): void {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
this.dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
`;
// Insert dropdown after input
const parentElement = this.input.parentNode as HTMLElement;
parentElement.style.position = 'relative';
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents(): void {
this.input.addEventListener('input', (e) => {
this.handleInput((e.target as HTMLInputElement).value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= (this.options.minLength || 1)) {
this.showDropdown();
}
});
this.input.addEventListener('blur', () => {
// Delay to allow click events on dropdown items
setTimeout(() => {
const activeElement = document.activeElement;
if (!activeElement || !this.dropdown.contains(activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
const target = e.target as Node;
if (!this.input.contains(target) && !this.dropdown.contains(target)) {
this.hideDropdown();
}
});
}
initMultipleMode(): void {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.selectedContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
`;
const parentElement = this.input.parentNode as HTMLElement;
parentElement.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value: string): void {
if (value.length >= (this.options.minLength || 1)) {
this.filteredData = this.options.filterFunction!(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e: KeyboardEvent): void {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query: string): any[] {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults || 10);
}
defaultRender(item: any): string {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown(): void {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction!(item);
return `<div class="autocomplete-option" data-index="${index}" style="
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
">${content}</div>`;
})
.join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight(): void {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
(option as HTMLElement).style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item: any): void {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text: string): void {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay(): void {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag" style="
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style="
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</span>
`)
.join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem((btn as HTMLElement).getAttribute('data-item')!);
});
});
}
updateInputValue(): void {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator || ', ');
}
}
showDropdown(): void {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown(): void {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource: any[]): void {
this.dataSource = newDataSource;
}
getSelectedItems(): string[] {
return Array.from(this.selectedItems);
}
setSelectedItems(items: string[]): void {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy(): void {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}

View File

@ -128,7 +128,9 @@ async function loadRawData(): Promise<ToolsData> {
const rawData = load(yamlContent);
try {
console.log('Attempting to validate YAML structure...');
cachedData = ToolsDataSchema.parse(rawData);
console.log('Validation successful!');
if (!cachedData.skill_levels || Object.keys(cachedData.skill_levels).length === 0) {
cachedData.skill_levels = {
@ -144,8 +146,15 @@ async function loadRawData(): Promise<ToolsData> {
console.log(`[DATA SERVICE] Loaded enhanced data version: ${dataVersion}`);
} catch (error) {
console.error('YAML validation failed:', error);
throw new Error('Invalid tools.yaml structure');
if (error instanceof z.ZodError) {
console.error('ToolsDataSchema validation errors:');
error.errors.forEach((err, index) => {
console.error(`${index + 1}. Path: ${err.path.join('.')}, Error: ${err.message}`);
});
} else {
console.error('Non-Zod validation error:', error);
}
throw new Error(`Invalid tools.yaml structure: ${error}`);
}
}
return cachedData;

View File

@ -2,6 +2,8 @@
import { promises as fs } from 'fs';
import path from 'path';
import { getCompressedToolsDataForAI } from './dataService.js';
import 'dotenv/config';
import crypto from 'crypto';
interface EmbeddingData {
id: string;
@ -35,12 +37,52 @@ class EmbeddingsService {
private readonly embeddingsPath = path.join(process.cwd(), 'data', 'embeddings.json');
private readonly batchSize: number;
private readonly batchDelay: number;
private readonly enabled: boolean;
private enabled: boolean = false;
constructor() {
this.enabled = process.env.AI_EMBEDDINGS_ENABLED === 'true';
this.batchSize = parseInt(process.env.AI_EMBEDDINGS_BATCH_SIZE || '20', 10);
this.batchDelay = parseInt(process.env.AI_EMBEDDINGS_BATCH_DELAY_MS || '1000', 10);
this.enabled = true;
}
private async checkEnabledStatus(): Promise<void> {
try {
console.log('[EMBEDDINGS] Debug env check:', {
AI_EMBEDDINGS_ENABLED: process.env.AI_EMBEDDINGS_ENABLED,
envKeys: Object.keys(process.env).filter(k => k.includes('EMBEDDINGS')).length,
allEnvKeys: Object.keys(process.env).length
});
const envEnabled = process.env.AI_EMBEDDINGS_ENABLED;
if (envEnabled === 'true') {
const endpoint = process.env.AI_EMBEDDINGS_ENDPOINT;
const model = process.env.AI_EMBEDDINGS_MODEL;
if (!endpoint || !model) {
console.warn('[EMBEDDINGS] Embeddings enabled but API configuration missing - disabling');
this.enabled = false;
return;
}
console.log('[EMBEDDINGS] All requirements met - enabling embeddings');
this.enabled = true;
return;
}
try {
await fs.stat(this.embeddingsPath);
console.log('[EMBEDDINGS] Existing embeddings file found - enabling');
this.enabled = true;
} catch {
console.log('[EMBEDDINGS] Embeddings not explicitly enabled - disabling');
this.enabled = false;
}
} catch (error) {
console.error('[EMBEDDINGS] Error checking enabled status:', error);
this.enabled = false;
}
}
async initialize(): Promise<void> {
@ -57,60 +99,55 @@ class EmbeddingsService {
}
private async performInitialization(): Promise<void> {
await this.checkEnabledStatus();
if (!this.enabled) {
console.log('[EMBEDDINGS] Embeddings disabled, skipping initialization');
return;
}
const initStart = Date.now();
try {
console.log('[EMBEDDINGS] Initializing embeddings system...');
console.log('[EMBEDDINGS] Initializing embeddings system');
await fs.mkdir(path.dirname(this.embeddingsPath), { recursive: true });
const toolsData = await getCompressedToolsDataForAI();
const currentDataHash = this.hashData(toolsData);
const toolsData = await getCompressedToolsDataForAI();
const currentDataHash = await this.hashToolsFile();
const existingEmbeddings = await this.loadEmbeddings();
const existing = await this.loadEmbeddings();
console.log('[EMBEDDINGS] Current hash:', currentDataHash);
console.log('[EMBEDDINGS] Existing file version:', existing?.version);
console.log('[EMBEDDINGS] Existing embeddings length:', existing?.embeddings?.length);
if (existingEmbeddings && existingEmbeddings.version === currentDataHash) {
const cacheIsUsable =
existing &&
existing.version === currentDataHash &&
Array.isArray(existing.embeddings) &&
existing.embeddings.length > 0;
if (cacheIsUsable) {
console.log('[EMBEDDINGS] Using cached embeddings');
this.embeddings = existingEmbeddings.embeddings;
this.embeddings = existing.embeddings;
} else {
console.log('[EMBEDDINGS] Generating new embeddings...');
console.log('[EMBEDDINGS] Generating new embeddings');
await this.generateEmbeddings(toolsData, currentDataHash);
}
this.isInitialized = true;
console.log(`[EMBEDDINGS] Initialized with ${this.embeddings.length} embeddings`);
} catch (error) {
console.error('[EMBEDDINGS] Failed to initialize:', error);
console.log(`[EMBEDDINGS] Initialized with ${this.embeddings.length} embeddings in ${Date.now() - initStart} ms`);
} catch (err) {
console.error('[EMBEDDINGS] Failed to initialize:', err);
this.isInitialized = false;
throw error;
throw err;
} finally {
this.initializationPromise = null;
}
}
async waitForInitialization(): Promise<void> {
if (!this.enabled) {
return Promise.resolve();
}
if (this.isInitialized) {
return Promise.resolve();
}
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
return this.initialize();
}
private hashData(data: any): string {
return Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 32);
private async hashToolsFile(): Promise<string> {
const file = path.join(process.cwd(), 'src', 'data', 'tools.yaml');
const raw = await fs.readFile(file, 'utf8');
return crypto.createHash('sha256').update(raw).digest('hex');
}
private async loadEmbeddings(): Promise<EmbeddingsDatabase | null> {
@ -152,7 +189,10 @@ class EmbeddingsService {
const model = process.env.AI_EMBEDDINGS_MODEL;
if (!endpoint || !model) {
throw new Error('Missing embeddings API configuration');
const missing: string[] = [];
if (!endpoint) missing.push('AI_EMBEDDINGS_ENDPOINT');
if (!model) missing.push('AI_EMBEDDINGS_MODEL');
throw new Error(`Missing embeddings API configuration: ${missing.join(', ')}`);
}
const headers: Record<string, string> = {
@ -240,10 +280,35 @@ class EmbeddingsService {
}
public async embedText(text: string): Promise<number[]> {
if (!this.enabled || !this.isInitialized) {
throw new Error('Embeddings service not available');
}
const [embedding] = await this.generateEmbeddingsBatch([text.toLowerCase()]);
return embedding;
}
async waitForInitialization(): Promise<void> {
await this.checkEnabledStatus();
if (!this.enabled || this.isInitialized) {
return Promise.resolve();
}
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
return this.initialize();
}
async forceRecheckEnvironment(): Promise<void> {
this.enabled = false;
this.isInitialized = false;
await this.checkEnabledStatus();
console.log('[EMBEDDINGS] Environment status re-checked, enabled:', this.enabled);
}
private cosineSimilarity(a: number[], b: number[]): number {
let dotProduct = 0;
let normA = 0;
@ -259,54 +324,121 @@ class EmbeddingsService {
}
async findSimilar(query: string, maxResults: number = 30, threshold: number = 0.3): Promise<SimilarityResult[]> {
if (!this.enabled || !this.isInitialized || this.embeddings.length === 0) {
console.log('[EMBEDDINGS] Service not available for similarity search');
if (!this.enabled) {
console.log('[EMBEDDINGS] Service disabled for similarity search');
return [];
}
try {
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
const queryEmbedding = queryEmbeddings[0];
if (this.isInitialized && this.embeddings.length > 0) {
console.log(`[EMBEDDINGS] Using embeddings data for similarity search: ${query}`);
console.log(`[EMBEDDINGS] Computing similarities for ${this.embeddings.length} items`);
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
const queryEmbedding = queryEmbeddings[0];
const similarities: SimilarityResult[] = this.embeddings.map(item => ({
...item,
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
}));
console.log(`[EMBEDDINGS] Computing similarities for ${this.embeddings.length} items`);
const results = similarities
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults);
const similarities: SimilarityResult[] = this.embeddings.map(item => ({
...item,
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
}));
const orderingValid = results.every((item, index) => {
if (index === 0) return true;
return item.similarity <= results[index - 1].similarity;
});
const topScore = Math.max(...similarities.map(s => s.similarity));
const dynamicCutOff = Math.max(threshold, topScore * 0.85);
if (!orderingValid) {
console.error('[EMBEDDINGS] CRITICAL: Similarity ordering is broken!');
results.forEach((item, idx) => {
console.error(` ${idx}: ${item.name} = ${item.similarity.toFixed(4)}`);
});
}
const results = similarities
.filter(item => item.similarity >= dynamicCutOff)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults);
console.log(`[EMBEDDINGS] Found ${results.length} similar items (threshold: ${threshold})`);
if (results.length > 0) {
console.log('[EMBEDDINGS] Top 10 similarity matches:');
results.slice(0, 10).forEach((item, idx) => {
console.log(` ${idx + 1}. ${item.name} (${item.type}) = ${item.similarity.toFixed(4)}`);
const orderingValid = results.every((item, index) => {
if (index === 0) return true;
return item.similarity <= results[index - 1].similarity;
});
const topSimilarity = results[0].similarity;
const hasHigherSimilarity = results.some(item => item.similarity > topSimilarity);
if (hasHigherSimilarity) {
console.error('[EMBEDDINGS] CRITICAL: Top result is not actually the highest similarity!');
if (!orderingValid) {
console.error('[EMBEDDINGS] CRITICAL: Similarity ordering is broken!');
results.forEach((item, idx) => {
console.error(` ${idx}: ${item.name} = ${item.similarity.toFixed(4)}`);
});
}
}
return results;
console.log(`[EMBEDDINGS] Found ${results.length} similar items (threshold: ${threshold})`);
if (results.length > 0) {
console.log('[EMBEDDINGS] Top 10 similarity matches:');
results.slice(0, 10).forEach((item, idx) => {
console.log(` ${idx + 1}. ${item.name} (${item.type}) = ${item.similarity.toFixed(4)}`);
});
const topSimilarity = results[0].similarity;
const hasHigherSimilarity = results.some(item => item.similarity > topSimilarity);
if (hasHigherSimilarity) {
console.error('[EMBEDDINGS] CRITICAL: Top result is not actually the highest similarity!');
}
}
return results;
} else {
console.log(`[EMBEDDINGS] No embeddings data, using fallback text matching: ${query}`);
const { getToolsData } = await import('./dataService.js');
const toolsData = await getToolsData();
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
const similarities: SimilarityResult[] = toolsData.tools
.map((tool: any) => {
let similarity = 0;
if (tool.name.toLowerCase().includes(queryLower)) {
similarity += 0.8;
}
if (tool.description && tool.description.toLowerCase().includes(queryLower)) {
similarity += 0.6;
}
if (tool.tags && Array.isArray(tool.tags)) {
const matchingTags = tool.tags.filter((tag: string) =>
tag.toLowerCase().includes(queryLower) || queryLower.includes(tag.toLowerCase())
);
if (tool.tags.length > 0) {
similarity += (matchingTags.length / tool.tags.length) * 0.4;
}
}
const toolText = `${tool.name} ${tool.description || ''} ${(tool.tags || []).join(' ')}`.toLowerCase();
const matchingWords = queryWords.filter(word => toolText.includes(word));
if (queryWords.length > 0) {
similarity += (matchingWords.length / queryWords.length) * 0.3;
}
return {
id: `tool_${tool.name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`,
type: 'tool' as const,
name: tool.name,
content: toolText,
embedding: [],
metadata: {
domains: tool.domains || [],
phases: tool.phases || [],
tags: tool.tags || [],
skillLevel: tool.skillLevel,
type: tool.type
},
similarity: Math.min(similarity, 1.0)
};
})
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults);
console.log(`[EMBEDDINGS] Fallback found ${similarities.length} similar items`);
return similarities;
}
} catch (error) {
console.error('[EMBEDDINGS] Failed to find similar items:', error);
@ -315,7 +447,11 @@ class EmbeddingsService {
}
isEnabled(): boolean {
return this.enabled && this.isInitialized;
if (!this.enabled && !this.isInitialized) {
this.checkEnabledStatus().catch(console.error);
}
return this.enabled;
}
getStats(): { enabled: boolean; initialized: boolean; count: number } {
@ -327,14 +463,15 @@ class EmbeddingsService {
}
}
const embeddingsService = new EmbeddingsService();
export { embeddingsService, type EmbeddingData, type SimilarityResult };
if (typeof window === 'undefined' && process.env.NODE_ENV !== 'test') {
embeddingsService.initialize().catch(error => {
console.error('[EMBEDDINGS] Auto-initialization failed:', error);
});
}
export const debugEmbeddings = {
async recheckEnvironment() {
return embeddingsService.forceRecheckEnvironment();
},
getStatus() {
return embeddingsService.getStats();
}
};

View File

@ -5,22 +5,23 @@ export interface ContributionData {
type: 'add' | 'edit';
tool: {
name: string;
icon?: string;
icon?: string | null;
type: 'software' | 'method' | 'concept';
description: string;
domains: string[];
phases: string[];
platforms: string[];
skillLevel: string;
accessType?: string;
accessType?: string | null;
url: string;
projectUrl?: string;
license?: string;
knowledgebase?: boolean;
'domain-agnostic-software'?: string[];
related_concepts?: string[];
projectUrl?: string | null;
license?: string | null;
knowledgebase?: boolean | null;
'domain-agnostic-software'?: string[] | null;
related_concepts?: string[] | null;
related_software?: string[] | null;
tags: string[];
statusUrl?: string;
statusUrl?: string | null;
};
metadata: {
submitter: string;
@ -134,6 +135,7 @@ export class GitContributionManager {
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
if (tool.related_software?.length) cleanTool.related_software = tool.related_software;
if (tool.tags?.length) cleanTool.tags = tool.tags;
if (tool['domain-agnostic-software']?.length) {
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
@ -272,6 +274,8 @@ ${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(',
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''}
${data.metadata.reason ? `### Reason
${data.metadata.reason}
@ -299,9 +303,6 @@ ${data.metadata.contact}
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
const sections: string[] = [];
/* ------------------------------------------------------------------ */
/* Header */
/* ------------------------------------------------------------------ */
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
sections.push('');
sections.push(`**Submitted by:** ${data.submitter}`);
@ -310,18 +311,12 @@ ${data.metadata.contact}
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
sections.push('');
/* ------------------------------------------------------------------ */
/* Description */
/* ------------------------------------------------------------------ */
if (data.description) {
sections.push('### Description');
sections.push(data.description);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Content */
/* ------------------------------------------------------------------ */
if (data.content) {
sections.push('### Article Content');
sections.push('```markdown');
@ -330,18 +325,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* External resources */
/* ------------------------------------------------------------------ */
if (data.externalLink) {
sections.push('### External Resource');
sections.push(`- [External Documentation](${data.externalLink})`);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Uploaded files */
/* ------------------------------------------------------------------ */
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
sections.push('### Uploaded Files');
data.uploadedFiles.forEach((file) => {
@ -359,9 +348,6 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Categories & Tags */
/* ------------------------------------------------------------------ */
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
@ -372,18 +358,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Reason */
/* ------------------------------------------------------------------ */
if (data.reason) {
sections.push('### Reason for Contribution');
sections.push(data.reason);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Footer */
/* ------------------------------------------------------------------ */
sections.push('### For Maintainers');
sections.push('1. Review the content for quality and accuracy');
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');

View File

@ -338,7 +338,7 @@ export class NextcloudUploader {
info: {
path: remotePath,
exists: true,
response: text.substring(0, 200) + '...' // Truncated for safety
response: text.substring(0, 200) + '...'
}
};
}

2082
tools-yaml-editor.html Normal file

File diff suppressed because it is too large Load Diff