gated content

This commit is contained in:
overcuriousity 2025-08-11 22:00:49 +02:00
parent 2f17370938
commit d49b031eb9
12 changed files with 219 additions and 66 deletions

6
.astro/content.d.ts vendored
View File

@ -164,9 +164,11 @@ declare module 'astro:content' {
type DataEntryMap = {
"knowledgebase": Record<string, {
id: string;
body?: string;
render(): Render[".md"];
slug: string;
body: string;
collection: "knowledgebase";
data: any;
data: InferEntrySchema<"knowledgebase">;
rendered?: RenderedContent;
filePath?: string;
}>;

View File

@ -30,6 +30,7 @@ NODE_ENV=development
# Set to true to require authentication (RECOMMENDED for production)
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
AUTHENTICATION_NECESSARY_AI=false
AUTHENTICATION_NECESSARY_GATEDCONTENT=true
# OIDC Provider Configuration
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc

View File

@ -16,6 +16,11 @@ const knowledgebaseCollection = defineCollection({
tags: z.array(z.string()).default([]),
published: z.boolean().default(true),
gated_content: z.boolean().default(false), // NEW: Gated content flag
})
});
});
export const collections = {
knowledgebase: knowledgebaseCollection
};

View File

@ -6,6 +6,7 @@ last_updated: 2025-07-20
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
difficulty: "advanced"
categories: ["incident-response", "malware-analysis", "network-forensics"]
gated_content: true
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
sections:
overview: true

6
src/env.d.ts vendored
View File

@ -25,9 +25,9 @@ declare global {
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
isToolHosted: (tool: any) => boolean;
checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
checkClientAuth: (context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<void>;
setupAuthButtons: (selector?: string) => void;
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;

View File

@ -9,13 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
const contributionAuth = await withAPIAuth(request, 'contributions');
const aiAuth = await withAPIAuth(request, 'ai');
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent'); // ADDED
return apiResponse.success({
authenticated: contributionAuth.authenticated || aiAuth.authenticated,
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
contributionAuthRequired: contributionAuth.authRequired,
aiAuthRequired: aiAuth.authRequired,
gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED
contributionAuthenticated: contributionAuth.authenticated,
aiAuthenticated: aiAuth.authenticated,
gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
});
}, 'Status check failed');

View File

@ -166,6 +166,14 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" id="gated-content" name="gatedContent" />
<span>🔒 Als geschützten Inhalt markieren (Authentifizierung erforderlich)</span>
</label>
<small class="form-help">Nur für interne oder vertrauliche Inhalte</small>
</div>
<div class="form-group">
<label for="reason" class="form-label">Grund für den Beitrag (Optional)</label>
<textarea

View File

@ -3,12 +3,16 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import { getToolsData } from '../utils/dataService.js';
import ContributionButton from '../components/ContributionButton.astro';
import { isGatedContentAuthRequired } from '../utils/auth.js';
const data = await getToolsData();
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
return entry.data.published !== false;
});
// Check if gated content authentication is enabled globally
const gatedContentAuthEnabled = isGatedContentAuthRequired();
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
const associatedTool = entry.data.tool_name
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
@ -23,6 +27,7 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
difficulty: entry.data.difficulty,
categories: entry.data.categories || [],
tags: entry.data.tags || [],
gated_content: entry.data.gated_content || false, // NEW: Include gated content flag
tool_name: entry.data.tool_name,
related_tools: entry.data.related_tools || [],
@ -39,6 +44,10 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
});
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
// Count gated vs public articles for statistics
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
const publicCount = knowledgebaseEntries.length - gatedCount;
---
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
@ -52,6 +61,24 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
</p>
{gatedContentAuthEnabled && gatedCount > 0 && (
<div class="gated-content-info mb-4 p-3 rounded" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border);">
<div class="flex items-center justify-center gap-2 text-sm text-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<span>
{gatedCount} geschützte Artikel • {publicCount} öffentliche Artikel
</span>
</div>
<p class="text-xs text-secondary mt-1">
🔒 Geschützte Artikel erfordern Authentifizierung
</p>
</div>
)}
<div class="flex gap-4 justify-center flex-wrap">
<ContributionButton type="write" variant="primary" text="Artikel schreiben" style="padding: 0.75rem 1.5rem;" />
<button onclick="window.scrollToElementById('kb-entries')" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
@ -60,7 +87,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
Artikel durchsuchen
</a>
</button>
</div>
</div>
@ -107,6 +134,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
const isStandalone = !hasAssociatedTool;
const isGated = entry.gated_content === true;
const articleUrl = `/knowledgebase/${entry.slug}`;
@ -116,6 +144,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
id={`kb-${entry.slug}`}
data-tool-name={entry.title.toLowerCase()}
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
data-gated={isGated}
onclick={`window.location.href='${articleUrl}'`}
>
<!-- Card Header -->
@ -125,6 +154,11 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-primary mb-1 leading-tight">
{entry.title}
{isGated && gatedContentAuthEnabled && (
<span class="gated-indicator ml-2" title="Geschützter Inhalt - Authentifizierung erforderlich">
🔒
</span>
)}
</h3>
<div class="flex gap-2 flex-wrap mb-2">
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
@ -134,6 +168,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
<span class="badge badge-error">📖</span>
{isGated && gatedContentAuthEnabled && <span class="badge badge-warning">🔒</span>}
</div>
</div>
</div>
@ -145,7 +180,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Öffnen
{isGated && gatedContentAuthEnabled ? 'Anmelden' : 'Öffnen'}
</a>
<button
class="btn btn-secondary btn-sm"
@ -168,6 +203,16 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<!-- Description -->
<p class="text-secondary mb-4 leading-relaxed">
{entry.description}
{isGated && gatedContentAuthEnabled && (
<span class="gated-content-hint ml-2 text-xs">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline; margin-right: 0.25rem;">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Authentifizierung erforderlich
</span>
)}
</p>
<!-- Metadata Footer -->
@ -299,7 +344,26 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
lastScrollY = window.scrollY;
}
});
});
</script>
</script>
<style>
.gated-indicator {
font-size: 0.875rem;
opacity: 0.8;
}
.gated-content-hint {
color: var(--color-warning);
font-weight: 500;
}
.kb-entry[data-gated="true"] {
border-left: 3px solid var(--color-warning);
}
.gated-content-info {
border-left: 4px solid var(--color-warning) !important;
}
</style>
</BaseLayout>

