Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
GoogleSafeBrowsingProvider
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 getDriver
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 queryRemoteUrl
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
2
 parseResponse
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
3
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\SecurityCheckResult;
12use Throwable;
13
14/**
15 * Google Safe Browsing v4 Security Provider for malicious link and phishing detection.
16 */
17final 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}