add knowledgebase

This commit is contained in:
overcuriousity 2025-07-15 22:47:56 +02:00
parent d18cc060e5
commit a057120d7a
10 changed files with 514 additions and 222 deletions

View File

@ -1,124 +0,0 @@
#!/bin/bash
# CC24-Hub - Node.js Deployment Script
set -e
PROJECT_DIR="/var/www/dfir-tools-hub"
SERVICE_NAME="dfir-tools-hub"
SERVICE_PORT="3000"
echo "🔧 Setting up Node.js deployment..."
cd $PROJECT_DIR
# Install PM2 globally if not present
if ! command -v pm2 &> /dev/null; then
sudo npm install -g pm2
fi
# Install dependencies
npm ci --production
# Update astro.config.mjs for Node.js adapter
cat > astro.config.mjs << 'EOF'
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'server',
adapter: '@astrojs/node',
server: {
port: 3000,
host: '127.0.0.1'
}
});
EOF
# Install Node.js adapter
npm install @astrojs/node
# Build for Node.js
npm run build
# Create PM2 ecosystem file
cat > ecosystem.config.js << EOF
module.exports = {
apps: [{
name: '$SERVICE_NAME',
script: './dist/server/entry.mjs',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: $SERVICE_PORT,
HOST: '127.0.0.1'
},
error_file: '/var/log/pm2/$SERVICE_NAME-error.log',
out_file: '/var/log/pm2/$SERVICE_NAME-out.log',
log_file: '/var/log/pm2/$SERVICE_NAME.log',
time: true,
max_memory_restart: '1G'
}]
};
EOF
# Create log directory
sudo mkdir -p /var/log/pm2
sudo chown -R $(whoami):$(whoami) /var/log/pm2
# Start/restart with PM2
pm2 delete $SERVICE_NAME 2>/dev/null || true
pm2 start ecosystem.config.js
pm2 save
pm2 startup
echo "🔧 Configuring nginx reverse proxy..."
# Create nginx configuration for Node.js
sudo tee /etc/nginx/sites-available/$SERVICE_NAME << EOF
upstream dfir_backend {
server 127.0.0.1:$SERVICE_PORT;
}
server {
listen 80;
server_name dfir-tools.yourdomain.com; # Replace with your domain
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Proxy to Node.js application
location / {
proxy_pass http://dfir_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://dfir_backend/health;
}
}
EOF
# Enable site and reload nginx
sudo ln -sf /etc/nginx/sites-available/$SERVICE_NAME /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
echo "✅ Node.js deployment completed!"
echo "🔍 Status: pm2 status"
echo "📋 Logs: pm2 logs $SERVICE_NAME"

View File

@ -1,47 +0,0 @@
Proposed Framework Categories (Y-axis):
1. Storage & File System Artifacts
Static file system Analyse (encrypted/unencrypted)
Registry Analyse
Database forensics
2. Memory & Runtime Artifacts
Memory forensics of live systems
Process Analyse
Virtualization forensics
3. Network & Communication Artifacts
Webserver log Analyse
System log Analyse
PKI Auswertung
Radio signal Analyse
VoIP forensics
Network packet Analyse (PCAP)
4. Application & Code Artifacts
Malware Analyse
Darknet website source code Analyse
Browser forensics
Email forensics
5. Multimedia & Content Artifacts
Video/image/audio authenticity Analyse
Steganography detection
Content recovery
6. Transaction & Financial Artifacts
Blockchain payment Analyse
Cryptocurrency exchange Analyse
Financial transaction forensics
7. Platform & Infrastructure Artifacts
Mobile forensics
Cloud forensics
IoT device forensics
Social media/OSINT Analyse

View File

@ -18,6 +18,11 @@ const currentPath = Astro.url.pathname;
~/
</a>
</li>
<li>
<a href="/knowledgebase" class={`nav-link ${currentPath === '/knowledgebase' ? 'active' : ''}`}>
~/knowledgebase
</a>
</li>
<li>
<a href="/status" class={`nav-link ${currentPath === '/status' ? 'active' : ''}`}>
~/status

