Compare commits
No commits in common. "eff21865eacd61cf4f278e40226abce4c81fe5a3" and "b515a45e1ec1ad29836ab6a65c38b86067a1c668" have entirely different histories.
eff21865ea
...
b515a45e1e
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1754571688630
|
||||
"lastUpdateCheck": 1753528124767
|
||||
}
|
||||
}
|
365
.env.example
365
.env.example
@ -1,200 +1,257 @@
|
||||
# ============================================================================
|
||||
# ForensicPathways Environment Configuration
|
||||
# ForensicPathways Environment Configuration - COMPLETE
|
||||
# ============================================================================
|
||||
# Copy this file to .env and configure the REQUIRED values below.
|
||||
# Optional features can be enabled by uncommenting and configuring them.
|
||||
# Copy this file to .env and adjust the values below.
|
||||
# This file covers ALL environment variables used in the codebase.
|
||||
|
||||
# ============================================================================
|
||||
# 🔥 CRITICAL - REQUIRED FOR BASIC OPERATION
|
||||
# 1. CORE APPLICATION SETTINGS (REQUIRED)
|
||||
# ============================================================================
|
||||
|
||||
# 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
|
||||
|
||||
# === AUTHENTICATION & SECURITY ===
|
||||
# Set to true to require authentication (RECOMMENDED for production)
|
||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
||||
AUTHENTICATION_NECESSARY_AI=false
|
||||
# Secret key for session encryption (CHANGE IN PRODUCTION!)
|
||||
AUTH_SECRET=your-secret-key-change-in-production-please
|
||||
|
||||
# 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
|
||||
# ============================================================================
|
||||
# 2. AI SERVICES CONFIGURATION (REQUIRED FOR AI FEATURES)
|
||||
# ============================================================================
|
||||
|
||||
# === FILE HANDLING ===
|
||||
# Nextcloud server for file uploads (knowledgebase contributions)
|
||||
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
|
||||
NEXTCLOUD_USERNAME=your-username
|
||||
NEXTCLOUD_PASSWORD=your-app-password
|
||||
NEXTCLOUD_UPLOAD_PATH=/kb-media
|
||||
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
|
||||
# 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
|
||||
|
||||
# === COLLABORATION & CONTRIBUTIONS ===
|
||||
# Git provider: gitea, github, or gitlab
|
||||
GIT_PROVIDER=gitea
|
||||
GIT_REPO_URL=https://git.example.com/owner/forensic-pathways.git
|
||||
GIT_API_ENDPOINT=https://git.example.com/api/v1
|
||||
GIT_API_TOKEN=your-git-api-token
|
||||
|
||||
# === AUDIT TRAIL (Important for forensic work) ===
|
||||
FORENSIC_AUDIT_ENABLED=true
|
||||
FORENSIC_AUDIT_DETAIL_LEVEL=standard
|
||||
FORENSIC_AUDIT_RETENTION_HOURS=24
|
||||
FORENSIC_AUDIT_MAX_ENTRIES=50
|
||||
|
||||
# === AI SEMANTIC SEARCH ===
|
||||
# Enable semantic search (highly recommended for better results)
|
||||
# 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=your-embeddings-api-key-here
|
||||
AI_EMBEDDINGS_API_KEY=
|
||||
AI_EMBEDDINGS_MODEL=mistral-embed
|
||||
|
||||
# User rate limiting (queries per minute)
|
||||
AI_RATE_LIMIT_MAX_REQUESTS=4
|
||||
|
||||
# ============================================================================
|
||||
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
|
||||
# 3. AI PIPELINE CONFIGURATION (CONTEXT & PERFORMANCE TUNING)
|
||||
# ============================================================================
|
||||
|
||||
# === AI Pipeline Configuration ===
|
||||
# These values are pre-tuned for optimal performance - adjust only if needed
|
||||
|
||||
# Vector similarity search settings
|
||||
# === 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
|
||||
|
||||
# === 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)
|
||||
# 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 limits
|
||||
# === 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
|
||||
|
||||
# Efficiency thresholds
|
||||
AI_EMBEDDINGS_MIN_TOOLS=8
|
||||
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
|
||||
# ============================================================================
|
||||
# 5. AI CONTEXT & TOKEN MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
# 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 ===
|
||||
# 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
|
||||
|
||||
# === Confidence Scoring ===
|
||||
CONFIDENCE_SEMANTIC_WEIGHT=0.5
|
||||
CONFIDENCE_SUITABILITY_WEIGHT=0.5
|
||||
CONFIDENCE_MINIMUM_THRESHOLD=50
|
||||
CONFIDENCE_MEDIUM_THRESHOLD=70
|
||||
CONFIDENCE_HIGH_THRESHOLD=80
|
||||
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 📋 QUICK SETUP CHECKLIST
|
||||
# ============================================================================
|
||||
#
|
||||
# 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
|
||||
|
||||
# ============================================================================
|
||||
# 🏃♂️ PERFORMANCE PRESETS - UNCOMMENT ONE IF NEEDED
|
||||
# 6. AUTHENTICATION & AUTHORIZATION (OPTIONAL)
|
||||
# ============================================================================
|
||||
|
||||
# 🚀 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
|
||||
# Enable authentication for different features
|
||||
AUTHENTICATION_NECESSARY=false
|
||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
||||
AUTHENTICATION_NECESSARY_AI=false
|
||||
|
||||
# 🎯 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
|
||||
# OIDC Provider Settings (only needed if authentication enabled)
|
||||
OIDC_ENDPOINT=https://your-oidc-provider.com
|
||||
OIDC_CLIENT_ID=your-client-id
|
||||
OIDC_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# ============================================================================
|
||||
# 🌐 AI SERVICE EXAMPLES
|
||||
# 7. FILE UPLOADS - NEXTCLOUD INTEGRATION (OPTIONAL)
|
||||
# ============================================================================
|
||||
|
||||
# === 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
|
||||
# Nextcloud server for file uploads (knowledgebase contributions)
|
||||
# Leave empty to disable file upload functionality
|
||||
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
|
||||
|
||||
# === 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
|
||||
# Nextcloud credentials (app password recommended)
|
||||
NEXTCLOUD_USERNAME=your-username
|
||||
NEXTCLOUD_PASSWORD=your-app-password
|
||||
|
||||
# === 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
|
||||
# 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)
|
||||
# ============================================================================
|
||||
|
||||
# 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
|
||||
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
|
||||
# ============================================================================
|
||||
|
||||
# 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
|
||||
|
||||
# ============================================================================
|
||||
# PERFORMANCE TUNING PRESETS
|
||||
# ============================================================================
|
||||
|
||||
# 🚀 FOR FASTER RESPONSES (prevent token overflow):
|
||||
# AI_NO_EMBEDDINGS_TOOL_LIMIT=25
|
||||
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=10
|
||||
|
||||
# 🎯 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
|
||||
# ============================================================================
|
||||
|
||||
# 📝 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
|
||||
|
||||
# ============================================================================
|
||||
# SETUP CHECKLIST
|
||||
# ============================================================================
|
||||
# ✅ 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
|
||||
# ============================================================================
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,7 +11,6 @@ _site/
|
||||
dist/
|
||||
|
||||
.astro/
|
||||
.astro/*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
@ -86,4 +85,4 @@ temp/
|
||||
.astro/data-store.json
|
||||
.astro/content.d.ts
|
||||
prompt.md
|
||||
.astro/settings.json
|
||||
data/embeddings.json
|
||||
|
665
README.md
665
README.md
@ -1,150 +1,232 @@
|
||||
# ForensicPathways
|
||||
|
||||
Ein umfassendes Verzeichnis digitaler Forensik- und Incident-Response-Tools mit KI-gestützten Empfehlungen basierend auf der NIST SP 800-86 Methodik.
|
||||
Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR) Tools, Methoden und Konzepte mit KI-gestützten Workflow-Empfehlungen.
|
||||
|
||||
## Lizenz
|
||||
## ✨ Funktionen
|
||||
|
||||
Dieses Projekt ist unter der BSD-3-Clause-Lizenz lizenziert.
|
||||
### 🎯 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
|
||||
|
||||
## Funktionen
|
||||
### 🔍 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
|
||||
|
||||
### 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
|
||||
### 📚 Inhaltstypen
|
||||
- **Software/Tools:** Open Source und proprietäre forensische Software
|
||||
- **Methoden:** Bewährte forensische Verfahren und Prozesse
|
||||
- **Konzepte:** Grundlegendes Fachwissen und theoretische Grundlagen
|
||||
|
||||
### 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
|
||||
### 📖 Knowledgebase
|
||||
- **Erweiterte Dokumentation:** Detaillierte Artikel zu Tools und Methoden
|
||||
- **Praktische Anleitungen:** Installation, Konfiguration und Best Practices
|
||||
- **Markdown-basiert:** Einfache Erstellung und Wartung von Inhalten
|
||||
|
||||
### 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
|
||||
### 🤝 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
|
||||
|
||||
### 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)
|
||||
### 🔐 Authentifizierung
|
||||
- **OIDC-Integration:** Single Sign-On mit OpenID Connect
|
||||
- **Berechtigungssteuerung:** Schutz für AI-Features und Contribution-System
|
||||
- **Session-Management:** Sichere JWT-basierte Sessions
|
||||
|
||||
## Datenmodell
|
||||
## 🛠 Technische Grundlage
|
||||
|
||||
Das System verwendet eine YAML-basierte Konfiguration in `src/data/tools.yaml`:
|
||||
- **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)
|
||||
|
||||
```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
|
||||
## 📋 Voraussetzungen
|
||||
|
||||
domains:
|
||||
- id: incident-response
|
||||
name: Incident Response & Breach-Untersuchung
|
||||
- **Node.js:** Version 18.x oder höher
|
||||
- **npm:** Version 8.x oder höher
|
||||
- **Nginx:** Für Reverse Proxy (Produktion)
|
||||
|
||||
phases:
|
||||
- id: data-collection
|
||||
name: Datensammlung
|
||||
description: Imaging, Akquisition, Remote-Collection-Tools
|
||||
## 🔧 Externe Abhängigkeiten (Optional)
|
||||
|
||||
scenarios:
|
||||
- id: scenario:memory_dump
|
||||
icon: 🧠
|
||||
friendly_name: RAM-Analyse
|
||||
```
|
||||
### OIDC Provider
|
||||
- **Zweck:** Benutzerauthentifizierung
|
||||
- **Beispiel:** Nextcloud, Keycloak, Auth0
|
||||
- **Konfiguration:** `OIDC_ENDPOINT`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`
|
||||
|
||||
## AI Concept
|
||||
### Nextcloud
|
||||
- **Zweck:** File-Upload für Knowledgebase-Beiträge
|
||||
- **Features:** Medien-Management, öffentliche Links
|
||||
- **Konfiguration:** `NEXTCLOUD_ENDPOINT`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD`
|
||||
|
||||
### Micro-Task Architecture
|
||||
The AI system uses a sophisticated pipeline that breaks complex analysis into focused micro-tasks:
|
||||
### AI Service (Mistral/OpenAI-kompatibel)
|
||||
- **Zweck:** KI-gestützte Tool-Empfehlungen
|
||||
- **Konfiguration:** `AI_ANALYZER_ENDPOINT`, `AI_ANALYZER_API_KEY`, `AI_ANALYZER_MODEL`
|
||||
|
||||
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
|
||||
### Uptime Kuma
|
||||
- **Zweck:** Status-Monitoring für gehostete Services
|
||||
- **Integration:** Status-Badges in der Service-Übersicht
|
||||
|
||||
### 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
|
||||
### Git Provider (Gitea/GitHub/GitLab)
|
||||
- **Zweck:** Issue-Erstellung für Contributions
|
||||
- **Konfiguration:** `GIT_PROVIDER`, `GIT_API_ENDPOINT`, `GIT_API_TOKEN`
|
||||
|
||||
## NIST SP 800-86 Phases
|
||||
## 🚀 Installation
|
||||
|
||||
The system organizes tools according to the four-phase NIST methodology:
|
||||
### Lokale Entwicklung
|
||||
|
||||
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
|
||||
npm run build
|
||||
sudo ./deploy.sh # Copies dist/ to /var/www/forensic-pathways
|
||||
# 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
|
||||
```
|
||||
|
||||
2. **Configuration**:
|
||||
Die Seite ist dann unter `http://localhost:4321` verfügbar.
|
||||
|
||||
### Produktions-Deployment
|
||||
|
||||
#### 1. System vorbereiten
|
||||
|
||||
```bash
|
||||
cd /var/www/forensic-pathways
|
||||
sudo cp .env.example .env
|
||||
sudo nano .env # Configure AI services, authentication, etc.
|
||||
# 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
|
||||
```
|
||||
|
||||
3. **Systemd Service** (`/etc/systemd/system/forensic-pathways.service`):
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ForensicPathways
|
||||
After=network.target
|
||||
#### 2. Anwendung installieren
|
||||
|
||||
[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
|
||||
# Klonen des Repositorys
|
||||
sudo git clone https://git.cc24.dev/mstoeck3/forensic-pathways /opt/forensic-pathways
|
||||
cd /opt/forensic-pathways
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
# 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
|
||||
```
|
||||
|
||||
4. **Nginx Configuration**:
|
||||
#### 3. Umgebungsvariablen konfigurieren
|
||||
|
||||
Erstelle `/opt/forensic-pathways/.env`:
|
||||
|
||||
```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/
|
||||
```
|
||||
|
||||
```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`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name forensic-pathways.yourdomain.com;
|
||||
client_max_body_size 50M; # Important for uploads
|
||||
server_name ihre-domain.de;
|
||||
|
||||
# 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;
|
||||
@ -154,162 +236,251 @@ 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
|
||||
sudo systemctl enable forensic-pathways
|
||||
sudo systemctl start forensic-pathways
|
||||
# Site aktivieren
|
||||
sudo ln -s /etc/nginx/sites-available/forensic-pathways /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
#### 5. Systemd Service einrichten
|
||||
|
||||
Key configuration in `.env`:
|
||||
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
|
||||
```
|
||||
|
||||
```bash
|
||||
# Core Application
|
||||
PUBLIC_BASE_URL=https://forensic-pathways.yourdomain.com
|
||||
AUTH_SECRET=your-secure-random-secret
|
||||
# Service aktivieren und starten
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable forensic-pathways
|
||||
sudo systemctl start 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
|
||||
# Status prüfen
|
||||
sudo systemctl status forensic-pathways
|
||||
```
|
||||
|
||||
## Externe Abhängigkeiten (Optionale Features)
|
||||
## 🔧 Konfiguration
|
||||
|
||||
### 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
|
||||
### Minimalkonfiguration (ohne Auth)
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
npm install
|
||||
cp .env.example .env
|
||||
|
||||
# Entwicklung
|
||||
npm run dev
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Deploy
|
||||
sudo ./deploy.sh
|
||||
# Nur für Tests geeignet
|
||||
AUTHENTICATION_NECESSARY=false
|
||||
PUBLIC_BASE_URL=http://localhost:4321
|
||||
```
|
||||
|
||||
## Konfigurationsübersicht
|
||||
### Tools-Datenbank
|
||||
|
||||
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.
|
||||
Die Tools werden in `src/data/tools.yaml` verwaltet. Vollständiges Beispiel:
|
||||
|
||||
## Architektur
|
||||
```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
|
||||
|
||||
- **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
|
||||
# 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.
|
202482
data/embeddings.json
202482
data/embeddings.json
File diff suppressed because it is too large
Load Diff
863
deploy.sh
863
deploy.sh
@ -1,863 +0,0 @@
|
||||
#!/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
@ -30,7 +30,6 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
<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">
|
||||
@ -51,27 +50,6 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -311,10 +289,6 @@ 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'),
|
||||
@ -350,48 +324,6 @@ 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';
|
||||
@ -562,18 +494,7 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
: `${count} von ${total} Tools`;
|
||||
}
|
||||
|
||||
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() {
|
||||
function filterTools() {
|
||||
const searchTerm = elements.searchInput.value.trim().toLowerCase();
|
||||
const selectedDomain = elements.domainSelect.value;
|
||||
const selectedPhaseFromSelect = elements.phaseSelect.value;
|
||||
@ -587,29 +508,15 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
|
||||
const activePhase = selectedPhaseFromSelect || selectedPhase;
|
||||
|
||||
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 =>
|
||||
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;
|
||||
}
|
||||
|
||||
filteredTools = filteredTools.filter(tool => {
|
||||
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
|
||||
return false;
|
||||
}
|
||||
@ -653,29 +560,13 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
return true;
|
||||
});
|
||||
|
||||
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);
|
||||
const finalResults = searchTerm && window.prioritizeSearchResults
|
||||
? window.prioritizeSearchResults(filtered, searchTerm)
|
||||
: filtered;
|
||||
|
||||
updateResultsCounter(finalResults.length);
|
||||
updateSemanticStatus(lastSemanticResults);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('toolsFiltered', {
|
||||
detail: {
|
||||
tools: finalResults,
|
||||
semanticSearch: semanticSearchEnabled && !!lastSemanticResults,
|
||||
},
|
||||
})
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
|
||||
}
|
||||
|
||||
function resetPrimaryFilters() {
|
||||
@ -708,10 +599,6 @@ 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();
|
||||
@ -732,29 +619,16 @@ 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 => {
|
||||
if (select) {
|
||||
select.addEventListener('change', filterTools);
|
||||
}
|
||||
});
|
||||
|
||||
[elements.hostedOnly, elements.knowledgebaseOnly].forEach(checkbox => {
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', filterTools);
|
||||
}
|
||||
});
|
||||
|
||||
if (elements.tagCloudToggle) {
|
||||
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
|
||||
}
|
||||
|
||||
elements.tagCloudItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
@ -775,24 +649,14 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
b.classList.toggle('active', b.getAttribute('data-view') === view);
|
||||
});
|
||||
|
||||
if (window.switchToView) {
|
||||
window.switchToView(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 }));
|
||||
} else {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -812,7 +676,6 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
window.clearTagFilters = resetTags;
|
||||
window.clearAllFilters = resetAllFilters;
|
||||
|
||||
checkEmbeddingsAvailability();
|
||||
initializeCollapsible();
|
||||
initTagCloud();
|
||||
filterTagCloud();
|
||||
|
@ -193,16 +193,6 @@ 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') : '';
|
||||
@ -226,7 +216,9 @@ 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) {
|
||||
@ -239,7 +231,9 @@ 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -273,7 +267,9 @@ 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()}`;
|
||||
}
|
||||
|
||||
@ -305,7 +301,7 @@ domains.forEach((domain: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
function showShareDialog(shareButton) {
|
||||
window.showShareDialog = function(shareButton) {
|
||||
const toolName = shareButton.getAttribute('data-tool-name');
|
||||
const context = shareButton.getAttribute('data-context');
|
||||
|
||||
@ -442,11 +438,16 @@ domains.forEach((domain: any) => {
|
||||
copyToClipboard(url, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function showToolDetails(toolName, modalType = 'primary') {
|
||||
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
||||
|
||||
window.showToolDetails = function(toolName, modalType = 'primary') {
|
||||
const tool = toolsData.find(t => t.name === toolName);
|
||||
if (!tool) return;
|
||||
if (!tool) {
|
||||
console.error('Tool not found:', toolName);
|
||||
return;
|
||||
}
|
||||
|
||||
const isMethod = tool.type === 'method';
|
||||
const isConcept = tool.type === 'concept';
|
||||
@ -461,7 +462,10 @@ domains.forEach((domain: any) => {
|
||||
};
|
||||
|
||||
for (const [key, element] of Object.entries(elements)) {
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
console.error(`Element not found: tool-${key}-${modalType}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const iconHtml = tool.icon ? `<span class="mr-3 text-xl">${tool.icon}</span>` : '';
|
||||
@ -705,9 +709,9 @@ domains.forEach((domain: any) => {
|
||||
if (primaryActive && secondaryActive) {
|
||||
document.body.classList.add('modals-side-by-side');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function hideToolDetails(modalType = 'both') {
|
||||
window.hideToolDetails = function(modalType = 'both') {
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
const primaryModal = document.getElementById('tool-details-primary');
|
||||
const secondaryModal = document.getElementById('tool-details-secondary');
|
||||
@ -759,44 +763,28 @@ domains.forEach((domain: any) => {
|
||||
} else {
|
||||
document.body.classList.remove('modals-side-by-side');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.hideAllToolDetails = function() {
|
||||
window.hideToolDetails('both');
|
||||
};
|
||||
|
||||
window.addEventListener('viewChanged', (event) => {
|
||||
const view = event.detail;
|
||||
if (view === 'matrix') {
|
||||
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);
|
||||
setTimeout(updateMatrixHighlighting, 100);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('toolsFiltered', (event) => {
|
||||
const { tools: filtered, semanticSearch } = event.detail;
|
||||
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 currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
||||
|
||||
if (currentView === 'matrix') {
|
||||
@ -805,6 +793,13 @@ 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';
|
||||
|
||||
@ -813,7 +808,9 @@ 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);
|
||||
@ -830,7 +827,6 @@ 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);
|
||||
|
@ -1,307 +1,232 @@
|
||||
// src/config/prompts.ts - Enhanced with phase completion reasoning
|
||||
// src/config/prompts.ts - Centralized German prompts for AI pipeline
|
||||
|
||||
export const AI_PROMPTS = {
|
||||
|
||||
toolSelection: (mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number) => {
|
||||
const modeInstruction = mode === 'workflow'
|
||||
? '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.';
|
||||
? '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.';
|
||||
|
||||
return `Du bist ein DFIR-Experte. Wähle die BESTEN Items aus dem vorgefilterten Set.
|
||||
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.
|
||||
|
||||
AUSWAHLMETHODE: ${selectionMethod}
|
||||
${selectionMethod === 'embeddings_candidates' ?
|
||||
'✓ 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'}
|
||||
'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.'}
|
||||
|
||||
${modeInstruction}
|
||||
|
||||
ANFRAGE: "${userQuery}"
|
||||
BENUTZER-ANFRAGE: "${userQuery}"
|
||||
|
||||
VERFÜGBARE ITEM-TYPEN:
|
||||
- TOOLS (type: "software"/"method") → praktische Anwendungen und Vorgehensweisen
|
||||
- KONZEPTE (type: "concept") → theoretisches Wissen und Methodiken
|
||||
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.
|
||||
|
||||
AUSWAHLSTRATEGIE:
|
||||
1. **ERSTE PRIORITÄT: Relevanz zur Anfrage**
|
||||
- Direkt anwendbar auf das Problem
|
||||
- Löst die Kernherausforderung
|
||||
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
|
||||
|
||||
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!
|
||||
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
|
||||
|
||||
3. **QUALITÄT > QUANTITÄT**
|
||||
- Lieber weniger perfekte Items als viele mittelmäßige
|
||||
- Jedes Item muss begründbar sein
|
||||
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
|
||||
|
||||
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")
|
||||
Wählen Sie die relevantesten Elemente aus (max ${maxSelectedItems} insgesamt).
|
||||
|
||||
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT:
|
||||
Antworten Sie NUR mit diesem JSON-Format:
|
||||
{
|
||||
"selectedTools": ["ToolName1", "MethodName1", ...],
|
||||
"selectedConcepts": ["ConceptName1", "ConceptName2", ...],
|
||||
"reasoning": "Kurze Begründung mit Erwähnung der Tool/Konzept-Balance"
|
||||
"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"
|
||||
}`;
|
||||
},
|
||||
|
||||
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 ? 'Szenario' : 'Problem';
|
||||
const focus = isWorkflow ?
|
||||
'Angriffsvektoren, betroffene Systeme, Zeitkritikalität' :
|
||||
'Kernherausforderung, verfügbare Daten, methodische Anforderungen';
|
||||
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`;
|
||||
|
||||
return `DFIR-Experte: Analysiere das ${analysisType}.
|
||||
return `Sie sind ein erfahrener DFIR-Experte. Analysieren Sie das folgende ${analysisType}.
|
||||
|
||||
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||
${isWorkflow ? 'FORENSISCHES SZENARIO' : 'TECHNISCHES PROBLEM'}: "${userQuery}"
|
||||
|
||||
Fokus: ${focus}
|
||||
Führen Sie eine systematische ${isWorkflow ? 'Szenario-Analyse' : 'Problem-Analyse'} durch und berücksichtigen Sie dabei:
|
||||
|
||||
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
|
||||
${considerations}
|
||||
|
||||
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen, Aufzählungen oder Markdown-Formatierung. Maximum 150 Wörter.`;
|
||||
},
|
||||
|
||||
investigationApproach: (isWorkflow: boolean, userQuery: string) => {
|
||||
const approachType = isWorkflow ? 'Untersuchungsansatz' : 'Lösungsansatz';
|
||||
const focus = isWorkflow ?
|
||||
'Triage-Prioritäten, Phasenabfolge, Kontaminationsvermeidung' :
|
||||
'Methodenauswahl, Validierung, Integration';
|
||||
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`;
|
||||
|
||||
return `Entwickle einen ${approachType}.
|
||||
return `Basierend auf der Analyse entwickeln Sie einen fundierten ${approachType} nach NIST SP 800-86 Methodik.
|
||||
|
||||
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||
|
||||
Fokus: ${focus}
|
||||
Entwickeln Sie einen systematischen ${approachType} unter Berücksichtigung von:
|
||||
|
||||
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
|
||||
${considerations}
|
||||
|
||||
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 150 Wörter.`;
|
||||
},
|
||||
|
||||
criticalConsiderations: (isWorkflow: boolean, userQuery: string) => {
|
||||
const focus = isWorkflow ?
|
||||
'Beweissicherung vs. Gründlichkeit, Chain of Custody' :
|
||||
'Tool-Validierung, False Positives/Negatives, Qualifikationen';
|
||||
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`;
|
||||
|
||||
return `Identifiziere kritische Überlegungen.
|
||||
return `Identifizieren Sie ${considerationType} für diesen Fall.
|
||||
|
||||
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||
|
||||
Fokus: ${focus}
|
||||
Berücksichtigen Sie folgende forensische Aspekte:
|
||||
|
||||
Antwort: Fließtext ohne Listen, max 100 Wörter.`;
|
||||
${aspects}
|
||||
|
||||
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.`;
|
||||
},
|
||||
|
||||
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
|
||||
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}".
|
||||
return `Wählen Sie 2-3 Methoden/Tools für die Phase "${phase.name}" und bewerten Sie deren Aufgaben-Eignung VERGLEICHEND.
|
||||
|
||||
SZENARIO: "${userQuery}"
|
||||
PHASE: ${phase.name} - ${phase.description || ''}
|
||||
SPEZIFISCHE PHASE: ${phase.name} - ${phase.description || 'Forensische Untersuchungsphase'}
|
||||
|
||||
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'}
|
||||
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')}
|
||||
|
||||
${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'}
|
||||
Bewerten Sie ALLE Tools vergleichend für diese spezifische Aufgabe UND Phase. Wählen Sie die 2-3 besten aus.
|
||||
|
||||
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
|
||||
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?
|
||||
|
||||
WICHTIG: Verwende EXAKT die Namen wie oben aufgelistet (ohne Präfixe wie M1./T2.)!
|
||||
|
||||
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB:
|
||||
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
|
||||
[
|
||||
{
|
||||
"toolName": "Exakter Name aus der Liste oben",
|
||||
"toolName": "Exakter Tool-Name",
|
||||
"taskRelevance": 85,
|
||||
"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"]
|
||||
"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) => {
|
||||
const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
|
||||
|
||||
return `Erkläre die Anwendung dieser/dieses ${itemType}.
|
||||
return `Sie sind ein DFIR-Experte. Erklären Sie DETAILLIERT die Anwendung dieses bereits bewerteten Tools.
|
||||
|
||||
PROBLEM: "${userQuery}"
|
||||
${itemType.toUpperCase()}: ${tool.name} (${taskRelevance}% Eignung)
|
||||
TYP: ${tool.type}
|
||||
TOOL: ${tool.name} (bereits bewertet mit ${taskRelevance}% Aufgaben-Eignung)
|
||||
BESCHREIBUNG: ${tool.description}
|
||||
|
||||
Bereits als Rang ${rank} bewertet.
|
||||
Das Tool wurde bereits als Rang ${rank} für diese Aufgabe bewertet. Erklären Sie nun:
|
||||
|
||||
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB DER JSON-STRUKTUR:
|
||||
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
|
||||
{
|
||||
"detailed_explanation": "Warum und wie einsetzen",
|
||||
"implementation_approach": "Konkrete Schritte",
|
||||
"pros": ["Vorteil 1", "Vorteil 2"],
|
||||
"limitations": ["Einschränkung 1"],
|
||||
"alternatives": "Alternative Ansätze"
|
||||
}`;
|
||||
"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ähle 2-4 relevante Konzepte.
|
||||
return `Wählen Sie relevante forensische Konzepte für das Verständnis der empfohlenen Methodik.
|
||||
|
||||
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ')}
|
||||
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ')}
|
||||
|
||||
VERFÜGBARE KONZEPTE (${availableConcepts.length} KI-kuratiert):
|
||||
${availableConcepts.map((c: any) =>
|
||||
`- ${c.name}: ${c.description}...`
|
||||
).join('\n')}
|
||||
VERFÜGBARE KONZEPTE:
|
||||
${availableConcepts.slice(0, 15).map((concept: any) => `- ${concept.name}: ${concept.description.slice(0, 80)}...`).join('\n')}
|
||||
|
||||
ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT OHNE JEGLICHEN TEXT AUSSERHALB DER JSON-STRUKTUR:
|
||||
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": "Name",
|
||||
"relevance": "Warum kritisch für Methodik"
|
||||
"conceptName": "Exakter Konzept-Name",
|
||||
"relevance": "Forensische Relevanz: Warum dieses Konzept für das Verständnis der Methodik kritisch ist"
|
||||
}
|
||||
]`;
|
||||
},
|
||||
|
||||
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';
|
||||
const prompt = isWorkflow ?
|
||||
`Erstellen Sie eine Workflow-Empfehlung basierend auf DFIR-Prinzipien.
|
||||
|
||||
return `Erstelle ${isWorkflow ? 'Workflow-Empfehlung' : 'methodische Überlegungen'}.
|
||||
SZENARIO: "${userQuery}"
|
||||
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ') || 'Keine Tools ausgewählt'}
|
||||
|
||||
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||
AUSGEWÄHLT: ${selectedToolNames.join(', ')}${selectedToolNames.length > 5 ? '...' : ''}
|
||||
Erstellen Sie konkrete methodische Workflow-Schritte für dieses spezifische Szenario unter Berücksichtigung forensischer Best Practices, Objektivität und rechtlicher Verwertbarkeit.
|
||||
|
||||
Fokus: ${focus}
|
||||
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.` :
|
||||
|
||||
Antwort: Fließtext ohne Listen, max ${isWorkflow ? '100' : '80'} Wörter.`;
|
||||
`Erstellen Sie wichtige methodische Überlegungen für die korrekte Methoden-/Tool-Anwendung.
|
||||
|
||||
PROBLEM: "${userQuery}"
|
||||
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ') || 'Keine Methoden/Tools ausgewählt'}
|
||||
|
||||
Geben Sie kritische methodische Überlegungen, Validierungsanforderungen und Qualitätssicherungsmaßnahmen für die korrekte Anwendung der empfohlenen Methoden/Tools.
|
||||
|
||||
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 100 Wörter.`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
} 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];
|
||||
|
@ -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 Sonnett (Prompt: Mario Stöckl)"
|
||||
author: "Claude 4 Sonnet (Research)"
|
||||
difficulty: "advanced"
|
||||
categories: ["data-collection"]
|
||||
tags: ["imaging", "filesystem", "hardware-interface"]
|
@ -1,616 +0,0 @@
|
||||
---
|
||||
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
|
@ -1,471 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,377 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,666 +0,0 @@
|
||||
---
|
||||
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
|
@ -1,517 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,556 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,770 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,601 +0,0 @@
|
||||
---
|
||||
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
|
@ -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-08-10
|
||||
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
|
||||
last_updated: 2025-07-20
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "intermediate"
|
||||
categories: ["incident-response", "forensics", "penetration-testing"]
|
||||
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]
|
@ -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 Sonnett (Prompt: Mario Stöckl)"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "intermediate"
|
||||
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
|
||||
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]
|
@ -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 Sonnett (Prompt: Mario Stöckl)"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "novice"
|
||||
categories: ["collaboration-general"]
|
||||
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]
|
110
src/content/knowledgebase/regular-expressions-regex.md
Normal file
110
src/content/knowledgebase/regular-expressions-regex.md
Normal file
@ -0,0 +1,110 @@
|
||||
---
|
||||
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.
|
||||
|
@ -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 Sonnett (Prompt: Mario Stöckl)"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "advanced"
|
||||
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
||||
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
5480
src/data/tools.yaml
5480
src/data/tools.yaml
File diff suppressed because it is too large
Load Diff
6
src/env.d.ts
vendored
6
src/env.d.ts
vendored
@ -11,9 +11,6 @@ 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;
|
||||
@ -42,9 +39,6 @@ declare global {
|
||||
toggleAllScenarios?: () => void;
|
||||
showShareDialog?: (shareButton: Element) => void;
|
||||
modalHideInProgress?: boolean;
|
||||
|
||||
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
|
||||
shareCurrentArticle: (button: HTMLElement) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,6 @@
|
||||
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;
|
||||
@ -25,44 +21,36 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
||||
<script>
|
||||
async function loadUtilityFunctions() {
|
||||
try {
|
||||
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
|
||||
function createToolSlug(toolName) {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
}
|
||||
|
||||
(window as any).createToolSlug = createToolSlug;
|
||||
(window as any).findToolByIdentifier = findToolByIdentifier;
|
||||
(window as any).isToolHosted = isToolHosted;
|
||||
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
|
||||
}
|
||||
|
||||
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) => {
|
||||
function findToolByIdentifier(tools, identifier) {
|
||||
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 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() !== "";
|
||||
};
|
||||
|
||||
console.log('[UTILS] Fallback utility functions registered');
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToElement(element: Element | null, options = {}) {
|
||||
function scrollToElement(element, options = {}) {
|
||||
if (!element) return;
|
||||
|
||||
setTimeout(() => {
|
||||
@ -78,21 +66,17 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function scrollToElementById(elementId: string, options = {}) {
|
||||
function scrollToElementById(elementId, options = {}) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToElementBySelector(selector: string, options = {}) {
|
||||
function scrollToElementBySelector(selector, options = {}) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
function prioritizeSearchResults(tools: any[], searchTerm: string) {
|
||||
function prioritizeSearchResults(tools, searchTerm) {
|
||||
if (!searchTerm || !searchTerm.trim()) {
|
||||
return tools;
|
||||
}
|
||||
@ -100,8 +84,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: string) => tag.toLowerCase());
|
||||
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
const aTagsLower = (a.tags || []).map(tag => tag.toLowerCase());
|
||||
const bTagsLower = (b.tags || []).map(tag => tag.toLowerCase());
|
||||
|
||||
const aExactTag = aTagsLower.includes(lowerSearchTerm);
|
||||
const bExactTag = bTagsLower.includes(lowerSearchTerm);
|
||||
@ -113,15 +97,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', async () => {
|
||||
// CRITICAL: Load utility functions FIRST before any URL handling
|
||||
await loadUtilityFunctions();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const THEME_KEY = 'dfir-theme';
|
||||
|
||||
function getSystemTheme() {
|
||||
@ -132,12 +116,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
return localStorage.getItem(THEME_KEY) || 'auto';
|
||||
}
|
||||
|
||||
function applyTheme(theme: string) {
|
||||
function applyTheme(theme) {
|
||||
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
||||
}
|
||||
|
||||
function updateThemeToggle(theme: string) {
|
||||
function updateThemeToggle(theme) {
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
|
||||
button.setAttribute('data-current-theme', theme);
|
||||
});
|
||||
@ -173,44 +157,6 @@ 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');
|
||||
@ -245,7 +191,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
}
|
||||
}
|
||||
|
||||
async function requireClientAuth(callback: () => void, returnUrl: string, context = 'general') {
|
||||
async function requireClientAuth(callback, returnUrl, context = 'general') {
|
||||
const authStatus = await checkClientAuth(context);
|
||||
|
||||
if (authStatus.authRequired && !authStatus.authenticated) {
|
||||
@ -260,12 +206,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
}
|
||||
}
|
||||
|
||||
async function showIfAuthenticated(selector: string, context = 'general') {
|
||||
async function showIfAuthenticated(selector, context = 'general') {
|
||||
const authStatus = await checkClientAuth(context);
|
||||
const element = document.querySelector(selector);
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated)
|
||||
element.style.display = (!authStatus.authRequired || authStatus.authenticated)
|
||||
? 'inline-flex'
|
||||
: 'none';
|
||||
}
|
||||
@ -294,51 +240,6 @@ 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]');
|
||||
|
||||
@ -346,6 +247,8 @@ 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>
|
||||
|
@ -1,37 +0,0 @@
|
||||
// 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' }
|
||||
});
|
||||
}
|
||||
};
|
22
src/pages/api/ai/embeddings.status.ts
Normal file
22
src/pages/api/ai/embeddings.status.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// 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');
|
||||
}
|
||||
};
|
@ -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;
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX = 5;
|
||||
|
||||
function sanitizeInput(input: string): string {
|
||||
@ -81,13 +81,12 @@ 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):
|
||||
[
|
||||
"spezifische Frage 1?",
|
||||
"spezifische Frage 2?",
|
||||
"spezifische Frage 3?"
|
||||
"Forensisch spezifische Frage 1?",
|
||||
"Forensisch spezifische Frage 2?",
|
||||
"Forensisch spezifische Frage 3?"
|
||||
]
|
||||
|
||||
NUTZER-EINGABE:
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/ai/query.ts
|
||||
// src/pages/api/ai/query.ts - FIXED: Rate limiting for micro-task pipeline
|
||||
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
|
||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
@ -27,7 +27,6 @@ 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()
|
||||
});
|
||||
@ -81,38 +80,6 @@ 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[] = [];
|
||||
|
||||
@ -142,17 +109,6 @@ 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) {
|
||||
@ -187,9 +143,6 @@ 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;
|
||||
@ -200,7 +153,6 @@ 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);
|
||||
}
|
||||
|
||||
@ -222,16 +174,6 @@ 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);
|
||||
|
@ -1,83 +0,0 @@
|
||||
// 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' } }
|
||||
);
|
||||
}
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
// 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';
|
||||
@ -21,9 +22,7 @@ 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',
|
||||
@ -31,29 +30,7 @@ const UPLOAD_CONFIG = {
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
'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'
|
||||
'text/plain', 'text/csv', 'application/json'
|
||||
]),
|
||||
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
|
||||
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
|
||||
@ -73,7 +50,6 @@ 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;
|
||||
}
|
||||
|
||||
@ -82,37 +58,27 @@ 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) {
|
||||
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 };
|
||||
return {
|
||||
valid: false,
|
||||
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
|
||||
};
|
||||
}
|
||||
|
||||
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
||||
const errorMsg = `File type ${file.type} not allowed`;
|
||||
console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
|
||||
return { valid: false, error: errorMsg };
|
||||
return {
|
||||
valid: false,
|
||||
error: `File type ${file.type} not allowed`
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@ -121,7 +87,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
||||
storage: 'nextcloud'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Nextcloud upload failed:', error);
|
||||
console.error('Nextcloud upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
|
||||
@ -131,10 +97,7 @@ 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, '-');
|
||||
@ -143,20 +106,11 @@ 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,
|
||||
@ -165,7 +119,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
||||
storage: 'local'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Local upload failed:', error);
|
||||
console.error('Local upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Local upload failed',
|
||||
@ -176,22 +130,12 @@ 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) {
|
||||
console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
|
||||
return apiError.unauthorized('Authentication required for file uploads');
|
||||
return apiError.unauthorized();
|
||||
}
|
||||
|
||||
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.');
|
||||
@ -199,59 +143,38 @@ 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) {
|
||||
console.error('[UPLOAD] Failed to parse form data:', error);
|
||||
return apiError.badRequest('Invalid form data - could not parse request');
|
||||
return apiError.badRequest('Invalid form data');
|
||||
}
|
||||
|
||||
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 (nextcloudConfigured) {
|
||||
console.log('[UPLOAD] Using Nextcloud as primary storage');
|
||||
if (isNextcloudConfigured()) {
|
||||
result = await uploadToNextcloud(file, userEmail);
|
||||
|
||||
if (!result.success) {
|
||||
console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
|
||||
console.warn('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(`[UPLOAD] Upload completed successfully:`, {
|
||||
filename: result.filename,
|
||||
storage: result.storage,
|
||||
url: result.url,
|
||||
user: userEmail
|
||||
});
|
||||
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
|
||||
|
||||
return apiSpecial.uploadSuccess({
|
||||
url: result.url!,
|
||||
@ -260,12 +183,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
storage: result.storage!
|
||||
});
|
||||
} else {
|
||||
console.error(`[UPLOAD] Upload failed completely:`, {
|
||||
filename: file.name,
|
||||
error: result.error,
|
||||
storage: result.storage,
|
||||
user: userEmail
|
||||
});
|
||||
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
|
||||
|
||||
return apiSpecial.uploadFailed(result.error!);
|
||||
}
|
||||
@ -275,8 +193,6 @@ 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();
|
||||
@ -288,14 +204,12 @@ 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('[UPLOAD] Local upload directory not accessible:', error);
|
||||
console.warn('Local upload directory not accessible:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,14 +237,9 @@ 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');
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
// src/pages/contribute/knowledgebase.astro
|
||||
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { withAuth } from '../../utils/auth.js';
|
||||
import { getToolsData } from '../../utils/dataService.js';
|
||||
@ -114,13 +114,8 @@ 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,.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">
|
||||
<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">
|
||||
<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"/>
|
||||
@ -309,12 +304,6 @@ 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,
|
||||
@ -328,98 +317,30 @@ 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) {
|
||||
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);
|
||||
if (!fileItem) return;
|
||||
|
||||
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) {
|
||||
console.log('[UPLOAD] Success result:', responseData);
|
||||
|
||||
const result = await response.json();
|
||||
fileItem.uploaded = true;
|
||||
fileItem.url = responseData.url;
|
||||
fileItem.url = result.url;
|
||||
this.renderFileList();
|
||||
|
||||
} else {
|
||||
|
||||
if (responseData && responseData.details) {
|
||||
console.error('[UPLOAD] Error details:', responseData.details);
|
||||
}
|
||||
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
|
||||
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown upload error';
|
||||
|
||||
this.showMessage('error', `Failed to upload ${fileItem.name}`);
|
||||
this.removeFile(fileId);
|
||||
}
|
||||
}
|
||||
@ -491,6 +412,7 @@ 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;
|
||||
@ -519,6 +441,18 @@ 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);
|
||||
}
|
||||
|
@ -22,17 +22,6 @@ 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'}>
|
||||
@ -205,27 +194,16 @@ const allConcepts = existingTools
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
@ -233,12 +211,9 @@ const allConcepts = existingTools
|
||||
<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-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>
|
||||
<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." />
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
@ -299,275 +274,7 @@ const allConcepts = existingTools
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
|
||||
console.log('[FORM] Script loaded, initializing...');
|
||||
|
||||
class ContributionForm {
|
||||
@ -576,7 +283,6 @@ class ContributionForm {
|
||||
this.editTool = editTool;
|
||||
this.elements = {};
|
||||
this.isSubmitting = false;
|
||||
this.autocompleteManagers = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -597,20 +303,14 @@ class ContributionForm {
|
||||
yamlPreview: document.getElementById('yaml-preview'),
|
||||
successModal: document.getElementById('success-modal'),
|
||||
softwareFields: document.getElementById('software-fields'),
|
||||
relationsFields: document.getElementById('relations-fields'),
|
||||
conceptsFields: document.getElementById('concepts-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'),
|
||||
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')
|
||||
licenseInput: document.getElementById('license')
|
||||
};
|
||||
|
||||
if (!this.elements.form || !this.elements.submitBtn) {
|
||||
@ -627,7 +327,6 @@ class ContributionForm {
|
||||
|
||||
console.log('[FORM] Setting up handlers...');
|
||||
this.setupEventListeners();
|
||||
this.setupAutocomplete();
|
||||
this.updateFieldVisibility();
|
||||
this.setupCharacterCounters();
|
||||
this.updateYAMLPreview();
|
||||
@ -635,65 +334,6 @@ 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();
|
||||
@ -726,28 +366,22 @@ class ContributionForm {
|
||||
updateFieldVisibility() {
|
||||
const type = this.elements.typeSelect.value;
|
||||
|
||||
// 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';
|
||||
this.elements.softwareFields.style.display = 'none';
|
||||
this.elements.conceptsFields.style.display = 'none';
|
||||
|
||||
// Always show relations - all tool types can have relationships
|
||||
this.elements.relationsFields.style.display = 'block';
|
||||
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
|
||||
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
|
||||
|
||||
// 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';
|
||||
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';
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@ -806,22 +440,14 @@ class ContributionForm {
|
||||
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);
|
||||
const tags = formData.get('tags');
|
||||
if (tags) {
|
||||
tool.tags = tags.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 relatedConcepts = formData.getAll('relatedConcepts');
|
||||
if (relatedConcepts.length > 0) {
|
||||
tool.related_concepts = relatedConcepts;
|
||||
}
|
||||
|
||||
const yaml = this.generateYAML(tool);
|
||||
@ -860,9 +486,6 @@ class ContributionForm {
|
||||
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');
|
||||
}
|
||||
@ -937,7 +560,6 @@ class ContributionForm {
|
||||
|
||||
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
console.log('[FORM] Submit handler called!');
|
||||
|
||||
@ -975,32 +597,14 @@ class ContributionForm {
|
||||
phases: formData.getAll('phases'),
|
||||
skillLevel: formData.get('skillLevel'),
|
||||
url: formData.get('url'),
|
||||
tags: []
|
||||
tags: formData.get('tags') ?
|
||||
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
|
||||
},
|
||||
metadata: {
|
||||
reason: formData.get('reason') || '',
|
||||
contact: formData.get('contact') || ''
|
||||
reason: formData.get('reason') || ''
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
@ -1016,6 +620,13 @@ class ContributionForm {
|
||||
}
|
||||
}
|
||||
|
||||
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', {
|
||||
@ -1070,14 +681,6 @@ class ContributionForm {
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clean up autocomplete managers
|
||||
this.autocompleteManagers.forEach(manager => {
|
||||
manager.destroy();
|
||||
});
|
||||
this.autocompleteManagers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeForm() {
|
||||
@ -1104,4 +707,3 @@ if (document.readyState === 'loading') {
|
||||
|
||||
console.log('[FORM] Script loaded successfully');
|
||||
</script>
|
||||
</BaseLayout>
|
@ -6,18 +6,10 @@ 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="~/">
|
||||
@ -44,21 +36,6 @@ if (aiAuthRequired) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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"/>
|
||||
@ -70,7 +47,6 @@ if (aiAuthRequired) {
|
||||
<polyline points="7,7 17,7 17,17"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div class="ai-features-mini">
|
||||
<span class="badge badge-secondary">Workflow-Empfehlungen</span>
|
||||
@ -202,39 +178,7 @@ if (aiAuthRequired) {
|
||||
<ToolFilters data={data} />
|
||||
</section>
|
||||
|
||||
{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">
|
||||
@ -251,7 +195,7 @@ if (aiAuthRequired) {
|
||||
<ToolMatrix data={data} />
|
||||
</BaseLayout>
|
||||
|
||||
<script define:vars={{ toolsData: data.tools, phases: data.phases, aiAuthRequired: aiAuthRequired, aiAuthenticated: aiAuthContext?.authenticated }}>
|
||||
<script define:vars={{ toolsData: data.tools, phases: data.phases }}>
|
||||
window.toolsData = toolsData;
|
||||
|
||||
window.selectApproach = function(approach) {
|
||||
@ -267,18 +211,14 @@ if (aiAuthRequired) {
|
||||
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');
|
||||
@ -298,12 +238,9 @@ if (aiAuthRequired) {
|
||||
selectedCard.classList.add('active');
|
||||
}
|
||||
|
||||
const phaseSelect = document.getElementById('phase-select');
|
||||
if (phaseSelect) {
|
||||
phaseSelect.value = phase;
|
||||
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
phaseSelect.dispatchEvent(changeEvent);
|
||||
const existingPhaseButton = document.querySelector(`[data-phase="${phase}"]`);
|
||||
if (existingPhaseButton && !existingPhaseButton.classList.contains('active')) {
|
||||
existingPhaseButton.click();
|
||||
}
|
||||
|
||||
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
|
||||
@ -311,9 +248,7 @@ if (aiAuthRequired) {
|
||||
gridToggle.click();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.scrollToElementById('tools-grid');
|
||||
}, 200);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@ -324,20 +259,12 @@ if (aiAuthRequired) {
|
||||
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');
|
||||
@ -356,79 +283,58 @@ if (aiAuthRequired) {
|
||||
}
|
||||
|
||||
function switchToView(view) {
|
||||
console.log('[VIEW] Switching to view:', 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');
|
||||
|
||||
const methodologySection = document.getElementById('methodology-section');
|
||||
const targetedSection = document.getElementById('targeted-section');
|
||||
|
||||
if (toolsGrid) toolsGrid.style.display = 'none';
|
||||
if (matrixContainer) {
|
||||
matrixContainer.style.display = 'none';
|
||||
matrixContainer.classList.add('hidden');
|
||||
}
|
||||
if (matrixContainer) matrixContainer.style.display = 'none';
|
||||
if (aiInterface) aiInterface.style.display = 'none';
|
||||
if (filtersSection) filtersSection.style.display = 'none';
|
||||
if (noResults) noResults.style.display = 'none';
|
||||
|
||||
if (methodologySection) methodologySection.classList.remove('active');
|
||||
if (targetedSection) targetedSection.classList.remove('active');
|
||||
|
||||
switch (view) {
|
||||
case 'grid':
|
||||
console.log('[VIEW] Showing grid view');
|
||||
if (toolsGrid) toolsGrid.style.display = 'block';
|
||||
if (filtersSection) filtersSection.style.display = 'block';
|
||||
break;
|
||||
|
||||
case 'matrix':
|
||||
console.log('[VIEW] Showing matrix view');
|
||||
if (matrixContainer) {
|
||||
matrixContainer.style.display = 'block';
|
||||
matrixContainer.classList.remove('hidden');
|
||||
}
|
||||
if (matrixContainer) matrixContainer.style.display = 'block';
|
||||
if (filtersSection) filtersSection.style.display = 'block';
|
||||
break;
|
||||
|
||||
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';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (aiInterface) aiInterface.style.display = 'block';
|
||||
|
||||
if (filtersSection) {
|
||||
filtersSection.style.display = 'block';
|
||||
|
||||
const filterSections = filtersSection.querySelectorAll('.filter-section');
|
||||
function hideFilterControls() {
|
||||
const filterSections = document.querySelectorAll('.filter-section');
|
||||
filterSections.forEach((section, index) => {
|
||||
if (index === filterSections.length - 1) {
|
||||
section.style.display = 'block';
|
||||
} else {
|
||||
if (index < filterSections.length - 1) {
|
||||
section.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[VIEW] Unknown view:', view);
|
||||
}
|
||||
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 (view !== 'ai' && filtersSection) {
|
||||
const filterSections = filtersSection.querySelectorAll('.filter-section');
|
||||
filterSections.forEach(section => {
|
||||
section.style.display = 'block';
|
||||
});
|
||||
}
|
||||
filterSections.forEach(section => section.style.display = 'block');
|
||||
|
||||
if (searchInput) searchInput.style.display = 'block';
|
||||
if (tagCloud) tagCloud.style.display = 'flex';
|
||||
if (tagControls) tagControls.style.display = 'flex';
|
||||
|
||||
allInputs.forEach(input => input.style.display = 'block');
|
||||
checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex');
|
||||
}
|
||||
|
||||
window.navigateToGrid = function(toolName) {
|
||||
@ -510,8 +416,6 @@ if (aiAuthRequired) {
|
||||
};
|
||||
|
||||
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');
|
||||
@ -524,18 +428,12 @@ if (aiAuthRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.findToolByIdentifier) {
|
||||
console.error('[SHARE] findToolByIdentifier not available, retrying...');
|
||||
setTimeout(() => handleSharedURL(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
|
||||
if (!tool) {
|
||||
console.warn('Shared tool not found:', toolParam);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
|
||||
@ -557,11 +455,11 @@ if (aiAuthRequired) {
|
||||
default:
|
||||
window.navigateToGrid(tool.name);
|
||||
}
|
||||
}, 300);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
window.addEventListener('toolsFiltered', (event) => {
|
||||
const { tools: filtered, semanticSearch } = event.detail;
|
||||
const filtered = event.detail;
|
||||
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
||||
|
||||
if (currentView === 'matrix' || currentView === 'ai') {
|
||||
@ -570,80 +468,11 @@ if (aiAuthRequired) {
|
||||
|
||||
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;
|
||||
|
||||
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++;
|
||||
@ -652,45 +481,20 @@ if (aiAuthRequired) {
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
if (!event.triggeredByButton) {
|
||||
switchToView(view);
|
||||
}
|
||||
});
|
||||
|
||||
window.switchToAIView = () => switchToView('ai');
|
||||
window.switchToView = switchToView;
|
||||
|
||||
// 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>
|
@ -108,15 +108,13 @@ 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='${articleUrl}'`}
|
||||
onclick={`window.location.href='/knowledgebase/${entry.slug}'`}
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="flex-between mb-3">
|
||||
@ -139,7 +137,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={articleUrl} class="btn btn-primary btn-sm">
|
||||
<a href={`/knowledgebase/${entry.slug}`} 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"/>
|
||||
@ -147,21 +145,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||
</svg>
|
||||
Öffnen
|
||||
</a>
|
||||
<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>
|
||||
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -299,7 +283,5 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||
lastScrollY = window.scrollY;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
</script>
|
@ -47,141 +47,117 @@ 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}>
|
||||
<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>}
|
||||
<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>}
|
||||
{entry.data.title}
|
||||
</h1>
|
||||
<p class="article-description">{entry.data.description}</p>
|
||||
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="article-metadata-grid">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Typ</span>
|
||||
<div class="metadata-badges">
|
||||
<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 badge-accent">Artikel</span>
|
||||
<span class="badge" style="background-color: var(--color-accent); color: white;">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>}
|
||||
{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 class="metadata-item">
|
||||
<span class="metadata-label">Schwierigkeit</span>
|
||||
<span class="metadata-value">{entry.data.difficulty}</span>
|
||||
<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 class="metadata-item">
|
||||
<span class="metadata-label">Aktualisiert</span>
|
||||
<span class="metadata-value">{entry.data.last_updated.toLocaleDateString('de-DE')}</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Autor</span>
|
||||
<span class="metadata-value">{entry.data.author}</span>
|
||||
<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 class="metadata-item">
|
||||
<span class="metadata-label">Lesezeit</span>
|
||||
<span class="metadata-value" id="reading-time">~5 min</span>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- Article Content -->
|
||||
<main class="article-main">
|
||||
<article class="article-content">
|
||||
<div class="markdown-content">
|
||||
<div class="card" style="padding: 2rem;">
|
||||
<div class="kb-content markdown-content" style="line-height: 1.7;">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Article Footer -->
|
||||
<footer class="article-footer">
|
||||
<div class="article-footer-actions">
|
||||
<h3>Tool-Aktionen</h3>
|
||||
<div class="footer-actions-grid">
|
||||
<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">
|
||||
<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-concept">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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"/>
|
||||
@ -189,8 +165,8 @@ const currentUrl = Astro.url.href;
|
||||
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">
|
||||
<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"/>
|
||||
@ -200,16 +176,16 @@ const currentUrl = Astro.url.href;
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
<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>
|
||||
Homepage
|
||||
Software-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">
|
||||
<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"/>
|
||||
@ -222,8 +198,35 @@ const currentUrl = Astro.url.href;
|
||||
</>
|
||||
)}
|
||||
|
||||
{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"/>
|
||||
</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);">
|
||||
{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>
|
||||
))}
|
||||
</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">
|
||||
<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>
|
||||
@ -231,251 +234,5 @@ const currentUrl = Astro.url.href;
|
||||
</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" class="related-tool-card">
|
||||
{tool.icon && <span class="tool-icon">{tool.icon}</span>}
|
||||
<span class="tool-name">{tool.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
|
||||
</article>
|
||||
</BaseLayout>
|
@ -1,407 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
/* ============================================================================
|
||||
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);
|
||||
}
|
@ -14,7 +14,72 @@
|
||||
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
|
||||
@ -682,13 +747,11 @@ input[type="checkbox"] {
|
||||
================================================================= */
|
||||
|
||||
.tool-card {
|
||||
min-height: 300px;
|
||||
max-height: 350px;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-card-header {
|
||||
@ -697,7 +760,6 @@ input[type="checkbox"] {
|
||||
align-items: flex-start;
|
||||
min-height: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-card-header h3 {
|
||||
@ -726,7 +788,6 @@ input[type="checkbox"] {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
word-break: break-word;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-card-metadata {
|
||||
@ -735,7 +796,6 @@ input[type="checkbox"] {
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
@ -764,11 +824,10 @@ input[type="checkbox"] {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
max-height: 3.5rem;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
flex: 0 0 3.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-tags-container::after {
|
||||
@ -802,8 +861,6 @@ input[type="checkbox"] {
|
||||
.tool-card-buttons {
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
flex-basis: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
@ -1632,156 +1689,6 @@ 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)
|
||||
================================================================= */
|
||||
@ -2064,9 +1971,6 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.phase-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
@ -3530,16 +3434,6 @@ 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) {
|
||||
@ -3699,30 +3593,179 @@ 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;
|
||||
}
|
||||
|
||||
}
|
@ -1,661 +0,0 @@
|
||||
/* ==========================================================================
|
||||
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; }
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
: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
@ -1,411 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
};
|
@ -260,6 +260,8 @@ 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;
|
||||
}
|
||||
|
@ -1,366 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -128,9 +128,7 @@ 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 = {
|
||||
@ -146,15 +144,8 @@ async function loadRawData(): Promise<ToolsData> {
|
||||
console.log(`[DATA SERVICE] Loaded enhanced data version: ${dataVersion}`);
|
||||
|
||||
} catch (error) {
|
||||
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}`);
|
||||
console.error('YAML validation failed:', error);
|
||||
throw new Error('Invalid tools.yaml structure');
|
||||
}
|
||||
}
|
||||
return cachedData;
|
||||
|
@ -2,8 +2,6 @@
|
||||
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;
|
||||
@ -37,52 +35,12 @@ class EmbeddingsService {
|
||||
private readonly embeddingsPath = path.join(process.cwd(), 'data', 'embeddings.json');
|
||||
private readonly batchSize: number;
|
||||
private readonly batchDelay: number;
|
||||
private enabled: boolean = false;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
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> {
|
||||
@ -99,55 +57,60 @@ 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 = await this.hashToolsFile();
|
||||
const currentDataHash = this.hashData(toolsData);
|
||||
|
||||
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);
|
||||
const existingEmbeddings = await this.loadEmbeddings();
|
||||
|
||||
const cacheIsUsable =
|
||||
existing &&
|
||||
existing.version === currentDataHash &&
|
||||
Array.isArray(existing.embeddings) &&
|
||||
existing.embeddings.length > 0;
|
||||
|
||||
if (cacheIsUsable) {
|
||||
if (existingEmbeddings && existingEmbeddings.version === currentDataHash) {
|
||||
console.log('[EMBEDDINGS] Using cached embeddings');
|
||||
this.embeddings = existing.embeddings;
|
||||
this.embeddings = existingEmbeddings.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 in ${Date.now() - initStart} ms`);
|
||||
} catch (err) {
|
||||
console.error('[EMBEDDINGS] Failed to initialize:', err);
|
||||
console.log(`[EMBEDDINGS] Initialized with ${this.embeddings.length} embeddings`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[EMBEDDINGS] Failed to initialize:', error);
|
||||
this.isInitialized = false;
|
||||
throw err;
|
||||
throw error;
|
||||
} finally {
|
||||
this.initializationPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
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 loadEmbeddings(): Promise<EmbeddingsDatabase | null> {
|
||||
@ -189,10 +152,7 @@ class EmbeddingsService {
|
||||
const model = process.env.AI_EMBEDDINGS_MODEL;
|
||||
|
||||
if (!endpoint || !model) {
|
||||
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(', ')}`);
|
||||
throw new Error('Missing embeddings API configuration');
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
@ -280,35 +240,10 @@ 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;
|
||||
@ -324,15 +259,12 @@ class EmbeddingsService {
|
||||
}
|
||||
|
||||
async findSimilar(query: string, maxResults: number = 30, threshold: number = 0.3): Promise<SimilarityResult[]> {
|
||||
if (!this.enabled) {
|
||||
console.log('[EMBEDDINGS] Service disabled for similarity search');
|
||||
if (!this.enabled || !this.isInitialized || this.embeddings.length === 0) {
|
||||
console.log('[EMBEDDINGS] Service not available for similarity search');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.isInitialized && this.embeddings.length > 0) {
|
||||
console.log(`[EMBEDDINGS] Using embeddings data for similarity search: ${query}`);
|
||||
|
||||
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
|
||||
const queryEmbedding = queryEmbeddings[0];
|
||||
|
||||
@ -343,15 +275,11 @@ class EmbeddingsService {
|
||||
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
|
||||
}));
|
||||
|
||||
const topScore = Math.max(...similarities.map(s => s.similarity));
|
||||
const dynamicCutOff = Math.max(threshold, topScore * 0.85);
|
||||
|
||||
const results = similarities
|
||||
.filter(item => item.similarity >= dynamicCutOff)
|
||||
.filter(item => item.similarity >= threshold)
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.slice(0, maxResults);
|
||||
|
||||
|
||||
const orderingValid = results.every((item, index) => {
|
||||
if (index === 0) return true;
|
||||
return item.similarity <= results[index - 1].similarity;
|
||||
@ -380,66 +308,6 @@ class EmbeddingsService {
|
||||
|
||||
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);
|
||||
return [];
|
||||
@ -447,11 +315,7 @@ class EmbeddingsService {
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
if (!this.enabled && !this.isInitialized) {
|
||||
this.checkEnabledStatus().catch(console.error);
|
||||
}
|
||||
|
||||
return this.enabled;
|
||||
return this.enabled && this.isInitialized;
|
||||
}
|
||||
|
||||
getStats(): { enabled: boolean; initialized: boolean; count: number } {
|
||||
@ -463,15 +327,14 @@ class EmbeddingsService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const embeddingsService = new EmbeddingsService();
|
||||
|
||||
export { embeddingsService, type EmbeddingData, type SimilarityResult };
|
||||
|
||||
export const debugEmbeddings = {
|
||||
async recheckEnvironment() {
|
||||
return embeddingsService.forceRecheckEnvironment();
|
||||
},
|
||||
getStatus() {
|
||||
return embeddingsService.getStats();
|
||||
if (typeof window === 'undefined' && process.env.NODE_ENV !== 'test') {
|
||||
embeddingsService.initialize().catch(error => {
|
||||
console.error('[EMBEDDINGS] Auto-initialization failed:', error);
|
||||
});
|
||||
}
|
||||
};
|
@ -5,23 +5,22 @@ export interface ContributionData {
|
||||
type: 'add' | 'edit';
|
||||
tool: {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
icon?: string;
|
||||
type: 'software' | 'method' | 'concept';
|
||||
description: string;
|
||||
domains: string[];
|
||||
phases: string[];
|
||||
platforms: string[];
|
||||
skillLevel: string;
|
||||
accessType?: string | null;
|
||||
accessType?: string;
|
||||
url: string;
|
||||
projectUrl?: string | null;
|
||||
license?: string | null;
|
||||
knowledgebase?: boolean | null;
|
||||
'domain-agnostic-software'?: string[] | null;
|
||||
related_concepts?: string[] | null;
|
||||
related_software?: string[] | null;
|
||||
projectUrl?: string;
|
||||
license?: string;
|
||||
knowledgebase?: boolean;
|
||||
'domain-agnostic-software'?: string[];
|
||||
related_concepts?: string[];
|
||||
tags: string[];
|
||||
statusUrl?: string | null;
|
||||
statusUrl?: string;
|
||||
};
|
||||
metadata: {
|
||||
submitter: string;
|
||||
@ -135,7 +134,6 @@ 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'];
|
||||
@ -274,8 +272,6 @@ ${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}
|
||||
@ -303,6 +299,9 @@ ${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}`);
|
||||
@ -311,12 +310,18 @@ ${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');
|
||||
@ -325,12 +330,18 @@ ${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) => {
|
||||
@ -348,6 +359,9 @@ ${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;
|
||||
|
||||
@ -358,12 +372,18 @@ ${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/`');
|
||||
|
@ -338,7 +338,7 @@ export class NextcloudUploader {
|
||||
info: {
|
||||
path: remotePath,
|
||||
exists: true,
|
||||
response: text.substring(0, 200) + '...'
|
||||
response: text.substring(0, 200) + '...' // Truncated for safety
|
||||
}
|
||||
};
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user