Files
michaelschiemer/src/Application/Security/WafFeedbackDashboardController.php
Michael Schiemer e30753ba0e fix: resolve RedisCache array offset error and improve discovery diagnostics
- Fix RedisCache driver to handle MGET failures gracefully with fallback
- Add comprehensive discovery context comparison debug tools
- Identify root cause: WEB context discovery missing 166 items vs CLI
- WEB context missing RequestFactory class entirely (52 vs 69 commands)
- Improved exception handling with detailed binding diagnostics
2025-09-12 20:05:18 +02:00

259 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Security;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\Feedback\FeedbackRepositoryInterface;
use App\Framework\Waf\Feedback\FeedbackService;
use App\Framework\Waf\Feedback\FeedbackType;
/**
* Controller for the WAF feedback dashboard
*/
final readonly class WafFeedbackDashboardController
{
/**
* @param FeedbackRepositoryInterface $repository Repository for accessing feedback data
* @param FeedbackService $feedbackService Service for handling WAF feedback
*/
public function __construct(
private FeedbackRepositoryInterface $repository,
private FeedbackService $feedbackService
) {
}
/**
* Show the WAF feedback dashboard
*/
#[Route(path: '/admin/security/waf/feedback', method: Method::GET)]
public function showDashboard(HttpRequest $request): ViewResult
{
// Get feedback statistics
$stats = $this->repository->getFeedbackStats();
// Get recent feedback
$recentFeedback = $this->repository->getRecentFeedback(10);
// Get time period from query parameters (default to last 30 days)
$period = $request->queryParams['period'] ?? '30d';
$since = $this->getPeriodTimestamp($period);
// Get feedback by type for the selected period
$falsePositives = $this->repository->getFeedbackByFeedbackType(
FeedbackType::FALSE_POSITIVE,
$since
);
$falseNegatives = $this->repository->getFeedbackByFeedbackType(
FeedbackType::FALSE_NEGATIVE,
$since
);
$correctDetections = $this->repository->getFeedbackByFeedbackType(
FeedbackType::CORRECT_DETECTION,
$since
);
$severityAdjustments = $this->repository->getFeedbackByFeedbackType(
FeedbackType::SEVERITY_ADJUSTMENT,
$since
);
// Calculate accuracy metrics
$totalFeedback = count($falsePositives) + count($falseNegatives) + count($correctDetections);
$accuracy = $totalFeedback > 0
? (count($correctDetections) / $totalFeedback) * 100
: 0;
$falsePositiveRate = $totalFeedback > 0
? (count($falsePositives) / $totalFeedback) * 100
: 0;
$falseNegativeRate = $totalFeedback > 0
? (count($falseNegatives) / $totalFeedback) * 100
: 0;
// Get feedback by category
$feedbackByCategory = $this->getFeedbackByCategory($since);
// Get trend data
$trendData = $stats['trend_data'] ?? [];
return new ViewResult('admin/security/waf-feedback-dashboard', [
'stats' => $stats,
'recent_feedback' => $recentFeedback,
'period' => $period,
'accuracy' => round($accuracy, 1),
'false_positive_rate' => round($falsePositiveRate, 1),
'false_negative_rate' => round($falseNegativeRate, 1),
'feedback_by_category' => $feedbackByCategory,
'trend_data' => $trendData,
'false_positives_count' => count($falsePositives),
'false_negatives_count' => count($falseNegatives),
'correct_detections_count' => count($correctDetections),
'severity_adjustments_count' => count($severityAdjustments),
]);
}
/**
* Show detailed feedback for a specific category
*/
#[Route(path: '/admin/security/waf/feedback/category/{category}', method: Method::GET)]
public function showCategoryFeedback(HttpRequest $request, string $category): ViewResult
{
try {
$detectionCategory = DetectionCategory::from($category);
} catch (\ValueError $e) {
// If category is invalid, redirect to dashboard
return new ViewResult('admin/security/waf-feedback-dashboard', [
'error' => 'Invalid category: ' . $category,
]);
}
// Get time period from query parameters (default to last 30 days)
$period = $request->queryParams['period'] ?? '30d';
$since = $this->getPeriodTimestamp($period);
// Get feedback for this category
$feedback = $this->repository->getFeedbackByCategory($detectionCategory, $since);
// Group feedback by type
$feedbackByType = [];
foreach ($feedback as $item) {
$type = $item->feedbackType->value;
if (! isset($feedbackByType[$type])) {
$feedbackByType[$type] = [];
}
$feedbackByType[$type][] = $item;
}
// Calculate accuracy metrics for this category
$falsePositives = $feedbackByType[FeedbackType::FALSE_POSITIVE->value] ?? [];
$falseNegatives = $feedbackByType[FeedbackType::FALSE_NEGATIVE->value] ?? [];
$correctDetections = $feedbackByType[FeedbackType::CORRECT_DETECTION->value] ?? [];
$totalFeedback = count($falsePositives) + count($falseNegatives) + count($correctDetections);
$accuracy = $totalFeedback > 0
? (count($correctDetections) / $totalFeedback) * 100
: 0;
$falsePositiveRate = $totalFeedback > 0
? (count($falsePositives) / $totalFeedback) * 100
: 0;
$falseNegativeRate = $totalFeedback > 0
? (count($falseNegatives) / $totalFeedback) * 100
: 0;
return new ViewResult('admin/security/waf-feedback-category', [
'category' => $detectionCategory,
'feedback' => $feedback,
'feedback_by_type' => $feedbackByType,
'period' => $period,
'accuracy' => round($accuracy, 1),
'false_positive_rate' => round($falsePositiveRate, 1),
'false_negative_rate' => round($falseNegativeRate, 1),
'total_feedback' => $totalFeedback,
]);
}
/**
* Show feedback learning history
*/
#[Route(path: '/admin/security/waf/feedback/learning', method: Method::GET)]
public function showLearningHistory(HttpRequest $request): ViewResult
{
// In a real implementation, this would retrieve learning history from a database
// For now, we'll return a placeholder view
return new ViewResult('admin/security/waf-feedback-learning', [
'learning_history' => [],
'message' => 'Learning history not yet implemented',
]);
}
/**
* Get a timestamp for the specified period
*
* @param string $period Period string (e.g., '7d', '30d', '90d', 'all')
* @return Timestamp|null Timestamp for the start of the period, or null for 'all'
*/
private function getPeriodTimestamp(string $period): ?Timestamp
{
return match($period) {
'7d' => Timestamp::fromString('-7 days'),
'30d' => Timestamp::fromString('-30 days'),
'90d' => Timestamp::fromString('-90 days'),
'all' => null,
default => Timestamp::fromString('-30 days')
};
}
/**
* Get feedback grouped by category
*
* @param Timestamp|null $since Optional timestamp to filter feedback after a certain date
* @return array<string, array<string, mixed>> Feedback data grouped by category
*/
private function getFeedbackByCategory(?Timestamp $since): array
{
$result = [];
// Get all categories
$categories = DetectionCategory::cases();
foreach ($categories as $category) {
// Get feedback for this category
$feedback = $this->repository->getFeedbackByCategory($category, $since);
if (empty($feedback)) {
continue;
}
// Group feedback by type
$feedbackByType = [];
foreach ($feedback as $item) {
$type = $item->feedbackType->value;
if (! isset($feedbackByType[$type])) {
$feedbackByType[$type] = [];
}
$feedbackByType[$type][] = $item;
}
// Calculate metrics
$falsePositives = $feedbackByType[FeedbackType::FALSE_POSITIVE->value] ?? [];
$falseNegatives = $feedbackByType[FeedbackType::FALSE_NEGATIVE->value] ?? [];
$correctDetections = $feedbackByType[FeedbackType::CORRECT_DETECTION->value] ?? [];
$totalFeedback = count($falsePositives) + count($falseNegatives) + count($correctDetections);
if ($totalFeedback === 0) {
continue;
}
$accuracy = (count($correctDetections) / $totalFeedback) * 100;
$result[$category->value] = [
'category' => $category,
'total_feedback' => $totalFeedback,
'false_positives' => count($falsePositives),
'false_negatives' => count($falseNegatives),
'correct_detections' => count($correctDetections),
'accuracy' => round($accuracy, 1),
];
}
// Sort by total feedback count (descending)
uasort($result, fn ($a, $b) => $b['total_feedback'] <=> $a['total_feedback']);
return $result;
}
}