- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
262 lines
8.7 KiB
PHP
262 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Waf\BotProtection\ValueObjects;
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
/**
|
|
* Browser fingerprint with anomaly detection
|
|
*/
|
|
final readonly class BrowserFingerprint
|
|
{
|
|
public function __construct(
|
|
public string $fingerprintHash,
|
|
public array $features,
|
|
public array $anomalousFeatures,
|
|
public float $anomalyScore,
|
|
public Timestamp $createdAt,
|
|
public ?string $userAgent = null,
|
|
public ?string $acceptLanguage = null,
|
|
public ?string $acceptEncoding = null,
|
|
public ?array $canvasFingerprint = null,
|
|
public ?array $webglFingerprint = null,
|
|
public ?array $audioFingerprint = null,
|
|
public ?array $fontList = null,
|
|
public ?array $screenResolution = null,
|
|
public ?array $timezoneInfo = null,
|
|
public ?array $pluginList = null,
|
|
public bool $touchSupport = false,
|
|
public ?int $colorDepth = null,
|
|
public ?int $pixelRatio = null
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Create from raw fingerprint data
|
|
*/
|
|
public static function fromData(array $data, Timestamp $timestamp): self
|
|
{
|
|
$features = self::extractFeatures($data);
|
|
$anomalousFeatures = self::detectAnomalies($features);
|
|
$anomalyScore = self::calculateAnomalyScore($anomalousFeatures, $features);
|
|
|
|
return new self(
|
|
fingerprintHash: self::generateHash($features),
|
|
features: $features,
|
|
anomalousFeatures: $anomalousFeatures,
|
|
anomalyScore: $anomalyScore,
|
|
createdAt: $timestamp,
|
|
userAgent: $data['user_agent'] ?? null,
|
|
acceptLanguage: $data['accept_language'] ?? null,
|
|
acceptEncoding: $data['accept_encoding'] ?? null,
|
|
canvasFingerprint: $data['canvas'] ?? null,
|
|
webglFingerprint: $data['webgl'] ?? null,
|
|
audioFingerprint: $data['audio'] ?? null,
|
|
fontList: $data['fonts'] ?? null,
|
|
screenResolution: $data['screen'] ?? null,
|
|
timezoneInfo: $data['timezone'] ?? null,
|
|
pluginList: $data['plugins'] ?? null,
|
|
touchSupport: $data['touch_support'] ?? false,
|
|
colorDepth: $data['color_depth'] ?? null,
|
|
pixelRatio: $data['pixel_ratio'] ?? null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if fingerprint is anomalous
|
|
*/
|
|
public function isAnomalous(): bool
|
|
{
|
|
return $this->anomalyScore > 70.0;
|
|
}
|
|
|
|
/**
|
|
* Check if fingerprint is highly suspicious
|
|
*/
|
|
public function isHighlySuspicious(): bool
|
|
{
|
|
return $this->anomalyScore > 90.0;
|
|
}
|
|
|
|
/**
|
|
* Get anomaly score
|
|
*/
|
|
public function getAnomalyScore(): float
|
|
{
|
|
return $this->anomalyScore;
|
|
}
|
|
|
|
/**
|
|
* Get anomalous features
|
|
*/
|
|
public function getAnomalousFeatures(): array
|
|
{
|
|
return $this->anomalousFeatures;
|
|
}
|
|
|
|
/**
|
|
* Check if fingerprint matches another
|
|
*/
|
|
public function matches(self $other): bool
|
|
{
|
|
return $this->fingerprintHash === $other->fingerprintHash;
|
|
}
|
|
|
|
/**
|
|
* Calculate similarity with another fingerprint
|
|
*/
|
|
public function similarity(self $other): float
|
|
{
|
|
$commonFeatures = array_intersect_key($this->features, $other->features);
|
|
$totalFeatures = array_merge($this->features, $other->features);
|
|
|
|
if (empty($totalFeatures)) {
|
|
return 0.0;
|
|
}
|
|
|
|
$matchingFeatures = 0;
|
|
foreach ($commonFeatures as $key => $value) {
|
|
if ($this->features[$key] === $other->features[$key]) {
|
|
$matchingFeatures++;
|
|
}
|
|
}
|
|
|
|
return ($matchingFeatures / count($totalFeatures)) * 100;
|
|
}
|
|
|
|
/**
|
|
* Extract features from raw data
|
|
*/
|
|
private static function extractFeatures(array $data): array
|
|
{
|
|
return [
|
|
'user_agent_hash' => hash('sha256', $data['user_agent'] ?? ''),
|
|
'language' => $data['accept_language'] ?? null,
|
|
'encoding' => $data['accept_encoding'] ?? null,
|
|
'canvas_hash' => isset($data['canvas']) ? hash('sha256', json_encode($data['canvas'])) : null,
|
|
'webgl_hash' => isset($data['webgl']) ? hash('sha256', json_encode($data['webgl'])) : null,
|
|
'audio_hash' => isset($data['audio']) ? hash('sha256', json_encode($data['audio'])) : null,
|
|
'font_count' => isset($data['fonts']) ? count($data['fonts']) : null,
|
|
'screen_signature' => isset($data['screen']) ? implode('x', $data['screen']) : null,
|
|
'timezone_offset' => $data['timezone']['offset'] ?? null,
|
|
'plugin_count' => isset($data['plugins']) ? count($data['plugins']) : null,
|
|
'touch_support' => $data['touch_support'] ?? false,
|
|
'color_depth' => $data['color_depth'] ?? null,
|
|
'pixel_ratio' => $data['pixel_ratio'] ?? null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Detect anomalies in features
|
|
*/
|
|
private static function detectAnomalies(array $features): array
|
|
{
|
|
$anomalies = [];
|
|
|
|
// Check for common bot indicators
|
|
if (isset($features['user_agent_hash']) && self::isSuspiciousUserAgent($features['user_agent_hash'])) {
|
|
$anomalies['suspicious_user_agent'] = 'User agent matches known bot patterns';
|
|
}
|
|
|
|
if (isset($features['font_count']) && ($features['font_count'] < 10 || $features['font_count'] > 500)) {
|
|
$anomalies['unusual_font_count'] = "Unusual font count: {$features['font_count']}";
|
|
}
|
|
|
|
if (isset($features['plugin_count']) && $features['plugin_count'] === 0) {
|
|
$anomalies['no_plugins'] = 'No browser plugins detected';
|
|
}
|
|
|
|
if (! isset($features['canvas_hash']) || ! isset($features['webgl_hash'])) {
|
|
$anomalies['missing_fingerprinting'] = 'Canvas or WebGL fingerprinting blocked';
|
|
}
|
|
|
|
if (isset($features['screen_signature'])) {
|
|
$commonResolutions = ['1920x1080', '1366x768', '1440x900', '1280x720'];
|
|
if (! in_array($features['screen_signature'], $commonResolutions)) {
|
|
$anomalies['unusual_resolution'] = "Unusual screen resolution: {$features['screen_signature']}";
|
|
}
|
|
}
|
|
|
|
return $anomalies;
|
|
}
|
|
|
|
/**
|
|
* Calculate anomaly score
|
|
*/
|
|
private static function calculateAnomalyScore(array $anomalies, array $features): float
|
|
{
|
|
$score = 0.0;
|
|
|
|
// Base score from anomaly count
|
|
$score += count($anomalies) * 20;
|
|
|
|
// Weight specific anomalies
|
|
$weights = [
|
|
'suspicious_user_agent' => 30,
|
|
'unusual_font_count' => 15,
|
|
'no_plugins' => 25,
|
|
'missing_fingerprinting' => 35,
|
|
'unusual_resolution' => 10,
|
|
];
|
|
|
|
foreach ($anomalies as $type => $description) {
|
|
$score += $weights[$type] ?? 10;
|
|
}
|
|
|
|
// Cap at 100
|
|
return min(100.0, $score);
|
|
}
|
|
|
|
/**
|
|
* Check if user agent is suspicious
|
|
*/
|
|
private static function isSuspiciousUserAgent(string $userAgentHash): bool
|
|
{
|
|
// In a real implementation, this would check against a database of known bot user agents
|
|
$knownBotHashes = [
|
|
// Common bot user agent hashes would be stored here
|
|
];
|
|
|
|
return in_array($userAgentHash, $knownBotHashes);
|
|
}
|
|
|
|
/**
|
|
* Generate fingerprint hash
|
|
*/
|
|
private static function generateHash(array $features): string
|
|
{
|
|
ksort($features);
|
|
|
|
return hash('sha256', json_encode($features));
|
|
}
|
|
|
|
/**
|
|
* Convert to array
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'fingerprint_hash' => $this->fingerprintHash,
|
|
'anomaly_score' => $this->anomalyScore,
|
|
'is_anomalous' => $this->isAnomalous(),
|
|
'is_highly_suspicious' => $this->isHighlySuspicious(),
|
|
'anomalous_features' => $this->anomalousFeatures,
|
|
'created_at' => $this->createdAt->toIsoString(),
|
|
'user_agent' => $this->userAgent,
|
|
'accept_language' => $this->acceptLanguage,
|
|
'canvas_available' => $this->canvasFingerprint !== null,
|
|
'webgl_available' => $this->webglFingerprint !== null,
|
|
'audio_available' => $this->audioFingerprint !== null,
|
|
'font_count' => $this->fontList ? count($this->fontList) : null,
|
|
'screen_resolution' => $this->screenResolution,
|
|
'timezone_info' => $this->timezoneInfo,
|
|
'plugin_count' => $this->pluginList ? count($this->pluginList) : null,
|
|
'touch_support' => $this->touchSupport,
|
|
'color_depth' => $this->colorDepth,
|
|
'pixel_ratio' => $this->pixelRatio,
|
|
];
|
|
}
|
|
}
|