- 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
259 lines
9.2 KiB
PHP
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;
|
|
}
|
|
}
|