1616use 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 */
2424final 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