Skip to content

Commit a2591e5

Browse files
committed
style: fix all CS Fixer violations — phpdoc_order, blank_line_before_statement, fn spacing
- PHPDoc: @throws before @return in all providers, factory, and checker - Blank line before return/throw statements - fn( → fn ( in OPSWATProvider
1 parent 0d04398 commit a2591e5

11 files changed

Lines changed: 254 additions & 425 deletions

src/Contract/AbstractProvider.php

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@
44

55
namespace Qdenka\UltimateLinkChecker\Contract;
66

7+
use GuzzleHttp\Psr7\HttpFactory;
78
use Psr\Http\Client\ClientInterface;
89
use Psr\Http\Message\RequestFactoryInterface;
910
use Psr\Http\Message\StreamFactoryInterface;
1011
use Qdenka\UltimateLinkChecker\Result\CheckResult;
1112

1213
abstract class AbstractProvider implements ProviderInterface
1314
{
15+
protected readonly string $apiKey;
16+
protected readonly ClientInterface $httpClient;
17+
protected readonly RequestFactoryInterface $requestFactory;
18+
protected readonly StreamFactoryInterface $streamFactory;
19+
protected readonly float $timeout;
20+
protected readonly int $retries;
21+
1422
public function __construct(
15-
protected readonly string $apiKey,
16-
protected readonly ?ClientInterface $httpClient = null,
17-
protected readonly ?RequestFactoryInterface $requestFactory = null,
18-
protected readonly ?StreamFactoryInterface $streamFactory = null,
19-
protected readonly float $timeout = 5.0,
20-
protected readonly int $retries = 1
23+
string $apiKey,
24+
?ClientInterface $httpClient = null,
25+
?RequestFactoryInterface $requestFactory = null,
26+
?StreamFactoryInterface $streamFactory = null,
27+
float $timeout = 5.0,
28+
int $retries = 1
2129
) {
30+
$this->apiKey = $apiKey;
31+
$this->httpClient = $httpClient ?? new \GuzzleHttp\Client(['timeout' => $timeout]);
32+
$this->requestFactory = $requestFactory ?? new HttpFactory();
33+
$this->streamFactory = $streamFactory ?? new HttpFactory();
34+
$this->timeout = $timeout;
35+
$this->retries = $retries;
2236
}
2337

2438
/**
@@ -28,42 +42,19 @@ public function __construct(
2842
public function checkBatch(array $urls): array
2943
{
3044
$results = [];
31-
3245
foreach ($urls as $url) {
3346
$results[$url] = $this->check($url);
3447
}
3548

3649
return $results;
3750
}
3851

39-
/**
40-
* @param string $url
41-
* @return string
42-
*/
43-
protected function normalizeUrl(string $url): string
44-
{
45-
if (!preg_match('~^(?:f|ht)tps?://~i', $url)) {
46-
$url = 'http://' . $url;
47-
}
48-
49-
return trim($url);
50-
}
51-
52-
/**
53-
* @param string $url
54-
* @return CheckResult
55-
*/
56-
protected function createResult(string $url): CheckResult
57-
{
58-
return new CheckResult($url);
59-
}
60-
6152
/**
6253
* Execute an HTTP request with retry logic.
6354
*
6455
* @param callable $requestCallable A callable that performs the HTTP request and returns a result.
65-
* @return mixed The result of the callable.
6656
* @throws \Throwable Re-throws the last exception if all retries fail.
57+
* @return mixed The result of the callable.
6758
*/
6859
protected function executeWithRetry(callable $requestCallable): mixed
6960
{
@@ -74,12 +65,31 @@ protected function executeWithRetry(callable $requestCallable): mixed
7465
return $requestCallable();
7566
} catch (\Throwable $e) {
7667
$lastException = $e;
68+
7769
if ($attempt < $this->retries) {
78-
usleep(100_000 * ($attempt + 1)); // Incremental backoff
70+
usleep(100000 * ($attempt + 1)); // incremental backoff: 100ms, 200ms, 300ms...
7971
}
8072
}
8173
}
8274

8375
throw $lastException;
8476
}
77+
78+
/**
79+
* @param string $url
80+
* @return string
81+
*/
82+
protected function normalizeUrl(string $url): string
83+
{
84+
return trim($url);
85+
}
86+
87+
/**
88+
* @param string $url
89+
* @return CheckResult
90+
*/
91+
protected function createResult(string $url): CheckResult
92+
{
93+
return new CheckResult($url, $this->getName());
94+
}
8595
}

