add knowledgebase
This commit is contained in:
parent
d18cc060e5
commit
a057120d7a
124
deploy-static.sh
124
deploy-static.sh
@ -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"
|
|
47
framework.md
47
framework.md
@ -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
|
|
@ -18,6 +18,11 @@ const currentPath = Astro.url.pathname;
|
|||||||
~/
|
~/
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/knowledgebase" class={`nav-link ${currentPath === '/knowledgebase' ? 'active' : ''}`}>
|
||||||
|
~/knowledgebase
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/status" class={`nav-link ${currentPath === '/status' ? 'active' : ''}`}>
|
<a href="/status" class={`nav-link ${currentPath === '/status' ? 'active' : ''}`}>
|
||||||
~/status
|
~/status
|
||||||
|
@ -13,6 +13,7 @@ export interface Props {
|
|||||||
license: string;
|
license: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
statusUrl?: string;
|
statusUrl?: string;
|
||||||
|
knowledgebase?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,9 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
|||||||
tool.projectUrl !== "" &&
|
tool.projectUrl !== "" &&
|
||||||
tool.projectUrl.trim() !== "";
|
tool.projectUrl.trim() !== "";
|
||||||
|
|
||||||
|
// Check if tool has knowledgebase entry
|
||||||
|
const hasKnowledgebase = tool.knowledgebase === true;
|
||||||
|
|
||||||
// Determine card styling based on hosting status (derived from projectUrl)
|
// Determine card styling based on hosting status (derived from projectUrl)
|
||||||
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
|
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 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;">
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
|
||||||
<h3 style="margin: 0;">{tool.name}</h3>
|
<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>}
|
{hasValidProjectUrl && <span class="badge badge-primary">Self-Hosted</span>}
|
||||||
{tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
{tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||||
|
{hasKnowledgebase && <span class="badge badge-error">Infos 📖</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -259,29 +259,34 @@ const sortedTags = Object.entries(tagFrequency)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter function
|
// Filter function
|
||||||
function filterTools() {
|
function filterTools() {
|
||||||
const searchTerm = searchInput.value.toLowerCase();
|
const searchTerm = searchInput.value.toLowerCase();
|
||||||
const selectedDomain = domainSelect.value;
|
const selectedDomain = domainSelect.value;
|
||||||
const includeProprietary = proprietaryCheckbox.checked;
|
const includeProprietary = proprietaryCheckbox.checked;
|
||||||
|
|
||||||
const filtered = window.toolsData.filter(tool => {
|
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
|
// Search filter
|
||||||
if (searchTerm && !(
|
if (searchTerm && !(
|
||||||
tool.name.toLowerCase().includes(searchTerm) ||
|
tool.name.toLowerCase().includes(searchTerm) ||
|
||||||
tool.description.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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Domain filter
|
// Domain filter
|
||||||
if (selectedDomain && !tool.domains.includes(selectedDomain)) {
|
if (selectedDomain && !domains.includes(selectedDomain)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase filter
|
// Phase filter
|
||||||
if (selectedPhase && !tool.phases.includes(selectedPhase)) {
|
if (selectedPhase && !phases.includes(selectedPhase)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +296,7 @@ const sortedTags = Object.entries(tagFrequency)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tag filter
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@ const tools = data.tools;
|
|||||||
|
|
||||||
// Separate collaboration tools from domain-specific tools
|
// Separate collaboration tools from domain-specific tools
|
||||||
const collaborationTools = tools.filter((tool: any) =>
|
const collaborationTools = tools.filter((tool: any) =>
|
||||||
tool.phases.includes('collaboration')
|
tool.phases && tool.phases.includes('collaboration')
|
||||||
);
|
);
|
||||||
|
|
||||||
const domainTools = tools.filter((tool: any) =>
|
const domainTools = tools.filter((tool: any) =>
|
||||||
!tool.phases.includes('collaboration')
|
!tool.phases || !tool.phases.includes('collaboration')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create matrix structure for domain-specific tools only
|
// Create matrix structure for domain-specific tools only
|
||||||
@ -27,7 +27,7 @@ domains.forEach((domain: any) => {
|
|||||||
matrix[domain.id] = {};
|
matrix[domain.id] = {};
|
||||||
phases.filter((phase: any) => phase.id !== 'collaboration').forEach((phase: any) => {
|
phases.filter((phase: any) => phase.id !== 'collaboration').forEach((phase: any) => {
|
||||||
matrix[domain.id][phase.id] = domainTools.filter((tool: 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;">
|
<div style="display: flex; gap: 0.25rem;">
|
||||||
{hasValidProjectUrl && <span class="badge-mini badge-primary">Self-Hosted</span>}
|
{hasValidProjectUrl && <span class="badge-mini badge-primary">Self-Hosted</span>}
|
||||||
{tool.license !== 'Proprietary' && <span class="badge-mini badge-success">OSS</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>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size: 0.75rem; color: var(--color-text-secondary); margin: 0.25rem 0; line-height: 1.3;">
|
<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' : ''}`}
|
class={`tool-chip ${hasValidProjectUrl ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`}
|
||||||
data-tool-name={tool.name}
|
data-tool-name={tool.name}
|
||||||
onclick={`window.showToolDetails('${tool.name}')`}
|
onclick={`window.showToolDetails('${tool.name}')`}
|
||||||
|
title={`${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`}
|
||||||
>
|
>
|
||||||
{tool.name}
|
{tool.name}
|
||||||
|
{tool.knowledgebase === true && <span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -131,7 +134,7 @@ domains.forEach((domain: any) => {
|
|||||||
<div id="tool-tags" style="margin-bottom: 1rem;"></div>
|
<div id="tool-tags" style="margin-bottom: 1rem;"></div>
|
||||||
|
|
||||||
<!-- Updated button container for dual buttons -->
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<script define:vars={{ toolsData: tools, collaborationTools, domainTools }}>
|
<script define:vars={{ toolsData: tools, collaborationTools, domainTools }}>
|
||||||
@ -205,13 +208,14 @@ domains.forEach((domain: any) => {
|
|||||||
<div style="display: flex; gap: 0.25rem;">
|
<div style="display: flex; gap: 0.25rem;">
|
||||||
${hasValidProjectUrl ? '<span class="badge-mini badge-primary">Self-Hosted</span>' : ''}
|
${hasValidProjectUrl ? '<span class="badge-mini badge-primary">Self-Hosted</span>' : ''}
|
||||||
${tool.license !== 'Proprietary' ? '<span class="badge-mini badge-success">OSS</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>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size: 0.75rem; color: var(--color-text-secondary); margin: 0.25rem 0; line-height: 1.3;">
|
<p style="font-size: 0.75rem; color: var(--color-text-secondary); margin: 0.25rem 0; line-height: 1.3;">
|
||||||
${tool.description}
|
${tool.description}
|
||||||
</p>
|
</p>
|
||||||
<div style="display: flex; gap: 0.75rem; font-size: 0.6875rem; color: var(--color-text-secondary);">
|
<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>•</span>
|
||||||
<span>${tool.skillLevel}</span>
|
<span>${tool.skillLevel}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -243,14 +247,19 @@ domains.forEach((domain: any) => {
|
|||||||
if (tool.license !== 'Proprietary') {
|
if (tool.license !== 'Proprietary') {
|
||||||
badgesContainer.innerHTML += '<span class="badge badge-success">Open Source</span>';
|
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 metadataContainer = document.getElementById('tool-metadata');
|
||||||
const domainsText = tool.domains.length > 0 ? tool.domains.join(', ') : 'Domain-agnostic';
|
const domains = tool.domains || [];
|
||||||
const phasesText = tool.phases.join(', ');
|
const phases = tool.phases || [];
|
||||||
|
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
|
||||||
|
const phasesText = phases.join(', ');
|
||||||
metadataContainer.innerHTML = `
|
metadataContainer.innerHTML = `
|
||||||
<div style="display: grid; gap: 0.5rem;">
|
<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>Skill Level:</strong> ${tool.skillLevel}</div>
|
||||||
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
|
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
|
||||||
<div><strong>Deployment:</strong> ${tool.accessType}</div>
|
<div><strong>Deployment:</strong> ${tool.accessType}</div>
|
||||||
@ -259,36 +268,61 @@ domains.forEach((domain: any) => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Tags
|
// Tags - safe array handling
|
||||||
const tagsContainer = document.getElementById('tool-tags');
|
const tagsContainer = document.getElementById('tool-tags');
|
||||||
|
const tags = tool.tags || [];
|
||||||
tagsContainer.innerHTML = `
|
tagsContainer.innerHTML = `
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
<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>
|
</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');
|
const linksContainer = document.getElementById('tool-links');
|
||||||
|
|
||||||
|
let linksHTML = '';
|
||||||
|
|
||||||
|
// Main action buttons
|
||||||
if (hasValidProjectUrl) {
|
if (hasValidProjectUrl) {
|
||||||
// Two buttons for tools we're hosting
|
// 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;">
|
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary" style="flex: 1;">
|
||||||
Software-Homepage
|
Software-Homepage
|
||||||
</a>
|
</a>
|
||||||
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 1;">
|
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 1;">
|
||||||
Zugreifen
|
Zugreifen
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Single button for tools we're not hosting
|
// 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%;">
|
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%;">
|
||||||
Software-Homepage
|
Software-Homepage
|
||||||
</a>
|
</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
|
// Show modal
|
||||||
document.getElementById('modal-overlay').classList.add('active');
|
document.getElementById('modal-overlay').classList.add('active');
|
||||||
document.getElementById('tool-details').classList.add('active');
|
document.getElementById('tool-details').classList.add('active');
|
||||||
@ -336,7 +370,7 @@ domains.forEach((domain: any) => {
|
|||||||
dfirMatrixSection.style.display = 'none';
|
dfirMatrixSection.style.display = 'none';
|
||||||
|
|
||||||
// Filter collaboration tools
|
// Filter collaboration tools
|
||||||
const filteredCollaboration = filtered.filter(tool => tool.phases.includes('collaboration'));
|
const filteredCollaboration = filtered.filter(tool => (tool.phases || []).includes('collaboration'));
|
||||||
collaborationContainer.innerHTML = '';
|
collaborationContainer.innerHTML = '';
|
||||||
|
|
||||||
filteredCollaboration.forEach(tool => {
|
filteredCollaboration.forEach(tool => {
|
||||||
@ -352,7 +386,7 @@ domains.forEach((domain: any) => {
|
|||||||
collaborationSection.style.display = 'block';
|
collaborationSection.style.display = 'block';
|
||||||
|
|
||||||
// Show all collaboration tools that pass general filters
|
// 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 = '';
|
collaborationContainer.innerHTML = '';
|
||||||
|
|
||||||
filteredCollaboration.forEach(tool => {
|
filteredCollaboration.forEach(tool => {
|
||||||
@ -369,22 +403,26 @@ domains.forEach((domain: any) => {
|
|||||||
cell.innerHTML = '';
|
cell.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-populate with filtered DFIR tools
|
// Re-populate with filtered DFIR tools - with safe array handling
|
||||||
const filteredDfirTools = filtered.filter(tool => !tool.phases.includes('collaboration'));
|
const filteredDfirTools = filtered.filter(tool => !(tool.phases || []).includes('collaboration'));
|
||||||
filteredDfirTools.forEach(tool => {
|
filteredDfirTools.forEach(tool => {
|
||||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||||
tool.projectUrl !== null &&
|
tool.projectUrl !== null &&
|
||||||
tool.projectUrl !== "" &&
|
tool.projectUrl !== "" &&
|
||||||
tool.projectUrl.trim() !== "";
|
tool.projectUrl.trim() !== "";
|
||||||
|
|
||||||
tool.domains.forEach(domain => {
|
const domains = tool.domains || [];
|
||||||
tool.phases.forEach(phase => {
|
const phases = tool.phases || [];
|
||||||
|
|
||||||
|
domains.forEach(domain => {
|
||||||
|
phases.forEach(phase => {
|
||||||
if (phase !== 'collaboration') {
|
if (phase !== 'collaboration') {
|
||||||
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
|
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
|
||||||
if (cell) {
|
if (cell) {
|
||||||
const chip = document.createElement('span');
|
const chip = document.createElement('span');
|
||||||
chip.className = `tool-chip ${hasValidProjectUrl ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`;
|
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);
|
chip.onclick = () => window.showToolDetails(tool.name);
|
||||||
cell.appendChild(chip);
|
cell.appendChild(chip);
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@ tools:
|
|||||||
# Disk & File System Analysis Tools
|
# Disk & File System Analysis Tools
|
||||||
- name: Autopsy
|
- name: Autopsy
|
||||||
description: >-
|
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
|
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:
|
domains:
|
||||||
- incident-response
|
- incident-response
|
||||||
- law-enforcement
|
- law-enforcement
|
||||||
@ -13,15 +15,16 @@ tools:
|
|||||||
- data-collection
|
- data-collection
|
||||||
- examination
|
- examination
|
||||||
- analysis
|
- analysis
|
||||||
|
- reporting
|
||||||
platforms:
|
platforms:
|
||||||
- Windows
|
- Windows
|
||||||
- Linux
|
- Linux (Snap-Paket)
|
||||||
- macOS
|
|
||||||
skillLevel: intermediate
|
skillLevel: intermediate
|
||||||
accessType: download
|
accessType: download
|
||||||
url: https://www.autopsy.com/
|
url: https://www.autopsy.com/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- disk-imaging
|
- disk-imaging
|
||||||
- file-carving
|
- file-carving
|
||||||
@ -56,6 +59,7 @@ tools:
|
|||||||
url: https://www.volatilityfoundation.org/
|
url: https://www.volatilityfoundation.org/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: VSL
|
license: VSL
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- memory-analysis
|
- memory-analysis
|
||||||
- malware-detection
|
- malware-detection
|
||||||
@ -74,6 +78,7 @@ tools:
|
|||||||
Kollaborative Security-Incident-Response-Plattform für SOCs, CERTs und
|
Kollaborative Security-Incident-Response-Plattform für SOCs, CERTs und
|
||||||
Sicherheitsteams mit Case-Management. Ideal für alle Phasen einer
|
Sicherheitsteams mit Case-Management. Ideal für alle Phasen einer
|
||||||
Untersuchung, besonders für Koordination und Berichterstattung.
|
Untersuchung, besonders für Koordination und Berichterstattung.
|
||||||
|
Keine Erfahrungswerte.
|
||||||
domains:
|
domains:
|
||||||
- incident-response
|
- incident-response
|
||||||
- law-enforcement
|
- law-enforcement
|
||||||
@ -83,7 +88,6 @@ tools:
|
|||||||
- examination
|
- examination
|
||||||
- analysis
|
- analysis
|
||||||
- reporting
|
- reporting
|
||||||
- collaboration
|
|
||||||
platforms:
|
platforms:
|
||||||
- Web
|
- Web
|
||||||
skillLevel: intermediate
|
skillLevel: intermediate
|
||||||
@ -91,6 +95,7 @@ tools:
|
|||||||
url: https://strangebee.com/
|
url: https://strangebee.com/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Community Edition (Free) / Commercial
|
license: Community Edition (Free) / Commercial
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- case-management
|
- case-management
|
||||||
- team-collaboration
|
- team-collaboration
|
||||||
@ -117,7 +122,6 @@ tools:
|
|||||||
- data-collection
|
- data-collection
|
||||||
- examination
|
- examination
|
||||||
- analysis
|
- analysis
|
||||||
- collaboration
|
|
||||||
platforms:
|
platforms:
|
||||||
- Web
|
- Web
|
||||||
skillLevel: intermediate
|
skillLevel: intermediate
|
||||||
@ -125,6 +129,7 @@ tools:
|
|||||||
url: https://misp-project.org/
|
url: https://misp-project.org/
|
||||||
projectUrl: https://misp.cc24.dev
|
projectUrl: https://misp.cc24.dev
|
||||||
license: AGPL-3.0
|
license: AGPL-3.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- threat-intelligence
|
- threat-intelligence
|
||||||
- ioc-sharing
|
- ioc-sharing
|
||||||
@ -156,6 +161,7 @@ tools:
|
|||||||
url: https://timesketch.org/
|
url: https://timesketch.org/
|
||||||
projectUrl: https://timesketch.cc24.dev
|
projectUrl: https://timesketch.cc24.dev
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- timeline-analysis
|
- timeline-analysis
|
||||||
- data-visualization
|
- data-visualization
|
||||||
@ -190,6 +196,7 @@ tools:
|
|||||||
url: https://www.wireshark.org/
|
url: https://www.wireshark.org/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: GPL-2.0
|
license: GPL-2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- packet-capture
|
- packet-capture
|
||||||
- protocol-analysis
|
- protocol-analysis
|
||||||
@ -224,6 +231,7 @@ tools:
|
|||||||
url: https://www.magnetforensics.com/products/magnet-axiom/
|
url: https://www.magnetforensics.com/products/magnet-axiom/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Proprietary
|
license: Proprietary
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- mobile-forensics
|
- mobile-forensics
|
||||||
- cloud-acquisition
|
- cloud-acquisition
|
||||||
@ -255,6 +263,7 @@ tools:
|
|||||||
url: https://cellebrite.com/en/ufed/
|
url: https://cellebrite.com/en/ufed/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Proprietary
|
license: Proprietary
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- mobile-extraction
|
- mobile-extraction
|
||||||
- physical-extraction
|
- physical-extraction
|
||||||
@ -283,6 +292,7 @@ tools:
|
|||||||
url: https://github.com/cert-ee/cuckoo3
|
url: https://github.com/cert-ee/cuckoo3
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: GPL-3.0
|
license: GPL-3.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- dynamic-analysis
|
- dynamic-analysis
|
||||||
- behavior-monitoring
|
- behavior-monitoring
|
||||||
@ -314,6 +324,7 @@ tools:
|
|||||||
url: https://ghidra-sre.org/
|
url: https://ghidra-sre.org/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- reverse-engineering
|
- reverse-engineering
|
||||||
- disassembly
|
- disassembly
|
||||||
@ -346,6 +357,7 @@ tools:
|
|||||||
url: https://plaso.readthedocs.io/
|
url: https://plaso.readthedocs.io/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- timeline-generation
|
- timeline-generation
|
||||||
- log-parsing
|
- log-parsing
|
||||||
@ -376,6 +388,7 @@ tools:
|
|||||||
url: https://gchq.github.io/CyberChef/
|
url: https://gchq.github.io/CyberChef/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- data-transformation
|
- data-transformation
|
||||||
- encoding-decoding
|
- encoding-decoding
|
||||||
@ -410,6 +423,7 @@ tools:
|
|||||||
url: https://www.velociraptor.app/
|
url: https://www.velociraptor.app/
|
||||||
projectUrl: https://raptor.cc24.dev
|
projectUrl: https://raptor.cc24.dev
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- remote-collection
|
- remote-collection
|
||||||
- live-forensics
|
- live-forensics
|
||||||
@ -442,6 +456,7 @@ tools:
|
|||||||
url: https://github.com/google/grr
|
url: https://github.com/google/grr
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- remote-forensics
|
- remote-forensics
|
||||||
- scalable-collection
|
- scalable-collection
|
||||||
@ -473,6 +488,7 @@ tools:
|
|||||||
url: https://arkime.com/
|
url: https://arkime.com/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- full-packet-capture
|
- full-packet-capture
|
||||||
- pcap-indexing
|
- pcap-indexing
|
||||||
@ -503,6 +519,7 @@ tools:
|
|||||||
url: https://www.netresec.com/?page=NetworkMiner
|
url: https://www.netresec.com/?page=NetworkMiner
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: GPL-2.0 / Commercial
|
license: GPL-2.0 / Commercial
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- pcap-analysis
|
- pcap-analysis
|
||||||
- file-extraction
|
- file-extraction
|
||||||
@ -533,6 +550,7 @@ tools:
|
|||||||
https://www.kroll.com/en/services/cyber-risk/incident-response-litigation-support/kroll-artifact-parser-extractor-kape
|
https://www.kroll.com/en/services/cyber-risk/incident-response-litigation-support/kroll-artifact-parser-extractor-kape
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Freeware
|
license: Freeware
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- triage-collection
|
- triage-collection
|
||||||
- artifact-parsing
|
- artifact-parsing
|
||||||
@ -565,6 +583,7 @@ tools:
|
|||||||
url: https://exiftool.org/
|
url: https://exiftool.org/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Perl Artistic License
|
license: Perl Artistic License
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- metadata-extraction
|
- metadata-extraction
|
||||||
- exif-analysis
|
- exif-analysis
|
||||||
@ -594,6 +613,7 @@ tools:
|
|||||||
url: https://www.chainalysis.com/
|
url: https://www.chainalysis.com/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: Proprietary
|
license: Proprietary
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- blockchain-analysis
|
- blockchain-analysis
|
||||||
- crypto-tracing
|
- crypto-tracing
|
||||||
@ -627,6 +647,7 @@ tools:
|
|||||||
url: https://neo4j.com/
|
url: https://neo4j.com/
|
||||||
projectUrl: https://graph.cc24.dev
|
projectUrl: https://graph.cc24.dev
|
||||||
license: GPL-3.0 / Commercial
|
license: GPL-3.0 / Commercial
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- graph-database
|
- graph-database
|
||||||
- relationship-analysis
|
- relationship-analysis
|
||||||
@ -658,6 +679,7 @@ tools:
|
|||||||
url: https://qgis.org/
|
url: https://qgis.org/
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: GPL-2.0
|
license: GPL-2.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- geospatial-analysis
|
- geospatial-analysis
|
||||||
- gps-visualization
|
- gps-visualization
|
||||||
@ -675,12 +697,8 @@ tools:
|
|||||||
kollaborative Phasen und sichere Speicherung von Beweismitteln
|
kollaborative Phasen und sichere Speicherung von Beweismitteln
|
||||||
mit Versionierung.
|
mit Versionierung.
|
||||||
domains:
|
domains:
|
||||||
- incident-response
|
|
||||||
- law-enforcement
|
|
||||||
- fraud-investigation
|
|
||||||
phases:
|
phases:
|
||||||
- collaboration
|
- collaboration-general
|
||||||
- reporting
|
|
||||||
platforms:
|
platforms:
|
||||||
- Web
|
- Web
|
||||||
skillLevel: novice
|
skillLevel: novice
|
||||||
@ -688,6 +706,7 @@ tools:
|
|||||||
url: https://nextcloud.com/
|
url: https://nextcloud.com/
|
||||||
projectUrl: https://cloud.cc24.dev
|
projectUrl: https://cloud.cc24.dev
|
||||||
license: AGPL-3.0
|
license: AGPL-3.0
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- file-sharing
|
- file-sharing
|
||||||
- collaboration
|
- collaboration
|
||||||
@ -708,8 +727,7 @@ tools:
|
|||||||
- incident-response
|
- incident-response
|
||||||
- malware-analysis
|
- malware-analysis
|
||||||
phases:
|
phases:
|
||||||
- collaboration
|
- collaboration-general
|
||||||
- reporting
|
|
||||||
platforms:
|
platforms:
|
||||||
- Web
|
- Web
|
||||||
skillLevel: beginner
|
skillLevel: beginner
|
||||||
@ -717,6 +735,7 @@ tools:
|
|||||||
url: https://gitea.io/
|
url: https://gitea.io/
|
||||||
projectUrl: https://git.cc24.dev
|
projectUrl: https://git.cc24.dev
|
||||||
license: MIT
|
license: MIT
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- version-control
|
- version-control
|
||||||
- code-repository
|
- code-repository
|
||||||
@ -749,6 +768,7 @@ tools:
|
|||||||
url: https://github.com/ReFirmLabs/binwalk
|
url: https://github.com/ReFirmLabs/binwalk
|
||||||
projectUrl: ''
|
projectUrl: ''
|
||||||
license: MIT
|
license: MIT
|
||||||
|
knowledgebase:
|
||||||
tags:
|
tags:
|
||||||
- firmware-analysis
|
- firmware-analysis
|
||||||
- file-carving
|
- file-carving
|
||||||
@ -786,5 +806,5 @@ phases:
|
|||||||
name: Analyse
|
name: Analyse
|
||||||
- id: reporting
|
- id: reporting
|
||||||
name: Bericht & Präsentation
|
name: Bericht & Präsentation
|
||||||
- id: collaboration
|
- id: collaboration-general
|
||||||
name: Übergreifend & Kollaboration
|
name: Übergreifend & Kollaboration
|
@ -138,13 +138,14 @@ const tools = data.tools;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create tool card element
|
|
||||||
function createToolCard(tool) {
|
function createToolCard(tool) {
|
||||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||||
tool.projectUrl !== null &&
|
tool.projectUrl !== null &&
|
||||||
tool.projectUrl !== "" &&
|
tool.projectUrl !== "" &&
|
||||||
tool.projectUrl.trim() !== "";
|
tool.projectUrl.trim() !== "";
|
||||||
|
|
||||||
|
const hasKnowledgebase = tool.knowledgebase === true;
|
||||||
|
|
||||||
const cardDiv = document.createElement('div');
|
const cardDiv = document.createElement('div');
|
||||||
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
|
const cardClass = hasValidProjectUrl ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
|
||||||
cardDiv.className = cardClass;
|
cardDiv.className = cardClass;
|
||||||
@ -177,9 +178,10 @@ function createToolCard(tool) {
|
|||||||
cardDiv.innerHTML = `
|
cardDiv.innerHTML = `
|
||||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
|
||||||
<h3 style="margin: 0;">${tool.name}</h3>
|
<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>' : ''}
|
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
|
||||||
${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
|
${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
|
||||||
|
${hasKnowledgebase ? '<span class="badge badge-error">Infos 📖</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -195,7 +197,7 @@ function createToolCard(tool) {
|
|||||||
<line x1="9" y1="15" x2="15" y2="15"></line>
|
<line x1="9" y1="15" x2="15" y2="15"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-muted" style="font-size: 0.75rem;">
|
<span class="text-muted" style="font-size: 0.75rem;">
|
||||||
${tool.platforms.join(', ')}
|
${(tool.platforms || []).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -221,7 +223,7 @@ function createToolCard(tool) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1rem;">
|
<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>
|
</div>
|
||||||
|
|
||||||
${buttonHTML}
|
${buttonHTML}
|
||||||
|
277
src/pages/knowledgebase.astro
Normal file
277
src/pages/knowledgebase.astro
Normal 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>
|
@ -389,6 +389,54 @@ input[type="checkbox"] {
|
|||||||
color: white;
|
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 Modal */
|
||||||
.tool-details {
|
.tool-details {
|
||||||
display: none;
|
display: none;
|
||||||
@ -522,6 +570,69 @@ footer {
|
|||||||
to { opacity: 1; }
|
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 {
|
.fade-in {
|
||||||
animation: fadeIn 0.3s ease-in;
|
animation: fadeIn 0.3s ease-in;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user