View File

@ -13,6 +13,7 @@ export interface Props {
license: string;
tags: string[];
statusUrl?: string;
knowledgebase?: boolean;
};
}
@ -24,6 +25,9 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
// Check if tool has knowledgebase entry
const hasKnowledgebase = tool.knowledgebase === true;
// Determine card styling based on hosting status (derived from projectUrl)
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
---
@ -31,9 +35,10 @@ const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'P
<div class={cardClass} onclick={`window.showToolDetails('${tool.name}')`} style="cursor: pointer;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<h3 style="margin: 0;">{tool.name}</h3>
<div style="display: flex; gap: 0.5rem;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
{hasValidProjectUrl && <span class="badge badge-primary">Self-Hosted</span>}
{tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
{hasKnowledgebase && <span class="badge badge-error">Infos 📖</span>}
</div>
</div>

View File

@ -266,22 +266,27 @@ const sortedTags = Object.entries(tagFrequency)
const includeProprietary = proprietaryCheckbox.checked;
const filtered = window.toolsData.filter(tool => {
// Ensure arrays exist with fallbacks
const domains = tool.domains || [];
const phases = tool.phases || [];
const tags = tool.tags || [];
// Search filter
if (searchTerm && !(
tool.name.toLowerCase().includes(searchTerm) ||
tool.description.toLowerCase().includes(searchTerm) ||
tool.tags.some(tag => tag.toLowerCase().includes(searchTerm))
tags.some(tag => tag.toLowerCase().includes(searchTerm))
)) {
return false;
}
// Domain filter
if (selectedDomain && !tool.domains.includes(selectedDomain)) {
if (selectedDomain && !domains.includes(selectedDomain)) {
return false;
}
// Phase filter
if (selectedPhase && !tool.phases.includes(selectedPhase)) {
if (selectedPhase && !phases.includes(selectedPhase)) {
return false;
}
@ -291,7 +296,7 @@ const sortedTags = Object.entries(tagFrequency)
}
// Tag filter
if (selectedTags.size > 0 && !Array.from(selectedTags).some(tag => tool.tags.includes(tag))) {
if (selectedTags.size > 0 && !Array.from(selectedTags).some(tag => tags.includes(tag))) {
return false;
}

View File

@ -14,11 +14,11 @@ const tools = data.tools;
// Separate collaboration tools from domain-specific tools
const collaborationTools = tools.filter((tool: any) =>
tool.phases.includes('collaboration')
tool.phases && tool.phases.includes('collaboration')
);
const domainTools = tools.filter((tool: any) =>
!tool.phases.includes('collaboration')
!tool.phases || !tool.phases.includes('collaboration')
);
// Create matrix structure for domain-specific tools only
@ -27,7 +27,7 @@ domains.forEach((domain: any) => {
matrix[domain.id] = {};
phases.filter((phase: any) => phase.id !== 'collaboration').forEach((phase: any) => {
matrix[domain.id][phase.id] = domainTools.filter((tool: any) =>
tool.domains.includes(domain.id) && tool.phases.includes(phase.id)
tool.domains && tool.domains.includes(domain.id) && tool.phases && tool.phases.includes(phase.id)
);
});
});
@ -51,6 +51,7 @@ domains.forEach((domain: any) => {
<div style="display: flex; gap: 0.25rem;">
{hasValidProjectUrl && <span class="badge-mini badge-primary">Self-Hosted</span>}
{tool.license !== 'Proprietary' && <span class="badge-mini badge-success">OSS</span>}
{tool.knowledgebase === true && <span class="badge-mini badge-error">Infos 📖</span>}
</div>
</div>
<p style="font-size: 0.75rem; color: var(--color-text-secondary); margin: 0.25rem 0; line-height: 1.3;">
@ -95,8 +96,10 @@ domains.forEach((domain: any) => {
class={`tool-chip ${hasValidProjectUrl ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`}
data-tool-name={tool.name}
onclick={`window.showToolDetails('${tool.name}')`}
title={`${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`}
>
{tool.name}
{tool.knowledgebase === true && <span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>}
</span>
);
})}
@ -131,7 +134,7 @@ domains.forEach((domain: any) => {
<div id="tool-tags" style="margin-bottom: 1rem;"></div>
<!-- Updated button container for dual buttons -->
<div id="tool-links" style="display: flex; gap: 0.5rem;"></div>
<div id="tool-links" style="display: flex; gap: 0.5rem; flex-direction: column;"></div>
</div>
<script define:vars={{ toolsData: tools, collaborationTools, domainTools }}>
@ -205,13 +208,14 @@ domains.forEach((domain: any) => {
<div style="display: flex; gap: 0.25rem;">
${hasValidProjectUrl ? '<span class="badge-mini badge-primary">Self-Hosted</span>' : ''}
${tool.license !== 'Proprietary' ? '<span class="badge-mini badge-success">OSS</span>' : ''}
${tool.knowledgebase === true ? '<span class="badge-mini badge-error">Infos 📖</span>' : ''}
</div>
</div>
<p style="font-size: 0.75rem; color: var(--color-text-secondary); margin: 0.25rem 0; line-height: 1.3;">
${tool.description}
</p>
<div style="display: flex; gap: 0.75rem; font-size: 0.6875rem; color: var(--color-text-secondary);">
<span>${tool.platforms.join(', ')}</span>
<span>${(tool.platforms || []).join(', ')}</span>
<span>•</span>
<span>${tool.skillLevel}</span>
</div>
@ -243,14 +247,19 @@ domains.forEach((domain: any) => {
if (tool.license !== 'Proprietary') {
badgesContainer.innerHTML += '<span class="badge badge-success">Open Source</span>';
}
if (tool.knowledgebase === true) {
badgesContainer.innerHTML += '<span class="badge badge-error">Infos 📖</span>';
}
// Metadata
// Metadata - safe array handling
const metadataContainer = document.getElementById('tool-metadata');
const domainsText = tool.domains.length > 0 ? tool.domains.join(', ') : 'Domain-agnostic';
const phasesText = tool.phases.join(', ');
const domains = tool.domains || [];
const phases = tool.phases || [];
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
const phasesText = phases.join(', ');
metadataContainer.innerHTML = `
<div style="display: grid; gap: 0.5rem;">
<div><strong>Betriebssystem:</strong> ${tool.platforms.join(', ')}</div>
<div><strong>Betriebssystem:</strong> ${(tool.platforms || []).join(', ')}</div>
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
<div><strong>Deployment:</strong> ${tool.accessType}</div>
@ -259,36 +268,61 @@ domains.forEach((domain: any) => {
</div>
`;
// Tags
// Tags - safe array handling
const tagsContainer = document.getElementById('tool-tags');
const tags = tool.tags || [];
tagsContainer.innerHTML = `
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
${tool.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
${tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
`;
// Links - Updated to handle dual buttons for hosted tools
// Links - Updated to handle dual buttons for hosted tools AND knowledgebase links
const linksContainer = document.getElementById('tool-links');
let linksHTML = '';
// Main action buttons
if (hasValidProjectUrl) {
// Two buttons for tools we're hosting
linksContainer.innerHTML = `
linksHTML += `
<div style="display: flex; gap: 0.5rem;">
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary" style="flex: 1;">
Software-Homepage
</a>
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 1;">
Zugreifen
</a>
</div>
`;
} else {
// Single button for tools we're not hosting
linksContainer.innerHTML = `
linksHTML += `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%;">
Software-Homepage
</a>
`;
}
// Add knowledgebase link if available
if (tool.knowledgebase === true) {
const kbId = tool.name.toLowerCase().replace(/\s+/g, '-');
linksHTML += `
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary" style="width: 100%; margin-top: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="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>
Knowledgebase anzeigen
</a>
`;
}
linksContainer.innerHTML = linksHTML;
// Show modal
document.getElementById('modal-overlay').classList.add('active');
document.getElementById('tool-details').classList.add('active');
@ -336,7 +370,7 @@ domains.forEach((domain: any) => {
dfirMatrixSection.style.display = 'none';
// Filter collaboration tools
const filteredCollaboration = filtered.filter(tool => tool.phases.includes('collaboration'));
const filteredCollaboration = filtered.filter(tool => (tool.phases || []).includes('collaboration'));
collaborationContainer.innerHTML = '';
filteredCollaboration.forEach(tool => {
@ -352,7 +386,7 @@ domains.forEach((domain: any) => {
collaborationSection.style.display = 'block';
// Show all collaboration tools that pass general filters
const filteredCollaboration = filtered.filter(tool => tool.phases.includes('collaboration'));
const filteredCollaboration = filtered.filter(tool => (tool.phases || []).includes('collaboration'));
collaborationContainer.innerHTML = '';
filteredCollaboration.forEach(tool => {
@ -369,22 +403,26 @@ domains.forEach((domain: any) => {
cell.innerHTML = '';
});
// Re-populate with filtered DFIR tools
const filteredDfirTools = filtered.filter(tool => !tool.phases.includes('collaboration'));
// Re-populate with filtered DFIR tools - with safe array handling
const filteredDfirTools = filtered.filter(tool => !(tool.phases || []).includes('collaboration'));
filteredDfirTools.forEach(tool => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
tool.domains.forEach(domain => {
tool.phases.forEach(phase => {
const domains = tool.domains || [];
const phases = tool.phases || [];
domains.forEach(domain => {
phases.forEach(phase => {
if (phase !== 'collaboration') {
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
if (cell) {
const chip = document.createElement('span');
chip.className = `tool-chip ${hasValidProjectUrl ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`;
chip.textContent = 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);
cell.appendChild(chip);
}

View File

@ -2,9 +2,11 @@ tools:
# Disk & File System Analysis Tools
- name: Autopsy
description: >-
Open-Source digitale Forensik-Plattform mit grafischer Benutzeroberfläche
Open-Source digitale Forensik-Anwendung mit grafischer Benutzeroberfläche
für Festplatten- und Dateisystemanalyse. Besonders geeignet für die
Analyse-Phase mit umfangreichen Carving- und Timeline-Funktionen.
Auswertungs- und Analyse-Phase mit umfangreichen Carving- und Timeline-Funktionen.
DIE Alternative für kommerzielle Software im Bereich, wenn es um die
kriminalistische Untersuchung von Images geht.
domains:
- incident-response
- law-enforcement
@ -13,15 +15,16 @@ tools:
- data-collection
- examination
- analysis
- reporting
platforms:
- Windows
- Linux
- macOS
- Linux (Snap-Paket)
skillLevel: intermediate
accessType: download
url: https://www.autopsy.com/
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- disk-imaging
- file-carving
@ -56,6 +59,7 @@ tools:
url: https://www.volatilityfoundation.org/
projectUrl: ''
license: VSL
knowledgebase:
tags:
- memory-analysis
- malware-detection
@ -74,6 +78,7 @@ tools:
Kollaborative Security-Incident-Response-Plattform für SOCs, CERTs und
Sicherheitsteams mit Case-Management. Ideal für alle Phasen einer
Untersuchung, besonders für Koordination und Berichterstattung.
Keine Erfahrungswerte.
domains:
- incident-response
- law-enforcement
@ -83,7 +88,6 @@ tools:
- examination
- analysis
- reporting
- collaboration
platforms:
- Web
skillLevel: intermediate
@ -91,6 +95,7 @@ tools:
url: https://strangebee.com/
projectUrl: ''
license: Community Edition (Free) / Commercial
knowledgebase:
tags:
- case-management
- team-collaboration
@ -117,7 +122,6 @@ tools:
- data-collection
- examination
- analysis
- collaboration
platforms:
- Web
skillLevel: intermediate
@ -125,6 +129,7 @@ tools:
url: https://misp-project.org/
projectUrl: https://misp.cc24.dev
license: AGPL-3.0
knowledgebase:
tags:
- threat-intelligence
- ioc-sharing
@ -156,6 +161,7 @@ tools:
url: https://timesketch.org/
projectUrl: https://timesketch.cc24.dev
license: Apache 2.0
knowledgebase:
tags:
- timeline-analysis
- data-visualization
@ -190,6 +196,7 @@ tools:
url: https://www.wireshark.org/
projectUrl: ''
license: GPL-2.0
knowledgebase:
tags:
- packet-capture
- protocol-analysis
@ -224,6 +231,7 @@ tools:
url: https://www.magnetforensics.com/products/magnet-axiom/
projectUrl: ''
license: Proprietary
knowledgebase:
tags:
- mobile-forensics
- cloud-acquisition
@ -255,6 +263,7 @@ tools:
url: https://cellebrite.com/en/ufed/
projectUrl: ''
license: Proprietary
knowledgebase:
tags:
- mobile-extraction
- physical-extraction
@ -283,6 +292,7 @@ tools:
url: https://github.com/cert-ee/cuckoo3
projectUrl: ''
license: GPL-3.0
knowledgebase:
tags:
- dynamic-analysis
- behavior-monitoring
@ -314,6 +324,7 @@ tools:
url: https://ghidra-sre.org/
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- reverse-engineering
- disassembly
@ -346,6 +357,7 @@ tools:
url: https://plaso.readthedocs.io/
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- timeline-generation
- log-parsing
@ -376,6 +388,7 @@ tools:
url: https://gchq.github.io/CyberChef/
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- data-transformation
- encoding-decoding
@ -410,6 +423,7 @@ tools:
url: https://www.velociraptor.app/
projectUrl: https://raptor.cc24.dev
license: Apache 2.0
knowledgebase:
tags:
- remote-collection
- live-forensics
@ -442,6 +456,7 @@ tools:
url: https://github.com/google/grr
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- remote-forensics
- scalable-collection
@ -473,6 +488,7 @@ tools:
url: https://arkime.com/
projectUrl: ''
license: Apache 2.0
knowledgebase:
tags:
- full-packet-capture
- pcap-indexing
@ -503,6 +519,7 @@ tools:
url: https://www.netresec.com/?page=NetworkMiner
projectUrl: ''
license: GPL-2.0 / Commercial
knowledgebase:
tags:
- pcap-analysis
- file-extraction
@ -533,6 +550,7 @@ tools:
https://www.kroll.com/en/services/cyber-risk/incident-response-litigation-support/kroll-artifact-parser-extractor-kape
projectUrl: ''
license: Freeware
knowledgebase:
tags:
- triage-collection
- artifact-parsing
@ -565,6 +583,7 @@ tools:
url: https://exiftool.org/
projectUrl: ''
license: Perl Artistic License
knowledgebase:
tags:
- metadata-extraction
- exif-analysis
@ -594,6 +613,7 @@ tools:
url: https://www.chainalysis.com/
projectUrl: ''
license: Proprietary
knowledgebase:
tags:
- blockchain-analysis
- crypto-tracing
@ -627,6 +647,7 @@ tools:
url: https://neo4j.com/
projectUrl: https://graph.cc24.dev
license: GPL-3.0 / Commercial
knowledgebase:
tags:
- graph-database
- relationship-analysis
@ -658,6 +679,7 @@ tools:
url: https://qgis.org/
projectUrl: ''
license: GPL-2.0
knowledgebase:
tags:
- geospatial-analysis
- gps-visualization
@ -675,12 +697,8 @@ tools:
kollaborative Phasen und sichere Speicherung von Beweismitteln
mit Versionierung.
domains:
- incident-response
- law-enforcement
- fraud-investigation
phases:
- collaboration
- reporting
- collaboration-general
platforms:
- Web
skillLevel: novice
@ -688,6 +706,7 @@ tools:
url: https://nextcloud.com/
projectUrl: https://cloud.cc24.dev
license: AGPL-3.0
knowledgebase:
tags:
- file-sharing
- collaboration
@ -708,8 +727,7 @@ tools:
- incident-response
- malware-analysis
phases:
- collaboration
- reporting
- collaboration-general
platforms:
- Web
skillLevel: beginner
@ -717,6 +735,7 @@ tools:
url: https://gitea.io/
projectUrl: https://git.cc24.dev
license: MIT
knowledgebase:
tags:
- version-control
- code-repository
@ -749,6 +768,7 @@ tools:
url: https://github.com/ReFirmLabs/binwalk
projectUrl: ''
license: MIT
knowledgebase:
tags:
- firmware-analysis
- file-carving
@ -786,5 +806,5 @@ phases:
name: Analyse
- id: reporting
name: Bericht & Präsentation
- id: collaboration
- id: collaboration-general
name: Übergreifend & Kollaboration

View File

@ -138,13 +138,14 @@ const tools = data.tools;
}
});
// Create tool card element
function createToolCard(tool) {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const hasKnowledgebase = tool.knowledgebase === true;
const cardDiv = document.createElement('div');
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
cardDiv.className = cardClass;
@ -177,9 +178,10 @@ function createToolCard(tool) {
cardDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<h3 style="margin: 0;">${tool.name}</h3>
<div style="display: flex; gap: 0.5rem;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
${hasKnowledgebase ? '<span class="badge badge-error">Infos 📖</span>' : ''}
</div>
</div>
@ -195,7 +197,7 @@ function createToolCard(tool) {
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
<span class="text-muted" style="font-size: 0.75rem;">
${tool.platforms.join(', ')}
${(tool.platforms || []).join(', ')}
</span>
</div>
@ -221,7 +223,7 @@ function createToolCard(tool) {
</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1rem;">
${tool.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
${(tool.tags || []).map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
${buttonHTML}

View File

@ -0,0 +1,277 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { promises as fs } from 'fs';
import { load } from 'js-yaml';
import path from 'path';
// Load tools data
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
const yamlContent = await fs.readFile(yamlPath, 'utf8');
const data = load(yamlContent) as any;
// Filter tools that have knowledgebase entries
const knowledgebaseTools = data.tools.filter((tool: any) => tool.knowledgebase === true);
// Sort alphabetically by name
knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
---
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
<section style="padding: 2rem 0;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 2.5rem; color: var(--color-primary);">Knowledgebase</h1>
<p style="font-size: 1.25rem; color: var(--color-text-secondary); margin-bottom: 0.5rem;">
Erweiterte Dokumentation und Erkenntnisse
</p>
<p style="font-size: 1rem; color: var(--color-text-secondary);">
Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
</p>
</div>
<!-- Search and Filter -->
<div style="margin-bottom: 2rem;">
<input
type="text"
id="kb-search"
placeholder="Knowledgebase durchsuchen..."
style="max-width: 500px; margin: 0 auto; display: block;"
/>
</div>
<!-- Tools Count -->
<div style="text-align: center; margin-bottom: 2rem;">
<p class="text-muted" style="font-size: 0.875rem;">
<span id="visible-count">{knowledgebaseTools.length}</span> von {knowledgebaseTools.length} Einträgen
</p>
</div>
<!-- Knowledgebase Entries -->
<div style="max-width: 1000px; margin: 0 auto;">
{knowledgebaseTools.length === 0 ? (
<div class="card" style="text-align: center; padding: 3rem;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin: 0 auto 1rem;">
<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>
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
<p class="text-muted">
Knowledgebase-Einträge werden automatisch angezeigt, sobald Tools das Attribut "knowledgebase: true" haben.
</p>
</div>
) : (
<div id="kb-entries">
{knowledgebaseTools.map((tool: any, index: number) => (
<article class="kb-entry card kb-entry-collapsed" id={`kb-${tool.name.toLowerCase().replace(/\s+/g, '-')}`} data-tool-name={tool.name.toLowerCase()}>
<!-- Collapsed Header (default state) -->
<div class="kb-entry-header" onclick={`toggleKbEntry('${tool.name.toLowerCase().replace(/\s+/g, '-')}')`} style="cursor: pointer;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 1rem;">
<h3 style="margin: 0; color: var(--color-primary);">{tool.name}</h3>
<div style="display: flex; gap: 0.5rem;">
{(() => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
return (
<>
{hasValidProjectUrl && <span class="badge badge-primary">Self-Hosted</span>}
{tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
<span class="badge badge-error">Infos 📖</span>
</>
);
})()}
</div>
</div>
<div class="kb-expand-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
<p class="text-muted" style="font-size: 0.875rem; margin: 0.5rem 0 0 0;">
{tool.description}
</p>
</div>
<!-- Expanded Content (hidden by default) -->
<div class="kb-entry-content" style="display: none;">
<!-- Quick Info -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 1.5rem 0; padding: 1rem; background-color: var(--color-bg-secondary); border-radius: 0.5rem;">
<div>
<strong style="font-size: 0.75rem; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Plattformen</strong>
<p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">{tool.platforms.join(', ')}</p>
</div>
<div>
<strong style="font-size: 0.75rem; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Skill Level</strong>
<p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">{tool.skillLevel}</p>
</div>
<div>
<strong style="font-size: 0.75rem; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Lizenz</strong>
<p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">{tool.license}</p>
</div>
<div>
<strong style="font-size: 0.75rem; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Phasen</strong>
<p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">{tool.phases.join(', ')}</p>
</div>
</div>
<!-- Knowledge Content Area -->
<div class="kb-content">
<h4 style="margin-bottom: 1rem; color: var(--color-text); font-size: 1.125rem;">Praktische Erkenntnisse</h4>
{tool.knowledgebaseContent ? (
<div style="line-height: 1.7;">
<p>{tool.knowledgebaseContent}</p>
</div>
) : (
<div style="padding: 2rem; text-align: center; background-color: var(--color-bg-secondary); border-radius: 0.5rem; border: 2px dashed var(--color-border);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin-bottom: 0.75rem;">
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
<path d="M12 17h.01"/>
</svg>
<p class="text-muted" style="margin: 0; font-style: italic;">
Inhalt wird manuell hinzugefügt
</p>
</div>
)}
</div>
<!-- Action Links -->
<div style="display: flex; gap: 1rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
{(() => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
return (
<>
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Software-Homepage
</a>
{hasValidProjectUrl && (
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
</svg>
Zugreifen
</a>
)}
</>
);
})()}
</div>
</div>
</article>
))}
</div>
)}
</div>
<!-- No Results Message -->
<div id="no-kb-results" style="display: none; text-align: center; padding: 4rem 0;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin-bottom: 1rem;">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Keine Ergebnisse gefunden</h3>
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
</div>
</section>
</BaseLayout>
<script>
// Toggle knowledgebase entry expansion
function toggleKbEntry(entryId: string) {
const entry = document.getElementById(`kb-${entryId}`);
if (!entry) return;
const content = entry.querySelector('.kb-entry-content') as HTMLElement;
const icon = entry.querySelector('.kb-expand-icon svg') as HTMLElement;
if (!content || !icon) return;
const isExpanded = entry.classList.contains('kb-entry-expanded');
if (isExpanded) {
// Collapse
entry.classList.remove('kb-entry-expanded');
entry.classList.add('kb-entry-collapsed');
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
} else {
// Expand
entry.classList.remove('kb-entry-collapsed');
entry.classList.add('kb-entry-expanded');
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
}
}
// Make toggle function globally available
(window as any).toggleKbEntry = toggleKbEntry;
// Search functionality
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('kb-search') as HTMLInputElement;
const entries = document.querySelectorAll('.kb-entry');
const visibleCount = document.getElementById('visible-count');
const noResults = document.getElementById('no-kb-results');
const entriesContainer = document.getElementById('kb-entries');
if (!searchInput || !entries.length) return;
// Check if page loaded with hash for specific entry
if (window.location.hash && window.location.hash.startsWith('#kb-')) {
const targetId = window.location.hash.replace('#kb-', '');
setTimeout(() => toggleKbEntry(targetId), 100);
}
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase().trim();
let visibleEntries = 0;
entries.forEach(entry => {
const toolName = entry.getAttribute('data-tool-name') || '';
const entryText = entry.textContent?.toLowerCase() || '';
if (searchTerm === '' ||
toolName.includes(searchTerm) ||
entryText.includes(searchTerm)) {
(entry as HTMLElement).style.display = 'block';
visibleEntries++;
} else {
(entry as HTMLElement).style.display = 'none';
}
});
// Update count
if (visibleCount) {
visibleCount.textContent = visibleEntries.toString();
}
// Show/hide no results message
if (noResults && entriesContainer) {
if (visibleEntries === 0 && searchTerm !== '') {
(noResults as HTMLElement).style.display = 'block';
(entriesContainer as HTMLElement).style.display = 'none';
} else {
(noResults as HTMLElement).style.display = 'none';
(entriesContainer as HTMLElement).style.display = 'block';
}
}
});
});
</script>

View File

@ -389,6 +389,54 @@ input[type="checkbox"] {
color: white;
}
/* Knowledgebase entries styling */
.kb-entry {
margin-bottom: 1rem;
position: relative;
transition: all 0.3s ease;
}
.kb-entry:target {
animation: highlight-flash 2s ease-out;
}
.kb-entry-collapsed {
/* Collapsed state - compact view with minimal spacing */
margin-bottom: 1rem;
}
.kb-entry-expanded {
margin-bottom: 2rem;
}
.kb-entry-header {
padding: 1rem 0;
transition: all 0.2s ease;
}
.kb-entry-header:hover {
background-color: var(--color-bg-secondary);
margin: -0.5rem;
padding: 1.5rem;
border-radius: 0.5rem;
}
.kb-expand-icon {
transition: transform 0.3s ease;
color: var(--color-text-secondary);
}
.kb-expand-icon svg {
transition: transform 0.3s ease;
}
.kb-entry-content {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
animation: slideDown 0.3s ease-out;
}
/* Tool Details Modal */
.tool-details {
display: none;
@ -522,6 +570,69 @@ footer {
to { opacity: 1; }
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
padding-top: 0;
margin-top: 0;
}
to {
opacity: 1;
max-height: 1000px;
padding-top: 1rem;
margin-top: 1rem;
}
}
@keyframes highlight-flash {
0% { background-color: rgba(37, 99, 235, 0.1); }
100% { background-color: transparent; }
}
.kb-content {
line-height: 1.7;
}
.kb-content h3 {
color: var(--color-text);
border-bottom: 2px solid var(--color-primary);
padding-bottom: 0.5rem;
}
.kb-content p {
margin-bottom: 1rem;
}
.kb-content ul, .kb-content ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.kb-content code {
background-color: var(--color-bg-secondary);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.875em;
}
.kb-content pre {
background-color: var(--color-bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
border: 1px solid var(--color-border);
}
.kb-content blockquote {
border-left: 4px solid var(--color-primary);
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: var(--color-text-secondary);
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}