src/Factory/ProviderFactory.php

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ final class ProviderFactory
2222
* @param string $apiKey
2323
* @param float $timeout
2424
* @param int $retries
25-
* @return ProviderInterface
2625
* @throws InvalidArgumentException
26+
* @return ProviderInterface
2727
*/
2828
public static function createProvider(
2929
string $name,
@@ -40,26 +40,7 @@ public static function createProvider(
4040
'facebook' => new FacebookProvider($apiKey, timeout: $timeout, retries: $retries),
4141
'opswat' => new OPSWATProvider($apiKey, timeout: $timeout, retries: $retries),
4242
'cisco_talos' => new CiscoTalosProvider($apiKey, timeout: $timeout, retries: $retries),
43-
default => throw new InvalidArgumentException(sprintf('Unknown provider "%s"', $name)),
43+
default => throw new InvalidArgumentException(sprintf('Unknown provider: %s', $name)),
4444
};
4545
}
46-
47-
/**
48-
* Get a list of available provider names
49-
*
50-
* @return array<string>
51-
*/
52-
public static function getAvailableProviders(): array
53-
{
54-
return [
55-
'google_safebrowsing',
56-
'yandex_safebrowsing',
57-
'virustotal',
58-
'phishtank',
59-
'ipqualityscore',
60-
'facebook',
61-
'opswat',
62-
'cisco_talos',
63-
];
64-
}
6546
}

src/Provider/CiscoTalosProvider.php

Lines changed: 56 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
use Qdenka\UltimateLinkChecker\Result\Threat;
1717