View File

@ -2,6 +2,7 @@
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getToolsData } from '../../utils/dataService.js';
import { isGatedContentAuthRequired } from '../../utils/auth.js';
export const prerender = true;
@ -20,6 +21,13 @@ export async function getStaticPaths() {
const { entry }: { entry: any } = Astro.props;
// Check if this article is gated and if gated content auth is required globally
const isGatedContent = entry.data.gated_content === true;
const gatedContentAuthRequired = isGatedContentAuthRequired();
const requiresAuth = isGatedContent && gatedContentAuthRequired;
console.log(`[GATED CONTENT] Article: ${entry.data.title}, Gated: ${isGatedContent}, Auth Required: ${requiresAuth}`);
const { Content } = await entry.render();
const data = await getToolsData();
@ -52,6 +60,68 @@ const currentUrl = Astro.url.href;
---
<BaseLayout title={entry.data.title} description={entry.data.description}>
{requiresAuth && (
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
// Client-side authentication check for gated content
document.addEventListener('DOMContentLoaded', async () => {
if (!requiresAuth) return;
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
try {
const response = await fetch('/api/auth/status');
const authStatus = await response.json();
const isAuthenticated = authStatus.gatedContentAuthenticated || false;
const authRequired = authStatus.gatedContentAuthRequired || false;
console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + isAuthenticated);
if (authRequired && !isAuthenticated) {
console.log('[GATED CONTENT] Redirecting for authentication: ' + articleTitle);
// Show loading message briefly
const contentArea = document.querySelector('.article-content');
if (contentArea) {
contentArea.innerHTML = [
'<div style="text-align: center; padding: 3rem;">',
'<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>',
'<h3 style="margin-bottom: 1rem;">Authentifizierung erforderlich</h3>',
'<p style="margin-bottom: 2rem;">Sie werden zur Anmeldung weitergeleitet...</p>',
'</div>'
].join('');
}
// Redirect to login after brief delay
setTimeout(() => {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = '/api/auth/login?returnTo=' + currentUrl;
}, 1000);
} else {
console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
}
} catch (error) {
console.error('[GATED CONTENT] Auth check failed:', error);
// On error, show auth required message
if (requiresAuth) {
const contentArea = document.querySelector('.article-content');
if (contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [
'<div style="text-align: center; padding: 3rem;">',
'<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>',
'<h3 style="margin-bottom: 1rem;">Authentifizierungsfehler</h3>',
'<p style="margin-bottom: 2rem;">Bitte versuchen Sie es später erneut oder melden Sie sich an.</p>',
'<a href="' + loginUrl + '" class="btn btn-primary">Anmelden</a>',
'</div>'
].join('');
}
}
}
});
</script>
)}
<div class="article-layout">
<!-- Article Header -->
<header class="article-header">
@ -77,8 +147,24 @@ const currentUrl = Astro.url.href;
<h1 class="article-title">
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
{entry.data.title}
{isGatedContent && (
<span class="gated-indicator" title="Geschützter Inhalt - Authentifizierung erforderlich">
🔒
</span>
)}
</h1>
<p class="article-description">{entry.data.description}</p>
{isGatedContent && gatedContentAuthRequired && (
<div class="gated-content-notice">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<span>Dieser Artikel enthält geschützte Inhalte</span>
</div>
)}
</div>
<div class="article-metadata-grid">
@ -96,6 +182,7 @@ const currentUrl = Astro.url.href;
</>
)}
<span class="badge badge-error">📖</span>
{isGatedContent && <span class="badge badge-warning">🔒</span>}
</div>
</div>
@ -477,5 +564,23 @@ const currentUrl = Astro.url.href;
});
</script>
<style>
.gated-indicator {
font-size: 0.875rem;
opacity: 0.8;
margin-left: 0.5rem;
}
.gated-content-notice {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background-color: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
border: 1px solid var(--color-warning, #ffc107);
border-radius: 0.375rem;
color: var(--color-warning-text, #856404);
font-size: 0.875rem;
margin-top: 1rem;
}
</style>
</BaseLayout>

View File

@ -112,27 +112,6 @@ class AuditService {
};
}
getDebugInfo(): {
config: AuditConfig;
environment: Record<string, any>;
context: string;
} {
const context = typeof process !== 'undefined' ? 'server' : 'client';
return {
config: this.config,
environment: {
FORENSIC_AUDIT_ENABLED: env('FORENSIC_AUDIT_ENABLED'),
FORENSIC_AUDIT_DETAIL_LEVEL: env('FORENSIC_AUDIT_DETAIL_LEVEL'),
FORENSIC_AUDIT_RETENTION_HOURS: env('FORENSIC_AUDIT_RETENTION_HOURS'),
FORENSIC_AUDIT_MAX_ENTRIES: env('FORENSIC_AUDIT_MAX_ENTRIES'),
processEnvKeys: typeof process !== 'undefined' ? Object.keys(process.env).filter(k => k.includes('AUDIT')) : [],
importMetaEnvAvailable: typeof import.meta !== 'undefined' && !!(import.meta as any).env
},
context
};
}
addEntry(
phase: string,
action: string,
@ -396,16 +375,4 @@ class AuditService {
}
export const auditService = new AuditService();
export type { ProcessedAuditTrail, CompressedAuditEntry };
export const debugAuditService = {
getDebugInfo() {
return auditService.getDebugInfo();
},
isEnabled() {
return auditService.isEnabled();
},
getConfig() {
return auditService.getConfig();
}
};
export type { ProcessedAuditTrail, CompressedAuditEntry };

View File

@ -1,4 +1,4 @@
// src/utils/auth.js (FIXED - Cleaned up and enhanced debugging)
// src/utils/auth.js (ENHANCED - Added gated content support)
import type { AstroGlobal } from 'astro';
import crypto from 'crypto';
import { config } from 'dotenv';
@ -27,7 +27,7 @@ export interface AuthContext {
userId: string;
}
export type AuthContextType = 'contributions' | 'ai' | 'general';
export type AuthContextType = 'contributions' | 'ai' | 'general' | 'gatedcontent';
export interface UserInfo {
sub?: string;
@ -260,6 +260,8 @@ function getAuthRequirement(context: AuthContextType): boolean {
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
case 'ai':
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
case 'gatedcontent':
return process.env.AUTHENTICATION_NECESSARY_GATEDCONTENT !== 'false';
default:
return true;
}
@ -386,4 +388,14 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
export function getAuthRequirementForContext(context: AuthContextType): boolean {
return getAuthRequirement(context);
}
// NEW: Helper function to check if gated content requires authentication
export function isGatedContentAuthRequired(): boolean {
return getAuthRequirement('gatedcontent');
}
// NEW: Check if specific content should be gated
export function shouldGateContent(isGatedContent: boolean): boolean {
return isGatedContent && isGatedContentAuthRequired();
}

View File

@ -47,13 +47,7 @@ class EmbeddingsService {
}
private async checkEnabledStatus(): Promise<void> {
try {
console.log('[EMBEDDINGS] Debug env check:', {
AI_EMBEDDINGS_ENABLED: process.env.AI_EMBEDDINGS_ENABLED,
envKeys: Object.keys(process.env).filter(k => k.includes('EMBEDDINGS')).length,
allEnvKeys: Object.keys(process.env).length
});
try {
const envEnabled = process.env.AI_EMBEDDINGS_ENABLED;
if (envEnabled === 'true') {
@ -465,13 +459,4 @@ class EmbeddingsService {
const embeddingsService = new EmbeddingsService();
export { embeddingsService, type EmbeddingData, type SimilarityResult };
export const debugEmbeddings = {
async recheckEnvironment() {
return embeddingsService.forceRecheckEnvironment();
},
getStatus() {
return embeddingsService.getStats();
}
};
export { embeddingsService, type EmbeddingData, type SimilarityResult };