22
22
def mock_debug (message ):
23
23
"""Print debug messages to the XSOAR logs"""
24
24
print (f"DEBUG: { message } " )
25
-
26
-
27
25
demisto .debug = mock_debug
28
26
29
27
''' CONSTANTS '''
@@ -105,42 +103,152 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data:
105
103
except Exception as e :
106
104
demisto .error (f'Error in API call: { str (e )} ' )
107
105
raise
108
-
109
- def list_domain_information (self , domain : str ) -> dict :
106
+
107
+ def list_domain_information (self , domains : Union [ str , List [ str ]] ) -> Dict :
110
108
"""
111
- Fetches domain information such as WHOIS data, domain age, and risk scores.
109
+ Fetches domain information including WHOIS data and risk scores for multiple domains .
112
110
113
111
Args:
114
- domain (str): The domain to fetch information for.
115
-
112
+ domains: Either a single domain string or a list of domain strings
113
+
116
114
Returns:
117
- dict : A dictionary containing domain information fetched from the API.
115
+ Dict : A dictionary containing combined domain information and risk scores
118
116
"""
119
- demisto .debug (f'Fetching domain information for domain: { domain } ' )
120
- url_suffix = f'explore/domain/domaininfo/{ domain } '
121
- return self ._http_request ('GET' , url_suffix )
117
+ demisto .debug (f'Fetching domain information for: { domains } ' )
118
+ domain_list = [domains ] if isinstance (domains , str ) else domains
119
+
120
+ if len (domain_list ) > 100 :
121
+ raise DemistoException ("Maximum of 100 domains can be submitted in a single request" )
122
+
123
+ if len (domain_list ) == 1 :
122
124
125
+ domain = domain_list [0 ]
126
+ try :
127
+
128
+ domain_info_response = self ._http_request (
129
+ method = 'GET' ,
130
+ url_suffix = f'explore/domain/domaininfo/{ domain } '
131
+ )
132
+ domain_info = domain_info_response .get ('response' , {}).get ('domaininfo' , {})
133
+
134
+ risk_score_response = self ._http_request (
135
+ method = 'GET' ,
136
+ url_suffix = f'explore/domain/riskscore/{ domain } '
137
+ )
138
+ risk_info = risk_score_response .get ('response' , {})
139
+
140
+ combined_info = {
141
+ 'domain' : domain ,
142
+ ** domain_info ,
143
+ 'sp_risk_score' : risk_info .get ('sp_risk_score' ),
144
+ 'sp_risk_score_explain' : risk_info .get ('sp_risk_score_explain' )
145
+ }
146
+
147
+ return {'domains' : [combined_info ]}
148
+
149
+ except Exception as e :
150
+ raise DemistoException (f'Failed to fetch information for domain { domain } : { str (e )} ' )
151
+
152
+ else :
153
+
154
+ try :
155
+
156
+ domains_data = {'domains' : domain_list }
157
+ bulk_info_response = self ._http_request (
158
+ method = 'POST' ,
159
+ url_suffix = 'explore/bulk/domaininfo' ,
160
+ data = domains_data
161
+ )
162
+
163
+ bulk_risk_response = self ._http_request (
164
+ method = 'POST' ,
165
+ url_suffix = 'explore/bulk/domain/riskscore' ,
166
+ data = domains_data
167
+ )
168
+
169
+ domain_info_list = bulk_info_response .get ('response' , {}).get ('domaininfo' , [])
170
+ risk_score_list = bulk_risk_response .get ('response' , [])
171
+ domain_info_dict = {item ['domain' ]: item for item in domain_info_list }
172
+ risk_score_dict = {item ['domain' ]: item for item in risk_score_list }
173
+
174
+ combined_results = []
175
+ for domain in domain_list :
176
+ domain_data = domain_info_dict .get (domain , {})
177
+ risk_data = risk_score_dict .get (domain , {})
178
+
179
+ combined_results .append ({
180
+ 'domain' : domain ,
181
+ ** domain_data ,
182
+ 'sp_risk_score' : risk_data .get ('sp_risk_score' ),
183
+ 'sp_risk_score_explain' : risk_data .get ('sp_risk_score_explain' )
184
+ })
185
+
186
+ return {'domains' : combined_results }
187
+
188
+ except Exception as e :
189
+ raise DemistoException (f'Failed to fetch bulk domain information: { str (e )} ' )
190
+
191
+
123
192
def get_domain_certificates (self , domain : str ) -> dict :
124
193
"""
125
194
Fetches SSL/TLS certificate data for a given domain.
126
-
195
+ If the job is not completed, it polls the job status periodically.
196
+
127
197
Args:
128
198
domain (str): The domain to fetch certificate information for.
129
-
199
+
130
200
Returns:
131
201
dict: A dictionary containing certificate information fetched from the API.
132
202
"""
133
203
demisto .debug (f'Fetching certificate information for domain: { domain } ' )
204
+
205
+
134
206
url_suffix = f'explore/domain/certificates/{ domain } '
135
- return self ._http_request ('GET' , url_suffix )
207
+ response = self ._http_request ('GET' , url_suffix , params = {
208
+ 'limit' : 100 ,
209
+ 'skip' : 0 ,
210
+ 'with_metadata' : 0
211
+ })
212
+
213
+
214
+ job_status_url = response .get ('response' , {}).get ('job_status' , {}).get ('get' )
215
+ if not job_status_url :
216
+ demisto .error ('Job status URL not found in the response' )
217
+ return response
218
+
219
+
220
+ job_complete = False
221
+ while not job_complete :
222
+ demisto .debug (f'Checking job status at { job_status_url } ' )
223
+
224
+
225
+ job_response = self ._http_request ('GET' , job_status_url )
226
+ job_status = job_response .get ('response' , {}).get ('job_status' , {}).get ('status' )
227
+
228
+ if job_status == 'COMPLETED' :
229
+ job_complete = True
230
+ demisto .debug ('Job completed, fetching certificates.' )
231
+
232
+ certificate_data = job_response .get ('response' , {}).get ('domain_certificates' , [])
233
+ return certificate_data
234
+ elif job_status == 'FAILED' :
235
+ demisto .error ('Job failed to complete.' )
236
+ return {'error' : 'Job failed' }
237
+ else :
238
+
239
+ demisto .debug ('Job is still in progress. Retrying...' )
240
+ time .sleep (5 )
241
+
242
+ return {}
243
+
136
244
137
245
def search_domains (self ,
138
- query : Optional [str ] = None ,
139
- start_date : Optional [str ] = None ,
140
- end_date : Optional [str ] = None ,
141
- risk_score_min : Optional [int ] = None ,
142
- risk_score_max : Optional [int ] = None ,
143
- limit : int = 100 ) -> dict :
246
+ query : Optional [str ] = None ,
247
+ start_date : Optional [str ] = None ,
248
+ end_date : Optional [str ] = None ,
249
+ risk_score_min : Optional [int ] = None ,
250
+ risk_score_max : Optional [int ] = None ,
251
+ limit : int = 100 ) -> dict :
144
252
"""
145
253
Search for domains with optional filters.
146
254
@@ -157,18 +265,21 @@ def search_domains(self,
157
265
"""
158
266
demisto .debug (f'Searching domains with query: { query } ' )
159
267
url_suffix = 'explore/domain/search'
160
-
268
+
161
269
params = {k : v for k , v in {
162
- 'query ' : query ,
270
+ 'domain ' : query ,
163
271
'start_date' : start_date ,
164
272
'end_date' : end_date ,
165
273
'risk_score_min' : risk_score_min ,
166
274
'risk_score_max' : risk_score_max ,
167
275
'limit' : limit
168
276
}.items () if v is not None }
169
277
170
- return self ._http_request ('GET' , url_suffix , params = params )
171
-
278
+ try :
279
+ return self ._http_request ('GET' , url_suffix , params = params )
280
+ except Exception as e :
281
+ demisto .error (f"Error in search_domains API request: { str (e )} " )
282
+
172
283
def list_domain_infratags (self , domains : list , cluster : Optional [bool ] = False , mode : Optional [str ] = 'live' , match : Optional [str ] = 'self' , as_of : Optional [str ] = None ) -> dict :
173
284
"""
174
285
Get infratags for multiple domains with optional clustering and additional filtering options.
@@ -185,7 +296,7 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False,
185
296
"""
186
297
demisto .debug (f'Fetching infratags for domains: { domains } with cluster={ cluster } , mode={ mode } , match={ match } , as_of={ as_of } ' )
187
298
188
- # Loop through the domains to create individual requests
299
+
189
300
results = {}
190
301
for domain in domains :
191
302
url = f'https://api.silentpush.com/api/v1/merge-api/explore/domain/infratag/{ domain } '
@@ -196,14 +307,17 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False,
196
307
'as_of' : as_of
197
308
}
198
309
try :
199
- response = self ._http_request ('GET' , url , params = data ) # Assuming GET method for this endpoint
310
+
311
+ response = self ._http_request ('GET' , url , params = data )
200
312
results [domain ] = response
201
313
except Exception as e :
202
314
demisto .error (f"Error fetching infratags for domain { domain } : { str (e )} " )
203
315
results [domain ] = {"error" : str (e )}
204
316
205
317
return results
206
318
319
+
320
+
207
321
def test_module (client : Client ) -> str :
208
322
"""
209
323
Tests connectivity to the SilentPush API and checks the authentication status.
@@ -232,49 +346,75 @@ def test_module(client: Client) -> str:
232
346
''' COMMAND FUNCTIONS '''
233
347
234
348
235
- def list_domain_information_command (client : Client , args : dict ) -> CommandResults :
349
+ def list_domain_information_command (client : Client , args : Dict [ str , Any ] ) -> CommandResults :
236
350
"""
237
351
Command handler for fetching domain information.
238
352
239
- This function processes the command for 'silentpush-list-domain-information', retrieves the
240
- domain information using the client, and formats it for XSOAR output.
241
-
242
353
Args:
243
- client (Client): The client instance to fetch the data.
244
- args (dict): The arguments passed to the command, including the domain.
245
-
354
+ client (Client): The client instance to fetch the data
355
+ args (dict): Command arguments
356
+
246
357
Returns:
247
- CommandResults: The command results containing readable output and the raw response.
358
+ CommandResults: XSOAR command results
248
359
"""
249
- domain = args .get ('domain' , 'silentpush.com' )
250
- demisto .debug (f'Processing domain: { domain } ' )
251
- raw_response = client .list_domain_information (domain )
360
+
361
+ domains_arg = args .get ('domains' , args .get ('domain' ))
362
+
363
+ if not domains_arg :
364
+ raise DemistoException ('No domains provided. Please provide domains using either the "domain" or "domains" argument.' )
365
+
366
+
367
+ if isinstance (domains_arg , str ):
368
+ domains = [d .strip () for d in domains_arg .split (',' )]
369
+ else :
370
+ domains = domains_arg
371
+
372
+ demisto .debug (f'Processing domain(s): { domains } ' )
373
+
374
+
375
+ raw_response = client .list_domain_information (domains )
252
376
demisto .debug (f'Response from API: { raw_response } ' )
253
-
254
- readable_output = tableToMarkdown ('Domain Information' , raw_response )
255
-
377
+
378
+
379
+ markdown = []
380
+ for domain_info in raw_response .get ('domains' , []):
381
+ markdown .append (f"### Domain: { domain_info .get ('domain' )} " )
382
+ markdown .append ("#### Domain Information:" )
383
+ markdown .append (f"- Age: { domain_info .get ('age' , 'N/A' )} days" )
384
+ markdown .append (f"- Registrar: { domain_info .get ('registrar' , 'N/A' )} " )
385
+ markdown .append (f"- Created Date: { domain_info .get ('whois_created_date' , 'N/A' )} " )
386
+ markdown .append (f"- Risk Score: { domain_info .get ('sp_risk_score' , 'N/A' )} " )
387
+ markdown .append ("\n " )
388
+
389
+ readable_output = '\n ' .join (markdown )
390
+
256
391
return CommandResults (
257
392
outputs_prefix = 'SilentPush.Domain' ,
258
393
outputs_key_field = 'domain' ,
259
- outputs = raw_response ,
394
+ outputs = raw_response . get ( 'domains' , []) ,
260
395
readable_output = readable_output ,
261
396
raw_response = raw_response
262
397
)
263
398
264
-
265
399
def get_domain_certificates_command (client : Client , args : dict ) -> CommandResults :
266
400
"""
267
401
Command handler for fetching domain certificate information.
268
402
"""
269
403
domain = args .get ('domain' , 'silentpush.com' )
270
404
demisto .debug (f'Processing certificates for domain: { domain } ' )
271
405
272
-
273
- demisto .debug ('Entering get_domain_certificates_command function' )
274
-
275
406
raw_response = client .get_domain_certificates (domain )
276
407
demisto .debug (f'Response from API: { raw_response } ' )
277
408
409
+ if 'error' in raw_response :
410
+ return CommandResults (
411
+ outputs_prefix = 'SilentPush.Certificates' ,
412
+ outputs_key_field = 'domain' ,
413
+ outputs = raw_response ,
414
+ readable_output = f"Error: { raw_response ['error' ]} " ,
415
+ raw_response = raw_response
416
+ )
417
+
278
418
readable_output = tableToMarkdown ('Domain Certificates' , raw_response )
279
419
280
420
return CommandResults (
@@ -284,10 +424,9 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult
284
424
readable_output = readable_output ,
285
425
raw_response = raw_response
286
426
)
287
-
427
+
288
428
289
429
def search_domains_command (client : Client , args : dict ) -> CommandResults :
290
-
291
430
query = args .get ('query' )
292
431
start_date = args .get ('start_date' )
293
432
end_date = args .get ('end_date' )
@@ -297,16 +436,30 @@ def search_domains_command(client: Client, args: dict) -> CommandResults:
297
436
298
437
demisto .debug (f'Searching domains with query: { query } ' )
299
438
300
- raw_response = client .search_domains (
301
- query = query ,
302
- start_date = start_date ,
303
- end_date = end_date ,
304
- risk_score_min = risk_score_min ,
305
- risk_score_max = risk_score_max ,
306
- limit = limit
307
- )
439
+ try :
440
+ raw_response = client .search_domains (
441
+ query = query ,
442
+ start_date = start_date ,
443
+ end_date = end_date ,
444
+ risk_score_min = risk_score_min ,
445
+ risk_score_max = risk_score_max ,
446
+ limit = limit
447
+ )
448
+ except Exception as e :
449
+ return CommandResults (
450
+ readable_output = f"Error: { str (e )} " ,
451
+ raw_response = {},
452
+ outputs_prefix = 'SilentPush.Error' ,
453
+ outputs_key_field = 'error'
454
+ )
308
455
309
- readable_output = tableToMarkdown ('Domain Search Results' , raw_response .get ('results' , []))
456
+
457
+ if raw_response .get ('response' ) and 'records' in raw_response ['response' ]:
458
+ records = raw_response ['response' ]['records' ]
459
+ else :
460
+ records = []
461
+
462
+ readable_output = tableToMarkdown ('Domain Search Results' , records )
310
463
311
464
return CommandResults (
312
465
outputs_prefix = 'SilentPush.SearchResults' ,
0 commit comments