+
+
+
+
+
+ {% if page.url == "/status/" %}
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/src/about/index.njk b/src/about/index.njk
index e69de29..fc6f398 100644
--- a/src/about/index.njk
+++ b/src/about/index.njk
@@ -0,0 +1,164 @@
+---
+layout: base.njk
+title: "About"
+description: "Learn about the FOSS DFIR Tools Framework and our mission"
+---
+
+
+
+
+
About
+
+ Learn more about the FOSS DFIR Tools Framework and our mission.
+
+
+
+
+
Our Mission
+
+ The FOSS DFIR Tools Framework serves as a comprehensive hub for digital forensics and incident response professionals.
+ We curate and organize open-source tools to make them easily discoverable and accessible to the security community.
+ Our platform bridges the gap between scattered tool documentation and practical implementation in academic and
+ professional environments.
+
+
+
Framework Methodology
+
+ Our tool categorization follows the standard DFIR methodology established by Kent, Chevalier, Grance, and Dang
+ in the NIST Special Publication 800-86. This systematic approach ensures that tools are organized according to
+ their primary function within the investigation process:
+
+
+
+
Data Collection
+
+ Tools for acquiring and preserving digital evidence while maintaining chain of custody and ensuring data integrity.
+
+
+
+
Examination
+
+ Tools for extracting, parsing, and organizing data from collected evidence in preparation for analysis.
+
+
+
+
Analysis
+
+ Tools for correlating, analyzing, and interpreting evidence to draw conclusions and identify patterns.
+
+
+
+
Reporting
+
+ Tools for documenting findings, creating timelines, and generating comprehensive investigation reports.
+
+
+
+
+
Academic Focus
+
+ This framework was designed specifically for academic and laboratory environments where self-hosted solutions
+ are preferred over cloud-based services. We emphasize tools that can be deployed locally, ensuring data sovereignty
+ and compliance with institutional policies. Our "Self-Hosted Services" section provides access to powerful platforms
+ like Timesketch, TheHive, and MISP that can be deployed within your network perimeter.
+
+
+
Community Driven
+
+ This platform is maintained by the DFIR community, for the DFIR community. We welcome contributions,
+ suggestions, and feedback to help improve the framework and keep tool information current. The entire
+ platform is built using static site generation with YAML-driven content management, making it easy for
+ contributors to add new tools or update existing information through simple file edits.
+
+
+
+
How to Contribute
+
+
• Edit src/_data/tools.yaml to add or update tool information
+
• Modify src/_data/services.yaml to configure service monitoring
+
• Submit pull requests with tool suggestions or corrections
+
• Report issues or suggest improvements via GitHub
+
+
+
+
+
+
+
Technical Implementation
+
+
+
+
Architecture
+
+
• Static Site Generator: 11ty (Eleventy)
+
• Content Management: YAML data files
+
• Styling: Sass with utility-first approach
+
• Interactivity: Vanilla JavaScript
+
• Monitoring: Uptime Kuma integration
+
+
+
+
+
Features
+
+
• Sub-500ms search and filtering
+
• Dark/light/auto theme support
+
• Mobile-responsive design
+
• Zero external dependencies
+
• Real-time service status monitoring
+
+
+
+
+
+
Performance Targets
+
+
+ Initial Load:
+ < 2 seconds
+
+
+ Search/Filter:
+ < 500ms
+
+
+ Theme Switch:
+ < 100ms
+
+
+ Modal Open:
+ < 200ms
+
+
+
+
+
+
+
+
Credits and References
+
+
+
+
Methodology Reference
+
+ Kent, K., Chevalier, S., Grance, T., & Dang, H. (2006).
+ Guide to integrating forensic techniques into incident response.
+ NIST Special Publication 800-86. National Institute of Standards and Technology.
+
+ Welcome to the comprehensive directory of Free and Open Source Software (FOSS) tools for Digital Forensics and Incident Response.
+ This framework organizes tools according to the standard DFIR methodology: Data Collection, Examination, Analysis, and Reporting.
+ Use the selectors below to discover tools that match your specific needs, or explore the complete matrix view to see the full ecosystem.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select Tool Dimensions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Matching Tools (0)
+
+
+
+
+
+
+
+
+
+
No tools found matching your criteria.
+
+
+
+
+
+
+
+
+
Select Your Criteria
+
+ Choose a forensics domain and/or DFIR phase above to discover relevant tools,
+ or use the search bar to find specific tools.
+
+
+
+
+
+
+
+
+
Complete Tool Matrix
+
+ Overview of all FOSS DFIR tools organized by domain and phase. Click any tool name for details.
+
+
+
+
+
+
+
+
Domain / Phase
+ {% for phase in phases %}
+
+ {{ phase }}
+
+ {% endfor %}
+
+
+
+ {% for domain in domains %}
+
+
{{ domain }}
+ {% for phase in phases %}
+
+
+
+
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ Self-Hosted Services (0)
+
+
+ Self-hosted DFIR services available in your lab environment.
+
+
+
+
+
+
+
+
+
No self-hosted services found matching your search.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/js/modal.js b/src/js/modal.js
index e69de29..bec856a 100644
--- a/src/js/modal.js
+++ b/src/js/modal.js
@@ -0,0 +1,243 @@
+// Tool detail modal functionality
+(function() {
+ 'use strict';
+
+ let modal;
+ let modalContent;
+ let currentTool = null;
+
+ // Initialize modal system
+ function init() {
+ modal = document.getElementById('tool-modal');
+ modalContent = document.getElementById('tool-modal-content');
+
+ if (!modal || !modalContent) {
+ console.warn('Modal elements not found');
+ return;
+ }
+
+ // Add event listeners
+ setupEventListeners();
+ }
+
+ // Set up event listeners for modal
+ function setupEventListeners() {
+ // Click outside modal to close
+ modal.addEventListener('click', (event) => {
+ if (event.target === modal) {
+ closeModal();
+ }
+ });
+
+ // Escape key to close modal
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape' && !modal.classList.contains('hidden')) {
+ closeModal();
+ }
+ });
+
+ // Prevent body scroll when modal is open
+ modal.addEventListener('scroll', (event) => {
+ event.stopPropagation();
+ });
+ }
+
+ // Show tool detail modal
+ function showToolModal(tool) {
+ if (!tool || !modal || !modalContent) return;
+
+ currentTool = tool;
+
+ // Generate modal content
+ modalContent.innerHTML = generateModalContent(tool);
+
+ // Add close button handler
+ const closeButton = modalContent.querySelector('.modal-close');
+ if (closeButton) {
+ closeButton.addEventListener('click', closeModal);
+ }
+
+ // Add external link handler
+ const externalLink = modalContent.querySelector('.external-link');
+ if (externalLink) {
+ externalLink.addEventListener('click', (event) => {
+ // Let the link work normally, but track the click
+ trackToolClick(tool);
+ });
+ }
+
+ // Show modal
+ modal.classList.remove('hidden');
+ document.body.style.overflow = 'hidden';
+
+ // Focus trap
+ trapFocus();
+ }
+
+ // Close modal
+ function closeModal() {
+ if (!modal) return;
+
+ modal.classList.add('hidden');
+ document.body.style.overflow = '';
+ currentTool = null;
+
+ // Return focus to the tool card that was clicked
+ const activeToolCard = document.querySelector('.tool-card:focus');
+ if (activeToolCard) {
+ activeToolCard.focus();
+ }
+ }
+
+ // Generate modal HTML content
+ function generateModalContent(tool) {
+ const typeLabel = tool.type === 'SaaS' ?
+ 'SaaS' :
+ 'FOSS';
+
+ const domains = (tool.domains || []).map(domain =>
+ `${domain}`
+ ).join('');
+
+ const phases = (tool.phases || []).map(phase =>
+ `${phase}`
+ ).join('');
+
+ const tags = (tool.tags || []).map(tag =>
+ `${tag}`
+ ).join('');
+
+ const platforms = (tool.platforms || []).join(', ');
+
+ // Handle service URL for self-hosted services
+ const serviceInfo = tool.selfHosted && tool.serviceUrl ?
+ `
`
+ ).join('');
+
+ // Add click handlers
+ const toolElements = cell.querySelectorAll('.matrix-cell-tool');
+ toolElements.forEach((element, index) => {
+ element.addEventListener('click', () => showToolModal(cellTools[index]));
+ });
+ }
+ });
+ });
+ }
+
+ // Populate SaaS grid
+ function populateSaasGrid() {
+ if (currentFilters.mode !== 'saas') return;
+
+ const saasGrid = document.getElementById('saas-grid');
+ const saasCount = document.getElementById('saas-count');
+ const noSaasResults = document.getElementById('no-saas-results');
+
+ if (!saasGrid) return;
+
+ const saasTools = filterTools();
+
+ if (saasCount) saasCount.textContent = saasTools.length;
+
+ if (saasTools.length === 0) {
+ saasGrid.classList.add('hidden');
+ if (noSaasResults) noSaasResults.classList.remove('hidden');
+ } else {
+ if (noSaasResults) noSaasResults.classList.add('hidden');
+ saasGrid.classList.remove('hidden');
+
+ saasGrid.innerHTML = saasTools.map(tool => createToolCard(tool)).join('');
+
+ // Add click handlers
+ const toolCards = saasGrid.querySelectorAll('.tool-card');
+ toolCards.forEach((card, index) => {
+ card.addEventListener('click', () => showToolModal(saasTools[index]));
+ });
+ }
+ }
+
+ // Show tool modal (defined in modal.js)
+ function showToolModal(tool) {
+ if (typeof window.showToolModal === 'function') {
+ window.showToolModal(tool);
+ }
+ }
+
+ // Export for use by other scripts
+ window.searchModule = {
+ init,
+ filterTools,
+ updateDisplay,
+ populateMatrix,
+ populateSaasGrid,
+ resetFilters
+ };
+
+ // Auto-initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
\ No newline at end of file
diff --git a/src/js/status.js b/src/js/status.js
index e69de29..ead893d 100644
--- a/src/js/status.js
+++ b/src/js/status.js
@@ -0,0 +1,385 @@
+// Status page functionality for Uptime Kuma integration
+(function() {
+ 'use strict';
+
+ let servicesData = {};
+ let uptimeKumaConfig = {};
+ let refreshInterval;
+
+ // Initialize status page
+ function init() {
+ // Get data from global variables
+ if (typeof window.servicesData !== 'undefined') {
+ servicesData = window.servicesData;
+ }
+
+ if (typeof window.uptimeKumaConfig !== 'undefined') {
+ uptimeKumaConfig = window.uptimeKumaConfig || {};
+ }
+
+ // Set up refresh button
+ const refreshButton = document.getElementById('refresh-status');
+ if (refreshButton) {
+ refreshButton.addEventListener('click', refreshStatus);
+ }
+
+ // Update Kuma configuration display
+ updateKumaStatusDisplay();
+
+ // Initial load
+ loadServiceStatus();
+
+ // Set up auto-refresh if Uptime Kuma is enabled
+ if (uptimeKumaConfig.enabled && uptimeKumaConfig.refreshInterval) {
+ refreshInterval = setInterval(loadServiceStatus, uptimeKumaConfig.refreshInterval);
+ }
+ }
+
+ // Load service status (from Uptime Kuma API or static data)
+ async function loadServiceStatus() {
+ try {
+ let services;
+
+ if (uptimeKumaConfig.enabled && uptimeKumaConfig.apiUrl) {
+ // Try to fetch from Uptime Kuma API
+ services = await fetchFromUptimeKuma();
+ }
+
+ if (!services) {
+ // Fall back to static data
+ services = servicesData.services || [];
+ updateKumaConnectionStatus('static');
+ } else {
+ updateKumaConnectionStatus('connected');
+ }
+
+ // Render services
+ renderServices(services);
+ renderOverallStatus(services);
+ updateLastUpdated();
+
+ } catch (error) {
+ console.error('Failed to load service status:', error);
+
+ // Fall back to static data
+ const services = servicesData.services || [];
+ renderServices(services);
+ renderOverallStatus(services);
+ updateKumaConnectionStatus('error');
+ updateLastUpdated();
+ }
+ }
+
+ // Fetch data from Uptime Kuma API
+ async function fetchFromUptimeKuma() {
+ if (!uptimeKumaConfig.apiUrl) {
+ throw new Error('Uptime Kuma API URL not configured');
+ }
+
+ const headers = {};
+ if (uptimeKumaConfig.apiKey) {
+ headers['Authorization'] = `Bearer ${uptimeKumaConfig.apiKey}`;
+ }
+
+ try {
+ const response = await fetch(`${uptimeKumaConfig.apiUrl}/status-page/heartbeat`, {
+ headers,
+ timeout: 5000
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Transform Uptime Kuma data to our format
+ return transformUptimeKumaData(data);
+
+ } catch (error) {
+ console.warn('Failed to fetch from Uptime Kuma:', error.message);
+ return null;
+ }
+ }
+
+ // Transform Uptime Kuma API response to our service format
+ function transformUptimeKumaData(kumaData) {
+ // This is a simplified transformation - adjust based on actual Uptime Kuma API response
+ const services = [];
+
+ if (kumaData.heartbeatList) {
+ Object.entries(kumaData.heartbeatList).forEach(([monitorId, heartbeats]) => {
+ const monitor = kumaData.monitors?.[monitorId];
+ if (!monitor) return;
+
+ const latestHeartbeat = heartbeats[heartbeats.length - 1];
+ if (!latestHeartbeat) return;
+
+ services.push({
+ id: monitorId,
+ name: monitor.name,
+ description: monitor.description || '',
+ url: monitor.url,
+ category: monitor.tags?.[0] || 'Infrastructure',
+ status: latestHeartbeat.status === 1 ? 'operational' : 'down',
+ uptime: calculateUptime(heartbeats),
+ responseTime: `${latestHeartbeat.ping}ms`,
+ lastChecked: new Date(latestHeartbeat.time).toISOString()
+ });
+ });
+ }
+
+ return services;
+ }
+
+ // Calculate uptime percentage from heartbeat data
+ function calculateUptime(heartbeats) {
+ if (!heartbeats || heartbeats.length === 0) return '0%';
+
+ const total = heartbeats.length;
+ const successful = heartbeats.filter(h => h.status === 1).length;
+ const percentage = (successful / total) * 100;
+
+ return `${percentage.toFixed(1)}%`;
+ }
+
+ // Render services list
+ function renderServices(services) {
+ const serviceList = document.getElementById('service-list');
+ if (!serviceList) return;
+
+ if (!services || services.length === 0) {
+ serviceList.innerHTML = `
+
+
No services configured
+
+ `;
+ return;
+ }
+
+ serviceList.innerHTML = services.map(service => createServiceCard(service)).join('');
+ }
+
+ // Create HTML for a service card
+ function createServiceCard(service) {
+ const statusClass = getStatusClass(service.status);
+ const statusText = service.status.charAt(0).toUpperCase() + service.status.slice(1);
+ const lastChecked = service.lastChecked ?
+ new Date(service.lastChecked).toLocaleString() : 'Unknown';
+
+ const issuesSection = service.issues ?
+ `
+
${service.issues}
+
` : '';
+
+ return `
+
+
+
+
+
+
${service.name}
+ ${service.description ? `
${service.description}
` : ''}
+
+
+ ${statusText}
+
+
+
+ Uptime:
+ ${service.uptime || 'N/A'}
+
+
+ Response:
+ ${service.responseTime || 'N/A'}
+
+
+ Category:
+ ${service.category || 'Unknown'}
+
+
+ Last Check:
+
+ ${formatRelativeTime(service.lastChecked)}
+
+
+
+ ${issuesSection}
+
+ `;
+ }
+
+ // Render overall status summary
+ function renderOverallStatus(services) {
+ if (!services || services.length === 0) return;
+
+ const statusCounts = {
+ operational: services.filter(s => s.status === 'operational').length,
+ degraded: services.filter(s => s.status === 'degraded').length,
+ maintenance: services.filter(s => s.status === 'maintenance').length,
+ down: services.filter(s => s.status === 'down').length
+ };
+
+ const total = services.length;
+
+ // Determine overall status
+ let overallStatus, overallText;
+ if (statusCounts.down > 0) {
+ overallStatus = 'down';
+ overallText = 'Major Outage';
+ } else if (statusCounts.degraded > 0 || statusCounts.maintenance > 0) {
+ overallStatus = statusCounts.maintenance > 0 ? 'maintenance' : 'degraded';
+ overallText = statusCounts.maintenance > 0 ? 'Partial Service' : 'Degraded Performance';
+ } else {
+ overallStatus = 'operational';
+ overallText = 'All Systems Operational';
+ }
+
+ // Update overall status display
+ const indicator = document.getElementById('overall-indicator');
+ const textElement = document.getElementById('overall-text');
+ const summaryElement = document.getElementById('overall-summary');
+
+ if (indicator) {
+ const statusDot = indicator.querySelector('.status-indicator');
+ if (statusDot) {
+ statusDot.className = `status-indicator ${overallStatus}`;
+ }
+ }
+
+ if (textElement) {
+ textElement.textContent = overallText;
+ textElement.className = getStatusTextClass(overallStatus);
+ }
+
+ if (summaryElement) {
+ const parts = [];
+ if (statusCounts.operational > 0) parts.push(`${statusCounts.operational} operational`);
+ if (statusCounts.degraded > 0) parts.push(`${statusCounts.degraded} degraded`);
+ if (statusCounts.maintenance > 0) parts.push(`${statusCounts.maintenance} maintenance`);
+ if (statusCounts.down > 0) parts.push(`${statusCounts.down} down`);
+
+ summaryElement.textContent = `${parts.join(' • ')} of ${total} services`;
+ }
+ }
+
+ // Update Uptime Kuma connection status display
+ function updateKumaConnectionStatus(status) {
+ const kumaStatus = document.getElementById('kuma-connection-status');
+ const kumaEndpoint = document.getElementById('kuma-endpoint');
+
+ if (kumaStatus) {
+ switch (status) {
+ case 'connected':
+ kumaStatus.textContent = 'Connected';
+ kumaStatus.className = 'tag tag-green';
+ break;
+ case 'static':
+ kumaStatus.textContent = 'Using Static Data';
+ kumaStatus.className = 'tag tag-blue';
+ break;
+ case 'error':
+ kumaStatus.textContent = 'Connection Failed';
+ kumaStatus.className = 'tag tag-red';
+ break;
+ default:
+ kumaStatus.textContent = 'Not Configured';
+ kumaStatus.className = 'tag tag-gray';
+ }
+ }
+
+ if (kumaEndpoint) {
+ kumaEndpoint.textContent = uptimeKumaConfig.apiUrl || 'Not configured';
+ }
+ }
+
+ // Update Kuma status display
+ function updateKumaStatusDisplay() {
+ const kumaStatusEl = document.getElementById('uptime-kuma-status');
+ if (!kumaStatusEl) return;
+
+ if (uptimeKumaConfig.enabled) {
+ kumaStatusEl.classList.remove('border-dashed');
+ kumaStatusEl.classList.add('border-solid', 'border-blue-300', 'dark:border-blue-600');
+ kumaStatusEl.classList.remove('bg-gray-100', 'dark:bg-gray-700');
+ kumaStatusEl.classList.add('bg-blue-50', 'dark:bg-blue-900');
+ }
+ }
+
+ // Manual refresh
+ function refreshStatus() {
+ const button = document.getElementById('refresh-status');
+ if (button) {
+ button.disabled = true;
+ button.textContent = 'Refreshing...';
+ }
+
+ loadServiceStatus().finally(() => {
+ if (button) {
+ button.disabled = false;
+ button.textContent = 'Refresh Status';
+ }
+ });
+ }
+
+ // Update last updated timestamp
+ function updateLastUpdated() {
+ const element = document.getElementById('last-updated');
+ if (element) {
+ element.textContent = new Date().toLocaleString();
+ }
+ }
+
+ // Utility functions
+ function getStatusClass(status) {
+ switch (status) {
+ case 'operational': return 'operational';
+ case 'degraded': return 'degraded';
+ case 'maintenance': return 'maintenance';
+ case 'down': return 'down';
+ default: return 'down';
+ }
+ }
+
+ function getStatusTextClass(status) {
+ switch (status) {
+ case 'operational': return 'text-green-600 dark:text-green-400 font-medium';
+ case 'degraded': return 'text-yellow-600 dark:text-yellow-400 font-medium';
+ case 'maintenance': return 'text-blue-600 dark:text-blue-400 font-medium';
+ case 'down': return 'text-red-600 dark:text-red-400 font-medium';
+ default: return 'text-gray-600 dark:text-gray-400 font-medium';
+ }
+ }
+
+ function formatRelativeTime(dateString) {
+ if (!dateString) return 'Unknown';
+
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffMs = now - date;
+ const diffMins = Math.floor(diffMs / 60000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `${diffHours}h ago`;
+
+ const diffDays = Math.floor(diffHours / 24);
+ return `${diffDays}d ago`;
+ }
+
+ // Cleanup on page unload
+ window.addEventListener('beforeunload', () => {
+ if (refreshInterval) {
+ clearInterval(refreshInterval);
+ }
+ });
+
+ // Initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
\ No newline at end of file
diff --git a/src/js/theme.js b/src/js/theme.js
index e69de29..c2ccdf8 100644
--- a/src/js/theme.js
+++ b/src/js/theme.js
@@ -0,0 +1,140 @@
+// Theme management functionality
+(function() {
+ 'use strict';
+
+ let currentTheme = 'auto';
+ let mediaQuery;
+
+ // Initialize theme system
+ function init() {
+ // Get stored theme or default to auto
+ currentTheme = localStorage.getItem('theme') || 'auto';
+
+ // Set up media query listener for auto mode
+ mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ mediaQuery.addEventListener('change', handleSystemThemeChange);
+
+ // Apply initial theme
+ applyTheme(currentTheme);
+
+ // Set up theme selector buttons
+ setupThemeSelector();
+ }
+
+ // Set up theme selector button event handlers
+ function setupThemeSelector() {
+ const themeButtons = document.querySelectorAll('.theme-btn');
+
+ themeButtons.forEach(button => {
+ const theme = button.getAttribute('data-theme');
+
+ // Set initial active state
+ button.classList.toggle('active', theme === currentTheme);
+
+ // Add click handler
+ button.addEventListener('click', () => {
+ setTheme(theme);
+ });
+ });
+ }
+
+ // Set theme and update UI
+ function setTheme(theme) {
+ if (!['light', 'dark', 'auto'].includes(theme)) {
+ console.warn('Invalid theme:', theme);
+ return;
+ }
+
+ currentTheme = theme;
+
+ // Store in localStorage
+ localStorage.setItem('theme', theme);
+
+ // Apply theme
+ applyTheme(theme);
+
+ // Update button states
+ updateThemeButtons();
+ }
+
+ // Apply theme to document
+ function applyTheme(theme) {
+ const html = document.documentElement;
+
+ // Update theme class
+ html.className = html.className.replace(/theme-\w+/g, '');
+ html.classList.add(`theme-${theme}`);
+
+ // Handle dark mode class
+ if (theme === 'auto') {
+ // Use system preference
+ const isDark = mediaQuery ? mediaQuery.matches :
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
+ html.classList.toggle('dark', isDark);
+ } else {
+ // Use explicit theme
+ html.classList.toggle('dark', theme === 'dark');
+ }
+ }
+
+ // Handle system theme changes (for auto mode)
+ function handleSystemThemeChange(event) {
+ if (currentTheme === 'auto') {
+ document.documentElement.classList.toggle('dark', event.matches);
+ }
+ }
+
+ // Update theme button active states
+ function updateThemeButtons() {
+ const themeButtons = document.querySelectorAll('.theme-btn');
+
+ themeButtons.forEach(button => {
+ const theme = button.getAttribute('data-theme');
+ button.classList.toggle('active', theme === currentTheme);
+ });
+ }
+
+ // Get current effective theme (resolves 'auto' to actual theme)
+ function getEffectiveTheme() {
+ if (currentTheme === 'auto') {
+ return mediaQuery && mediaQuery.matches ? 'dark' : 'light';
+ }
+ return currentTheme;
+ }
+
+ // Toggle between light and dark (skips auto)
+ function toggleTheme() {
+ const effectiveTheme = getEffectiveTheme();
+ setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
+ }
+
+ // Keyboard shortcut support
+ function setupKeyboardShortcuts() {
+ document.addEventListener('keydown', (event) => {
+ // Ctrl/Cmd + Shift + T to toggle theme
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'T') {
+ event.preventDefault();
+ toggleTheme();
+ }
+ });
+ }
+
+ // Export functions for external use
+ window.themeManager = {
+ setTheme,
+ getTheme: () => currentTheme,
+ getEffectiveTheme,
+ toggleTheme
+ };
+
+ // Initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ init();
+ setupKeyboardShortcuts();
+ });
+ } else {
+ init();
+ setupKeyboardShortcuts();
+ }
+})();
\ No newline at end of file
diff --git a/src/privacy/index.njk b/src/privacy/index.njk
index e69de29..2f6ce2e 100644
--- a/src/privacy/index.njk
+++ b/src/privacy/index.njk
@@ -0,0 +1,155 @@
+---
+layout: base.njk
+title: "Privacy Policy"
+description: "Privacy policy and data handling practices for the DFIR Tools Hub"
+---
+
+
+
+
+
Privatsphäre und Datenschutz
+
+ Ich stehe für Privatsphäre und Datenschutz und gehe damit transparent um.
+
+
+ Letztes Update: 13.07.2025
+
+
+
+
+
+
+
Datensammlung
+
+ Auf dieser Webseite werden nur minimale technisch nicht vermeidbare Dsten gesammelt. Diese Plattform agiert nach dem Prinzip privacy-first und existiert als statische Website ohne Datenaggregation durch den Server.
+
+
+
Daten, die unweigerlich gespeichert werden:
+
+
+ •
+ Lokaler Browserspeicher: Präferenzen zum ausgewählten Farbschema (hell/dunkel) werden in Ihrem Browser gespeichert.
+
+
+ •
+ Server Logs: Es werden standardisierte Accesslogs von NGINX undd HAProxy infrastrukturbedingt aufgezeichnet (IP-Adresse, Zeitstempel, HTTP-Endpunkt). Diese dienen ausschließlich der operativen Sicherheit und werden auf gerichtlichen Beschluss lediglich autorisierten Behörden ausgehändigt.
+
+ ✓
+ Nutzeraccounts oder Authentifizierungsdaten
+
+
+ ✓
+ Jegliche Art von Tracking-Cookies oder sonstige Mechanismen zum Fingerprinting
+
+
+ ✓
+ Verhaltensanalytische oder biometrische Daten
+
+
+ ✓
+ Keine Interaktion mit kommerziellen Netzwerken, außer dem ISP zur Bereitstellung der Seite
+
+
+
+
+
+
+
+
Cookies und lokaler Speicher Ihres Browsers
+
+ Der Browserspeicher wird minimalistisch und nur zu o.g. Zwecken genutzt. Wir speichern keine Cookies.
+
+
+
+
+
+
+
Storage Type
+
Purpose
+
Retention
+
+
+
+
+
Theme Preference
+
Remember your dark/light mode choice
+
Until manually cleared
+
+
+
Search Filters
+
Maintain search state during session
+
Session only
+
+
+
+
+
+
+
+
Drittparteien und Dienste
+
+
+
+
Externe Verlinkungen
+
+ Links to external tools (GitHub repositories, project websites) are provided for convenience.
+ These external sites have their own privacy policies and data handling practices.
+
+
+
+
+
Bereitgestellte Dienste
+
+ The "Self-Hosted Services" section links to services deployed within your local network.
+ Data handling for these services is governed by your institution's policies and the
+ specific configuration of each service.
+ Die als "on-premise" getaggten oder sonst offensichtlich gemachten Dienste werden innerhalb des CC24-Clusters (Teil des Mikoshi Compute Clusters) gehostet.
+ Jegliche Dateneingabe oder -verarbeitung unterliegt der Verantwortung des Nutzers, der die Daten erstmalig einbringt. Werden Vorschriften bestimmter Organisationen, die mit Ihnen in Verbindung stehen, berührt, sind Sie allein dafür verantwortlich, was Sie hochladen und verarbeiten.
+ Daten, deren Besitz strafbar ist, werden nicht toleriert.
+
+
+
+
+
+
+
Data Security
+
+ Ich implementiere zeitgemäße technische Maßnahmen, um Ihre Daten zu schützen:
+
+
+
+
+ •
+ Transport Security: Alle Verbindungen werden TLS-verschlüsselt.
+
+
+ •
+ Statische Architektur: Sofern es die Applikation nicht offensichtlich erfordert, werden serverseitig keine Daten gespeichert.
+
+
+ •
+ Lokale Verarbeitung: Suche und Filterung dieser Seite werden lokal auf Ihrem Gerät durchgeführt (Browser-Funktion)
+
+
+ •
+ Reguläre Updates: Abhängigkeiten und Sicherheitspatches werden regelmäßig geprüft und integriert.
+