Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
AbuseIpDbProvider
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
7 / 7
15
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDriver
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkIp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isMockMode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 queryRemoteIp
100.00% covered (success)
100.00%
63 / 63
100.00% covered (success)
100.00%
1 / 1
6
 checkDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Integrations\Infrastructure\Provider;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Integrations\Domain\Contract\IntegrationProviderInterface;
12use App\Modules\Integrations\Domain\Contract\SecurityCheckResult;
13use App\Modules\Integrations\Infrastructure\Security\SsrfProtectionClientInterface;
14use Throwable;
15
16/**
17 * AbuseIPDB v2 Security Provider for IP reputation, abuse reports, and botnet tracking.
18 */
19final class AbuseIpDbProvider implements IntegrationProviderInterface
20{
21    private const string PROVIDER_NAME = 'AbuseIPDB';
22
23    public function __construct(
24        private readonly SsrfProtectionClientInterface $httpClient,
25        private readonly DemoMockSecurityProvider $mockProvider
26    ) {
27    }
28
29    public function getDriver(): string
30    {
31        return 'abuseipdb';
32    }
33
34    public function checkIp(string $ip, ?string $apiKey, array $config = []): SecurityCheckResult
35    {
36        if ($this->isMockMode($apiKey, $config)) {
37            return $this->mockProvider->checkIp($ip, $apiKey, $config);
38        }
39
40        return $this->queryRemoteIp($ip, $apiKey, $config);
41    }
42
43    private function isMockMode(?string $apiKey, array $config): bool
44    {
45        return !empty($config['mock_mode'])
46            || empty($apiKey)
47            || str_starts_with((string) $apiKey, 'demo_');
48    }
49
50    private function queryRemoteIp(string $ip, ?string $apiKey, array $config): SecurityCheckResult
51    {
52        $threshold = (int) ($config['max_abuse_confidence_threshold'] ?? 25);
53        $timeout = (int) ($config['timeout_seconds'] ?? 3);
54        $endpoint = 'https://api.abuseipdb.com/api/v2/check?ipAddress=' . urlencode($ip) . '&maxAgeInDays=90';
55
56        try {
57            $response = $this->httpClient->request(
58                url: $endpoint,
59                method: 'GET',
60                headers: [
61                    'Key'    => (string) $apiKey,
62                    'Accept' => 'application/json',
63                ],
64                timeout: $timeout
65            );
66
67            if ($response['status'] !== 200) {
68                return new SecurityCheckResult(
69                    type: 'ip',
70                    target: $ip,
71                    isSafe: true,
72                    status: 'unknown',
73                    summary: "AbuseIPDB: Service unavailable (HTTP status: {$response['status']})",
74                    provider: self::PROVIDER_NAME,
75                    details: ['http_status' => $response['status']],
76                    cachedTtl: 300
77                );
78            }
79
80            /** @var array{data?: array<string, mixed>} $json */
81            $json = json_decode($response['body'], true);
82            $data = $json['data'] ?? [];
83            $score = (int) ($data['abuseConfidenceScore'] ?? 0);
84            $totalReports = (int) ($data['totalReports'] ?? 0);
85            $country = (string) ($data['countryCode'] ?? 'Unknown');
86            $isSafe = $score < $threshold;
87
88            $status = 'suspicious';
89            if ($isSafe) {
90                $status = 'safe';
91            } elseif ($score >= 75) {
92                $status = 'dangerous';
93            }
94
95            $summary = $isSafe
96                ? "AbuseIPDB: Sender server is verified and clean (Abuse score: {$score}%, Reports: {$totalReports})"
97                : "AbuseIPDB: Suspicious server detected (Abuse score: {$score}%, Reports: {$totalReports})";
98
99            return new SecurityCheckResult(
100                type: 'ip',
101                target: $ip,
102                isSafe: $isSafe,
103                status: $status,
104                summary: $summary,
105                provider: self::PROVIDER_NAME,
106                details: [
107                    'abuse_confidence_score' => $score,
108                    'total_reports'          => $totalReports,
109                    'country_code'           => $country,
110                    'threshold'              => $threshold,
111                ],
112                cachedTtl: 86400
113            );
114        } catch (Throwable $e) {
115            return new SecurityCheckResult(
116                type: 'ip',
117                target: $ip,
118                isSafe: true,
119                status: 'unknown',
120                summary: "AbuseIPDB: Connection error ({$e->getMessage()})",
121                provider: self::PROVIDER_NAME,
122                details: ['error' => $e->getMessage()],
123                cachedTtl: 300
124            );
125        }
126    }
127
128    public function checkDomain(string $domain, ?string $apiKey, array $config = []): SecurityCheckResult
129    {
130        return $this->mockProvider->checkDomain($domain, $apiKey, $config);
131    }
132
133    public function checkUrl(string $url, ?string $apiKey, array $config = []): SecurityCheckResult
134    {
135        return $this->mockProvider->checkUrl($url, $apiKey, $config);
136    }
137}