Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
UrlHausProvider
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
3 / 3
8
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%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 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 * abuse.ch URLhaus Security Provider for community-driven malware URL checking.
16 */
17final class UrlHausProvider extends AbstractUrlReputationProvider
18{
19    private const string PROVIDER_NAME = 'URLhaus (abuse.ch)';
20
21    public function getDriver(): string
22    {
23        return 'urlhaus';
24    }
25
26    protected function queryRemoteUrl(string $url, ?string $apiKey, array $config): SecurityCheckResult
27    {
28        $timeout = (int) ($config['timeout_seconds'] ?? 3);
29        $endpoint = 'https://urlhaus-api.abuse.ch/v1/url/';
30
31        try {
32            $headers = ['Accept' => 'application/json'];
33            if (!empty($apiKey) && !str_starts_with($apiKey, 'demo_')) {
34                $headers['Auth-Key'] = $apiKey;
35            }
36
37            $response = $this->httpClient->request(
38                url: $endpoint,
39                method: 'POST',
40                headers: $headers,
41                body: http_build_query(['url' => $url]),
42                timeout: $timeout
43            );
44
45            return $this->parseResponse($response, $url);
46        } catch (Throwable $e) {
47            return new SecurityCheckResult(
48                type: 'url',
49                target: $url,
50                isSafe: true,
51                status: 'unknown',
52                summary: "URLhaus: Connection error ({$e->getMessage()})",
53                provider: self::PROVIDER_NAME,
54                details: ['error' => $e->getMessage()],
55                cachedTtl: 300
56            );
57        }
58    }
59
60    /**
61     * @param array{status: int, body: string, headers?: array<string, string>} $response
62     */
63    private function parseResponse(array $response, string $url): SecurityCheckResult
64    {
65        if ($response['status'] !== 200) {
66            return new SecurityCheckResult(
67                type: 'url',
68                target: $url,
69                isSafe: true,
70                status: 'unknown',
71                summary: "URLhaus: Service unavailable (HTTP status: {$response['status']})",
72                provider: self::PROVIDER_NAME,
73                details: ['http_status' => $response['status']],
74                cachedTtl: 300
75            );
76        }
77
78        /** @var array{query_status?: string, url_status?: string, threat?: string} $json */
79        $json = json_decode($response['body'], true);
80        $queryStatus = (string) ($json['query_status'] ?? 'no_results');
81
82        if ($queryStatus === 'ok') {
83            $threat = (string) ($json['threat'] ?? 'malware_download');
84            return new SecurityCheckResult(
85                type: 'url',
86                target: $url,
87                isSafe: false,
88                status: 'dangerous',
89                summary: "URLhaus: Malware distribution detected ({$threat})",
90                provider: self::PROVIDER_NAME,
91                details: $json,
92                cachedTtl: 86400
93            );
94        }
95
96        return new SecurityCheckResult(
97            type: 'url',
98            target: $url,
99            isSafe: true,
100            status: 'safe',
101            summary: 'URLhaus: No threats found in abuse.ch malware database',
102            provider: self::PROVIDER_NAME,
103            details: ['query_status' => $queryStatus],
104            cachedTtl: 86400
105        );
106    }
107}