2222def mock_debug (message ):
2323 """Print debug messages to the XSOAR logs"""
2424 print (f"DEBUG: { message } " )
25-
26-
2725demisto .debug = mock_debug
2826
2927''' CONSTANTS '''
@@ -105,42 +103,152 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data:
105103 except Exception as e :
106104 demisto .error (f'Error in API call: { str (e )} ' )
107105 raise
108-
109- def list_domain_information (self , domain : str ) -> dict :
106+
107+ def list_domain_information (self , domains : Union [ str , List [ str ]] ) -> Dict :
110108 """
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 .
112110
113111 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+
116114 Returns:
117- dict : A dictionary containing domain information fetched from the API.
115+ Dict : A dictionary containing combined domain information and risk scores
118116 """
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 :
122124
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+
123192 def get_domain_certificates (self , domain : str ) -> dict :
124193 """
125194 Fetches SSL/TLS certificate data for a given domain.
126-
195+ If the job is not completed, it polls the job status periodically.
196+
127197 Args:
128198 domain (str): The domain to fetch certificate information for.
129-
199+
130200 Returns:
131201 dict: A dictionary containing certificate information fetched from the API.
132202 """
133203 demisto .debug (f'Fetching certificate information for domain: { domain } ' )
204+
205+
134206 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+
136244
137245 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 :
144252 """
145253 Search for domains with optional filters.
146254
@@ -157,18 +265,21 @@ def search_domains(self,
157265 """
158266 demisto .debug (f'Searching domains with query: { query } ' )
159267 url_suffix = 'explore/domain/search'
160-
268+
161269 params = {k : v for k , v in {
162- 'query ' : query ,
270+ 'domain ' : query ,
163271 'start_date' : start_date ,
164272 'end_date' : end_date ,
165273 'risk_score_min' : risk_score_min ,
166274 'risk_score_max' : risk_score_max ,
167275 'limit' : limit
168276 }.items () if v is not None }
169277
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+
172283 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 :
173284 """
174285 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,
185296 """
186297 demisto .debug (f'Fetching infratags for domains: { domains } with cluster={ cluster } , mode={ mode } , match={ match } , as_of={ as_of } ' )
187298
188- # Loop through the domains to create individual requests
299+
189300 results = {}
190301 for domain in domains :
191302 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,
196307 'as_of' : as_of
197308 }
198309 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 )
200312 results [domain ] = response
201313 except Exception as e :
202314 demisto .error (f"Error fetching infratags for domain { domain } : { str (e )} " )
203315 results [domain ] = {"error" : str (e )}
204316
205317 return results
206318
319+
320+
207321def test_module (client : Client ) -> str :
208322 """
209323 Tests connectivity to the SilentPush API and checks the authentication status.
@@ -232,49 +346,75 @@ def test_module(client: Client) -> str:
232346''' COMMAND FUNCTIONS '''
233347
234348
235- def list_domain_information_command (client : Client , args : dict ) -> CommandResults :
349+ def list_domain_information_command (client : Client , args : Dict [ str , Any ] ) -> CommandResults :
236350 """
237351 Command handler for fetching domain information.
238352
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-
242353 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+
246357 Returns:
247- CommandResults: The command results containing readable output and the raw response.
358+ CommandResults: XSOAR command results
248359 """
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 )
252376 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+
256391 return CommandResults (
257392 outputs_prefix = 'SilentPush.Domain' ,
258393 outputs_key_field = 'domain' ,
259- outputs = raw_response ,
394+ outputs = raw_response . get ( 'domains' , []) ,
260395 readable_output = readable_output ,
261396 raw_response = raw_response
262397 )
263398
264-
265399def get_domain_certificates_command (client : Client , args : dict ) -> CommandResults :
266400 """
267401 Command handler for fetching domain certificate information.
268402 """
269403 domain = args .get ('domain' , 'silentpush.com' )
270404 demisto .debug (f'Processing certificates for domain: { domain } ' )
271405
272-
273- demisto .debug ('Entering get_domain_certificates_command function' )
274-
275406 raw_response = client .get_domain_certificates (domain )
276407 demisto .debug (f'Response from API: { raw_response } ' )
277408
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+
278418 readable_output = tableToMarkdown ('Domain Certificates' , raw_response )
279419
280420 return CommandResults (
@@ -284,10 +424,9 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult
284424 readable_output = readable_output ,
285425 raw_response = raw_response
286426 )
287-
427+
288428
289429def search_domains_command (client : Client , args : dict ) -> CommandResults :
290-
291430 query = args .get ('query' )
292431 start_date = args .get ('start_date' )
293432 end_date = args .get ('end_date' )
@@ -297,16 +436,30 @@ def search_domains_command(client: Client, args: dict) -> CommandResults:
297436
298437 demisto .debug (f'Searching domains with query: { query } ' )
299438
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+ )
308455
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 )
310463
311464 return CommandResults (
312465 outputs_prefix = 'SilentPush.SearchResults' ,
0 commit comments