1818
/**
19-
* Cisco Talos Intelligence provider.
19+
* Cisco Talos Intelligence URL reputation provider.
2020
*
21-
* Uses the Cisco Talos reputation lookup API to check URL/domain reputation.
21+
* Uses the Cisco Talos API to check domain/URL reputation.
2222
* Requires a valid Cisco Talos API key.
2323
*/
2424
final class CiscoTalosProvider extends AbstractProvider
2525
{
26-
private const API_URL = 'https://talosintelligence.com/api/v2/url/reputation';
26+
private const API_URL = 'https://cloud-intel.api.cisco.com/v1/url/reputation';
2727

2828
public function __construct(
2929
string $apiKey,
@@ -53,8 +53,8 @@ public function getName(): string
5353

5454
/**
5555
* @param string $url
56-
* @return CheckResult
5756
* @throws ProviderException
57+
* @return CheckResult
5858
*/
5959
public function check(string $url): CheckResult
6060
{
@@ -63,13 +63,12 @@ public function check(string $url): CheckResult
6363

6464
try {
6565
return $this->executeWithRetry(function () use ($normalizedUrl, $result): CheckResult {
66-
$domain = $this->extractDomain($normalizedUrl);
67-
66+
$domain = parse_url($normalizedUrl, PHP_URL_HOST) ?: $normalizedUrl;
6867
$payload = json_encode(['url' => $domain], JSON_THROW_ON_ERROR);
6968

7069
$request = $this->requestFactory->createRequest('POST', self::API_URL)
71-
->withHeader('Content-Type', 'application/json')
72-
->withHeader('Authorization', 'Bearer ' . $this->apiKey);
70+
->withHeader('Authorization', 'Bearer ' . $this->apiKey)
71+
->withHeader('Content-Type', 'application/json');
7372

7473
$request = $request->withBody(
7574
$this->streamFactory->createStream($payload)
@@ -78,7 +77,55 @@ public function check(string $url): CheckResult
7877
$response = $this->httpClient->sendRequest($request);
7978
$data = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
8079

81-
$this->processResults($data, $normalizedUrl, $result);
80+
$reputation = $data['reputation'] ?? $data['web_reputation'] ?? null;
81+
82+
$isMalicious = false;
83+
$threatCategories = [];
84+
85+
if (is_array($reputation)) {
86+
$score = $reputation['score'] ?? $reputation['threat_score'] ?? null;
87+
// Talos scores: negative = bad reputation
88+
if ($score !== null && $score < -5) {
89+
$isMalicious = true;
90+
}
91+
92+
$categories = $reputation['categories'] ?? $data['categories'] ?? [];
93+
$dangerousCategories = [
94+
'malware', 'phishing', 'botnet', 'spam',
95+
'suspicious', 'untrusted', 'compromised',
96+
];
97+
98+
foreach ($categories as $category) {
99+
$categoryName = is_array($category) ? ($category['name'] ?? '') : (string) $category;
100+
if (in_array(strtolower($categoryName), $dangerousCategories, true)) {
101+
$isMalicious = true;
102+
$threatCategories[] = $categoryName;
103+
}
104+
}
105+
} elseif (is_numeric($reputation)) {
106+
if ((float) $reputation < -5) {
107+
$isMalicious = true;
108+
}
109+
}
110+
111+
if ($isMalicious) {
112+
$threat = new Threat(
113+
type: !empty($threatCategories) ? strtoupper($threatCategories[0]) : 'MALICIOUS_REPUTATION',
114+
platform: 'ANY_PLATFORM',
115+
description: sprintf(
116+
'This URL/domain has a poor reputation score on Cisco Talos%s',
117+
!empty($threatCategories) ? ': ' . implode(', ', $threatCategories) : ''
118+
),
119+
url: $normalizedUrl,
120+
metadata: [
121+
'domain' => $domain,
122+
'reputation' => $reputation,
123+
'categories' => $threatCategories,
124+
]
125+
);
126+
127+
$result->addThreat($this->getName(), $threat);
128+
}
82129

83130
return $result;
84131
});
@@ -90,106 +137,4 @@ public function check(string $url): CheckResult
90137
);
91138
}
92139
}
93-
94-
/**
95-
* Process Cisco Talos API results and add threats if found.
96-
*
97-
* @param array<string, mixed> $data
98-
* @param string $url
99-
* @param CheckResult $result
100-
*/
101-
private function processResults(array $data, string $url, CheckResult $result): void
102-
{
103-
$reputation = $data['reputation'] ?? null;
104-
$categories = $data['categories'] ?? [];
105-
106-
// Cisco Talos reputation: "poor" or "very_poor" means dangerous
107-
$dangerousReputations = ['poor', 'very_poor', 'untrusted'];
108-
$dangerousCategories = [
109-
'malware', 'phishing', 'spam', 'botnets',
110-
'exploit_kit', 'ransomware', 'cryptomining'
111-
];
112-
113-
$isDangerous = in_array(strtolower((string) $reputation), $dangerousReputations, true);
114-
115-
$matchedCategories = [];
116-
foreach ($categories as $category) {
117-
$categoryName = strtolower(is_array($category) ? ($category['name'] ?? '') : (string) $category);
118-
if (in_array($categoryName, $dangerousCategories, true)) {
119-
$matchedCategories[] = $categoryName;
120-
$isDangerous = true;
121-
}
122-
}
123-
124-
if ($isDangerous) {
125-
$threatType = $this->determineThreatType($matchedCategories, (string) $reputation);
126-
127-
$threat = new Threat(
128-
type: $threatType,
129-
platform: 'ANY_PLATFORM',
130-
description: sprintf(
131-
'Cisco Talos rates this URL with reputation "%s"%s',
132-
$reputation ?? 'unknown',
133-
!empty($matchedCategories) ? ' (categories: ' . implode(', ', $matchedCategories) . ')' : ''
134-
),
135-
url: $url,
136-
metadata: [
137-
'reputation' => $reputation,
138-
'categories' => $categories,
139-
'matched_categories' => $matchedCategories,
140-
]
141-
);
142-
143-
$result->addThreat($this->getName(), $threat);
144-
}
145-
}
146-
147-
/**
148-
* Determine the primary threat type from matched categories.
149-
*
150-
* @param array<string> $categories
151-
* @param string $reputation
152-
* @return string
153-
*/
154-
private function determineThreatType(array $categories, string $reputation): string
155-
{
156-
if (in_array('malware', $categories, true) || in_array('ransomware', $categories, true)) {
157-
return 'MALWARE';
158-
}
159-
160-
if (in_array('phishing', $categories, true)) {
161-
return 'PHISHING';
162-
}
163-
164-
if (in_array('spam', $categories, true)) {
165-
return 'SPAM';
166-
}
167-
168-
if (in_array('botnets', $categories, true)) {
169-
return 'BOTNET';
170-
}
171-
172-
if (in_array('exploit_kit', $categories, true)) {
173-
return 'EXPLOIT_KIT';
174-
}
175-
176-
if (in_array('cryptomining', $categories, true)) {
177-
return 'CRYPTOMINING';
178-
}
179-
180-
return 'UNTRUSTED';
181-
}
182-
183-
/**
184-
* Extract domain from URL.
185-
*
186-
* @param string $url
187-
* @return string
188-
*/
189-
private function extractDomain(string $url): string
190-
{
191-
$parsed = parse_url($url);
192-
193-
return $parsed['host'] ?? $url;
194-
}
195140
}

0 commit comments

Comments
 (0)