Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
77 / 77 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
| GoogleSafeBrowsingProvider | |
100.00% |
76 / 76 |
|
100.00% |
3 / 3 |
6 | |
100.00% |
1 / 1 |
| getDriver | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| queryRemoteUrl | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
2 | |||
| parseResponse | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Modules\Integrations\Infrastructure\Provider; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Modules\Integrations\Domain\Contract\SecurityCheckResult; |
| 12 | use Throwable; |
| 13 | |
| 14 | /** |
| 15 | * Google Safe Browsing v4 Security Provider for malicious link and phishing detection. |
| 16 | */ |
| 17 | final class GoogleSafeBrowsingProvider extends AbstractUrlReputationProvider |
| 18 | { |
| 19 | private const string PROVIDER_NAME = 'Google Safe Browsing'; |
| 20 | |
| 21 | public function getDriver(): string |
| 22 | { |
| 23 | return 'google_safe_browsing'; |
| 24 | } |
| 25 | |
| 26 | protected function queryRemoteUrl(string $url, ?string $apiKey, array $config): SecurityCheckResult |
| 27 | { |
| 28 | $timeout = (int) ($config['timeout_seconds'] ?? 3); |
| 29 | $endpoint = 'https://safebrowsing.googleapis.com/v4/threatMatches:find?key=' . urlencode((string) $apiKey); |
| 30 | |
| 31 | $payload = json_encode([ |
| 32 | 'client' => [ |
| 33 | 'clientId' => 'ammonly-app-admin', |
| 34 | 'clientVersion' => '1.0.0', |
| 35 | ], |
| 36 | 'threatInfo' => [ |
| 37 | 'threatTypes' => (array) ($config['threat_types'] ?? [ |
| 38 | 'MALWARE', |
| 39 | 'SOCIAL_ENGINEERING', |
| 40 | 'UNWANTED_SOFTWARE', |
| 41 | 'POTENTIALLY_HARMFUL_APPLICATION', |
| 42 | ]), |
| 43 | 'platformTypes' => ['ANY_PLATFORM'], |
| 44 | 'threatEntryTypes' => ['URL'], |
| 45 | 'threatEntries' => [['url' => $url]], |
| 46 | ], |
| 47 | ], JSON_THROW_ON_ERROR); |
| 48 | |
| 49 | try { |
| 50 | $response = $this->httpClient->request( |
| 51 | url: $endpoint, |
| 52 | method: 'POST', |
| 53 | headers: [ |
| 54 | 'Content-Type' => 'application/json', |
| 55 | 'Accept' => 'application/json', |
| 56 | ], |
| 57 | body: $payload, |
| 58 | timeout: $timeout |
| 59 | ); |
| 60 | |
| 61 | return $this->parseResponse($response, $url); |
| 62 | } catch (Throwable $e) { |
| 63 | return new SecurityCheckResult( |
| 64 | type: 'url', |
| 65 | target: $url, |
| 66 | isSafe: true, |
| 67 | status: 'unknown', |
| 68 | summary: "Google Safe Browsing: Connection error ({$e->getMessage()})", |
| 69 | provider: self::PROVIDER_NAME, |
| 70 | details: ['error' => $e->getMessage()], |
| 71 | cachedTtl: 300 |
| 72 | ); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * @param array{status: int, body: string, headers?: array<string, string>} $response |
| 78 | */ |
| 79 | private function parseResponse(array $response, string $url): SecurityCheckResult |
| 80 | { |
| 81 | if ($response['status'] !== 200) { |
| 82 | return new SecurityCheckResult( |
| 83 | type: 'url', |
| 84 | target: $url, |
| 85 | isSafe: true, |
| 86 | status: 'unknown', |
| 87 | summary: "Google Safe Browsing: Service unavailable (HTTP status: {$response['status']})", |
| 88 | provider: self::PROVIDER_NAME, |
| 89 | details: ['http_status' => $response['status']], |
| 90 | cachedTtl: 300 |
| 91 | ); |
| 92 | } |
| 93 | |
| 94 | /** @var array{matches?: array<array<string, mixed>>} $json */ |
| 95 | $json = json_decode($response['body'], true); |
| 96 | $matches = $json['matches'] ?? []; |
| 97 | |
| 98 | if (!empty($matches)) { |
| 99 | $firstThreat = (string) ($matches[0]['threatType'] ?? 'MALWARE'); |
| 100 | return new SecurityCheckResult( |
| 101 | type: 'url', |
| 102 | target: $url, |
| 103 | isSafe: false, |
| 104 | status: 'dangerous', |
| 105 | summary: "Google Safe Browsing: Threat detected in link ({$firstThreat})", |
| 106 | provider: self::PROVIDER_NAME, |
| 107 | details: ['matches' => $matches], |
| 108 | cachedTtl: 86400 |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | return new SecurityCheckResult( |
| 113 | type: 'url', |
| 114 | target: $url, |
| 115 | isSafe: true, |
| 116 | status: 'safe', |
| 117 | summary: 'Google Safe Browsing: Link is verified and clean', |
| 118 | provider: self::PROVIDER_NAME, |
| 119 | details: ['matches' => []], |
| 120 | cachedTtl: 86400 |
| 121 | ); |
| 122 | } |
| 123 | } |