From 707d212509da642c2b50245fcd9c19927fe7c8aa Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 10 Jan 2025 14:52:21 +0530 Subject: [PATCH 01/19] updated the code --- .../Integrations/SilentPush/SilentPush.py | 59 ++++++++++++++++--- .../Integrations/SilentPush/SilentPush.yml | 36 ++++++++++- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index fc56db7e38c2..f71c8c6657a9 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -11,6 +11,7 @@ from CommonServerUserPython import * # noqa +import requests import urllib3 from typing import Any @@ -119,6 +120,20 @@ def list_domain_information(self, domain: str) -> dict: url_suffix = f'explore/domain/domaininfo/{domain}' return self._http_request('GET', url_suffix) + def get_domain_certificates(self, domain: str) -> dict: + """ + Fetches SSL/TLS certificate data for a given domain. + + Args: + domain (str): The domain to fetch certificate information for. + + Returns: + dict: A dictionary containing certificate information fetched from the API. + """ + demisto.debug(f'Fetching certificate information for domain: {domain}') + url_suffix = f'explore/domain/certificates/{domain}' + return self._http_request('GET', url_suffix) + def test_module(client: Client) -> str: """ @@ -179,6 +194,30 @@ def list_domain_information_command(client: Client, args: dict) -> CommandResult ) +def get_domain_certificates_command(client: Client, args: dict) -> CommandResults: + """ + Command handler for fetching domain certificate information. + """ + domain = args.get('domain', 'silentpush.com') + demisto.debug(f'Processing certificates for domain: {domain}') + + + demisto.debug('Entering get_domain_certificates_command function') + + raw_response = client.get_domain_certificates(domain) + demisto.debug(f'Response from API: {raw_response}') + + readable_output = tableToMarkdown('Domain Certificates', raw_response) + + return CommandResults( + outputs_prefix='SilentPush.Certificates', + outputs_key_field='domain', + outputs=raw_response, + readable_output=readable_output, + raw_response=raw_response + ) + + ''' MAIN FUNCTION ''' @@ -213,13 +252,19 @@ def main(): command = demisto.command() demisto.debug(f'Command being called is {command}') - if command == 'test-module': - result = test_module(client) - return_results(result) - - elif command == 'silentpush-list-domain-information': - return_results(list_domain_information_command(client, demisto.args())) - + command_handlers = { + + 'test-module': test_module, + 'silentpush-list-domain-information': list_domain_information_command, + 'silentpush-get-domain-certificates': get_domain_certificates_command, + } + + if command in command_handlers: + if command == 'test-module': + result = command_handlers[command](client) + return_results(result) + else: + return_results(command_handlers[command](client, demisto.args())) else: raise DemistoException(f'Unsupported command: {command}') diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 098cfeef1186..375bbb45fadd 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -5,7 +5,7 @@ commonfields: type: python subType: python3 description: | - This integration allows fetching domain information from the SilentPush API. It includes commands to get domain-related information such as WHOIS data, domain age, and risk scores. + This integration allows fetching domain information from the SilentPush API. It includes commands to get domain-related information such as WHOIS data, domain age, and risk scores, as well as SSL/TLS certificate data. tags: [] enabled: true manufacturer: SilentPush @@ -18,7 +18,7 @@ commonfields: scripts: - path: SilentPush.py comment: | - Integration for SilentPush that enables fetching domain information, including WHOIS data, domain age, and risk scores. + Integration for SilentPush that enables fetching domain information, including WHOIS data, domain age, risk scores, and certificates. commands: - name: test-module @@ -49,6 +49,17 @@ commands: examples: | !silentpush-list-domain-information domain=example.com + - name: silentpush-get-domain-certificates + description: | + Fetches SSL/TLS certificate data for a given domain. + isArray: false + argContext: + - id: domain + type: string + description: The domain to fetch certificate information for. + examples: | + !silentpush-get-domain-certificates domain=example.com + args: - id: domain isArray: false @@ -71,6 +82,25 @@ outputs: - name: risk_score type: float + - id: SilentPush.Certificates + type: complex + description: | + The certificate information fetched from SilentPush API for the domain. + contents: + - name: domain + type: string + - name: certificates + type: list + items: + - name: certificate_issuer + type: string + - name: valid_from + type: string + - name: valid_to + type: string + - name: certificate_serial_number + type: string + tests: - name: Test SilentPush Integration description: Test the integration with the SilentPush API. @@ -81,7 +111,7 @@ tests: base_url: https://api.silentpush.com api_key: 'your_api_key' -# Optional: Adding the configuration section for any configuration-related parameters + configurations: - default: true isArray: false From e44654193cd25f4478adf1cdcbdc79b1017cb103 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 10 Jan 2025 17:42:40 +0530 Subject: [PATCH 02/19] implemented search-domain command --- .../Integrations/SilentPush/SilentPush.py | 73 +++++- .../Integrations/SilentPush/SilentPush.yml | 231 ++++++++---------- 2 files changed, 169 insertions(+), 135 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index f71c8c6657a9..ba417b681c26 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -13,7 +13,7 @@ import requests import urllib3 -from typing import Any +from typing import Any, Optional, Dict # Disable insecure warnings urllib3.disable_warnings() @@ -134,6 +134,42 @@ def get_domain_certificates(self, domain: str) -> dict: url_suffix = f'explore/domain/certificates/{domain}' return self._http_request('GET', url_suffix) + def search_domains(self, + query: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + risk_score_min: Optional[int] = None, + risk_score_max: Optional[int] = None, + limit: int = 100) -> dict: + """ + Search for domains with optional filters. + + Args: + query (str, optional): Search query string (e.g., domain pattern, keywords) + start_date (str, optional): Start date for domain registration (ISO8601 format) + end_date (str, optional): End date for domain registration (ISO8601 format) + risk_score_min (int, optional): Minimum risk score filter + risk_score_max (int, optional): Maximum risk score filter + limit (int, optional): Maximum number of results to return (default: 100) + + Returns: + dict: A dictionary containing the search results + """ + demisto.debug(f'Searching domains with query: {query}') + url_suffix = 'explore/domain/search' + + # Build parameters dictionary with only non-None values + params = {k: v for k, v in { + 'query': query, + 'start_date': start_date, + 'end_date': end_date, + 'risk_score_min': risk_score_min, + 'risk_score_max': risk_score_max, + 'limit': limit + }.items() if v is not None} + + return self._http_request('GET', url_suffix, params=params) + def test_module(client: Client) -> str: """ @@ -218,6 +254,38 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult ) +def search_domains_command(client: Client, args: dict) -> CommandResults: + + # Extract parameters from args with type conversion + query = args.get('query') + start_date = args.get('start_date') + end_date = args.get('end_date') + risk_score_min = arg_to_number(args.get('risk_score_min')) + risk_score_max = arg_to_number(args.get('risk_score_max')) + limit = arg_to_number(args.get('limit', 100)) + + demisto.debug(f'Searching domains with query: {query}') + + raw_response = client.search_domains( + query=query, + start_date=start_date, + end_date=end_date, + risk_score_min=risk_score_min, + risk_score_max=risk_score_max, + limit=limit + ) + + readable_output = tableToMarkdown('Domain Search Results', raw_response.get('results', [])) + + return CommandResults( + outputs_prefix='SilentPush.SearchResults', + outputs_key_field='domain', + outputs=raw_response, + readable_output=readable_output, + raw_response=raw_response + ) + + ''' MAIN FUNCTION ''' @@ -257,6 +325,7 @@ def main(): 'test-module': test_module, 'silentpush-list-domain-information': list_domain_information_command, 'silentpush-get-domain-certificates': get_domain_certificates_command, + 'silentpush-search-domains': search_domains_command, } if command in command_handlers: @@ -277,4 +346,4 @@ def main(): if __name__ in ('__main__', '__builtin__', 'builtins'): - main() + main() \ No newline at end of file diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 375bbb45fadd..fbf62f668863 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -1,139 +1,104 @@ commonfields: id: SilentPush - version: -1 - name: SilentPush - type: python - subType: python3 - description: | - This integration allows fetching domain information from the SilentPush API. It includes commands to get domain-related information such as WHOIS data, domain age, and risk scores, as well as SSL/TLS certificate data. - tags: [] - enabled: true - manufacturer: SilentPush - comment: '' - minVersion: -1 - dependencies: - - CommonServerPython - - CommonServerUserPython - -scripts: - - path: SilentPush.py - comment: | - Integration for SilentPush that enables fetching domain information, including WHOIS data, domain age, risk scores, and certificates. - -commands: - - name: test-module - description: | - Tests the connectivity to the SilentPush API and checks the authentication status. - isArray: false - argContext: - - id: base_url - type: string - description: The base URL for the SilentPush API. - - id: api_key - type: string - description: The API key used to authenticate requests. - - id: verify_ssl - type: boolean - description: Flag to determine whether SSL verification is enabled. - examples: | - !test-module - - - name: silentpush-list-domain-information - description: | - Fetches domain information, such as WHOIS data, domain age, and risk scores. - isArray: false - argContext: - - id: domain - type: string - description: The domain name to fetch information for. - examples: | - !silentpush-list-domain-information domain=example.com - - - name: silentpush-get-domain-certificates - description: | - Fetches SSL/TLS certificate data for a given domain. - isArray: false - argContext: - - id: domain - type: string - description: The domain to fetch certificate information for. - examples: | - !silentpush-get-domain-certificates domain=example.com + version: 1.0.0 -args: - - id: domain - isArray: false - description: | - The domain to fetch information for. - type: string +name: SilentPush +display: SilentPush +category: Data Enrichment & Threat Intelligence +description: Integration with SilentPush API for domain intelligence and analysis. +configuration: + - display: Server URL + name: url + defaultvalue: https://api.silentpush.com + type: 0 + required: true + + - display: API Key + name: credentials + type: 9 + required: true + + - display: Trust any certificate (not secure) + name: insecure + type: 8 + required: false + + - display: Use system proxy settings + name: proxy + type: 8 + required: false -outputs: - - id: SilentPush.Domain - type: complex - description: | - The domain information fetched from SilentPush API, including WHOIS data, domain age, and risk scores. - contents: - - name: domain - type: string - - name: whois_data - type: string - - name: domain_age - type: integer - - name: risk_score - type: float - - - id: SilentPush.Certificates - type: complex - description: | - The certificate information fetched from SilentPush API for the domain. - contents: - - name: domain - type: string - - name: certificates - type: list - items: - - name: certificate_issuer - type: string - - name: valid_from - type: string - - name: valid_to - type: string - - name: certificate_serial_number - type: string +script: + script: '' + type: python + commands: + - name: silentpush-list-domain-information + description: Fetches domain information such as WHOIS data, domain age, and risk scores + arguments: + - name: domain + description: The domain to fetch information for + required: true + default: false + outputs: + - contextPath: SilentPush.Domain + description: Domain information retrieved from SilentPush + type: unknown + - contextPath: SilentPush.Domain.domain + description: The domain name + type: string + + - name: silentpush-get-domain-certificates + description: Fetches SSL/TLS certificate data for a given domain + arguments: + - name: domain + description: The domain to fetch certificate information for + required: true + default: false + outputs: + - contextPath: SilentPush.Certificates + description: Certificate information for the domain + type: unknown + - contextPath: SilentPush.Certificates.domain + description: The domain name + type: string + + - name: silentpush-search-domains + description: Search for domains with optional filters + arguments: + - name: query + description: Search query string (e.g., domain pattern, keywords) + required: false + default: false + - name: start_date + description: Start date for domain registration (ISO8601 format) + required: false + default: false + - name: end_date + description: End date for domain registration (ISO8601 format) + required: false + default: false + - name: risk_score_min + description: Minimum risk score filter + required: false + default: false + - name: risk_score_max + description: Maximum risk score filter + required: false + default: false + - name: limit + description: Maximum number of results to return + required: false + default: true + defaultValue: "100" + outputs: + - contextPath: SilentPush.SearchResults + description: Search results from the domain query + type: unknown + - contextPath: SilentPush.SearchResults.domain + description: The domain name in the search results + type: string +dockerimage: demisto/python3:3.10 +fromversion: 6.0.0 tests: - - name: Test SilentPush Integration - description: Test the integration with the SilentPush API. - steps: - - script: test-module - name: Test SilentPush API Connectivity - args: - base_url: https://api.silentpush.com - api_key: 'your_api_key' - - -configurations: - - default: true - isArray: false - description: The configuration parameters required for connecting to SilentPush API. - context: - - id: base_url - type: string - description: The base URL for the SilentPush API. - - id: api_key - type: string - description: The API key used to authenticate requests. - - id: verify_ssl - type: boolean - description: Flag to determine whether SSL verification is enabled. - - id: proxy - type: boolean - description: Flag to determine whether to use a proxy. - -errorHandling: - - errorCode: 403 - description: | - If an authorization error is encountered, it could indicate an incorrect or expired API key. - - errorCode: 400 - description: | - Bad Request error, likely due to incorrect input format or invalid parameters in the request. + - No tests \ No newline at end of file From 7f47f4d5489f6af8e63dda0e41341de1dc863cff Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 10 Jan 2025 18:34:32 +0530 Subject: [PATCH 03/19] added silentpush-list-domain-infratags --- .../Integrations/SilentPush/SilentPush.py | 81 ++++++++++++++++++- .../Integrations/SilentPush/SilentPush.yml | 29 ++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index ba417b681c26..82ff2ad60473 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -157,8 +157,7 @@ def search_domains(self, """ demisto.debug(f'Searching domains with query: {query}') url_suffix = 'explore/domain/search' - - # Build parameters dictionary with only non-None values + params = {k: v for k, v in { 'query': query, 'start_date': start_date, @@ -169,7 +168,41 @@ def search_domains(self, }.items() if v is not None} return self._http_request('GET', url_suffix, params=params) + + 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: + """ + Get infratags for multiple domains with optional clustering and additional filtering options. + + Args: + domains (list): A list of domains to retrieve infratags for. + cluster (bool, optional): Whether to cluster the results. Defaults to False. + mode (str, optional): Mode for the lookup, either 'live' (default) or 'padns'. + match (str, optional): Handling of self-hosted infrastructure, either 'self' (default) or 'full'. + as_of (str, optional): Date or timestamp for filtering the data. + Returns: + dict: A dictionary containing infratags for the provided domains. + """ + demisto.debug(f'Fetching infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') + + # Loop through the domains to create individual requests + results = {} + for domain in domains: + url = f'https://api.silentpush.com/api/v1/merge-api/explore/domain/infratag/{domain}' + data = { + 'cluster': cluster, + 'mode': mode, + 'match': match, + 'as_of': as_of + } + try: + response = self._http_request('GET', url, params=data) # Assuming GET method for this endpoint + results[domain] = response + except Exception as e: + demisto.error(f"Error fetching infratags for domain {domain}: {str(e)}") + results[domain] = {"error": str(e)} + + return results def test_module(client: Client) -> str: """ @@ -215,7 +248,6 @@ def list_domain_information_command(client: Client, args: dict) -> CommandResult """ domain = args.get('domain', 'silentpush.com') demisto.debug(f'Processing domain: {domain}') - raw_response = client.list_domain_information(domain) demisto.debug(f'Response from API: {raw_response}') @@ -256,7 +288,6 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult def search_domains_command(client: Client, args: dict) -> CommandResults: - # Extract parameters from args with type conversion query = args.get('query') start_date = args.get('start_date') end_date = args.get('end_date') @@ -284,6 +315,47 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: readable_output=readable_output, raw_response=raw_response ) + +def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: + """ + Command handler for fetching infratags for multiple domains. + + Args: + client (Client): The client instance to fetch the data. + args (dict): The arguments passed to the command, including domains, clustering option, and optional filters. + + Returns: + CommandResults: The command results containing readable output and the raw response. + """ + + domains = argToList(args.get('domains', '')) + cluster = argToBoolean(args.get('cluster', False)) + mode = args.get('mode', 'live') # Default to 'live' + match = args.get('match', 'self') # Default to 'self' + as_of = args.get('as_of', None) # Default to None + + if not domains: + raise ValueError('"domains" argument is required and cannot be empty.') + + demisto.debug(f'Processing infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') + + try: + raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) + demisto.debug(f'Response from API: {raw_response}') + except Exception as e: + demisto.error(f'Error occurred while fetching infratags: {str(e)}') + raise + + readable_output = tableToMarkdown('Domain Infratags', raw_response.get('results', [])) + + return CommandResults( + outputs_prefix='SilentPush.InfraTags', + outputs_key_field='domain', + outputs=raw_response, + readable_output=readable_output, + raw_response=raw_response + ) + ''' MAIN FUNCTION ''' @@ -326,6 +398,7 @@ def main(): 'silentpush-list-domain-information': list_domain_information_command, 'silentpush-get-domain-certificates': get_domain_certificates_command, 'silentpush-search-domains': search_domains_command, + 'silentpush-list-domain-infratags': list_domain_infratags_command, } if command in command_handlers: diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index fbf62f668863..32accbf685fe 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -98,7 +98,34 @@ script: description: The domain name in the search results type: string + - name: silentpush-list-domain-infratags + description: Fetches infratag information for a given domain + arguments: + - name: domain + description: The domain to fetch infratags for + required: true + default: false + - name: mode + description: The mode for fetching infratags (live or padns) + required: false + default: "live" + - name: match + description: How to handle self-hosted infrastructure (self or full) + required: false + default: "self" + - name: as_of + description: The date or epoch time to use for fetching infratags from PADNS data + required: false + default: false + outputs: + - contextPath: SilentPush.Infratags + description: Infratag information for the domain + type: unknown + - contextPath: SilentPush.Infratags.domain + description: The domain name + type: string + dockerimage: demisto/python3:3.10 fromversion: 6.0.0 tests: - - No tests \ No newline at end of file + - No tests From 1397c5a7b05c24323da2ebc99758b624c1ec1592 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 10 Jan 2025 18:35:53 +0530 Subject: [PATCH 04/19] updated the code --- Packs/SilentPush/Integrations/SilentPush/SilentPush.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 82ff2ad60473..d0360e7688de 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -330,9 +330,9 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: domains = argToList(args.get('domains', '')) cluster = argToBoolean(args.get('cluster', False)) - mode = args.get('mode', 'live') # Default to 'live' - match = args.get('match', 'self') # Default to 'self' - as_of = args.get('as_of', None) # Default to None + mode = args.get('mode', 'live') + match = args.get('match', 'self') + as_of = args.get('as_of', None) if not domains: raise ValueError('"domains" argument is required and cannot be empty.') From de40fd87c3bb70304c0cc2b19d2a7de5438f051c Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Sat, 11 Jan 2025 02:47:20 +0530 Subject: [PATCH 05/19] updated the code with major changes --- .../Integrations/SilentPush/SilentPush.py | 265 ++++++++++++++---- .../Integrations/SilentPush/SilentPush.yml | 243 ++++++++-------- 2 files changed, 327 insertions(+), 181 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index d0360e7688de..59ef7ce46748 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -22,8 +22,6 @@ def mock_debug(message): """Print debug messages to the XSOAR logs""" print(f"DEBUG: {message}") - - demisto.debug = mock_debug ''' CONSTANTS ''' @@ -105,42 +103,152 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: except Exception as e: demisto.error(f'Error in API call: {str(e)}') raise - - def list_domain_information(self, domain: str) -> dict: + + def list_domain_information(self, domains: Union[str, List[str]]) -> Dict: """ - Fetches domain information such as WHOIS data, domain age, and risk scores. + Fetches domain information including WHOIS data and risk scores for multiple domains. Args: - domain (str): The domain to fetch information for. - + domains: Either a single domain string or a list of domain strings + Returns: - dict: A dictionary containing domain information fetched from the API. + Dict: A dictionary containing combined domain information and risk scores """ - demisto.debug(f'Fetching domain information for domain: {domain}') - url_suffix = f'explore/domain/domaininfo/{domain}' - return self._http_request('GET', url_suffix) + demisto.debug(f'Fetching domain information for: {domains}') + domain_list = [domains] if isinstance(domains, str) else domains + + if len(domain_list) > 100: + raise DemistoException("Maximum of 100 domains can be submitted in a single request") + + if len(domain_list) == 1: + domain = domain_list[0] + try: + + domain_info_response = self._http_request( + method='GET', + url_suffix=f'explore/domain/domaininfo/{domain}' + ) + domain_info = domain_info_response.get('response', {}).get('domaininfo', {}) + + risk_score_response = self._http_request( + method='GET', + url_suffix=f'explore/domain/riskscore/{domain}' + ) + risk_info = risk_score_response.get('response', {}) + + combined_info = { + 'domain': domain, + **domain_info, + 'sp_risk_score': risk_info.get('sp_risk_score'), + 'sp_risk_score_explain': risk_info.get('sp_risk_score_explain') + } + + return {'domains': [combined_info]} + + except Exception as e: + raise DemistoException(f'Failed to fetch information for domain {domain}: {str(e)}') + + else: + + try: + + domains_data = {'domains': domain_list} + bulk_info_response = self._http_request( + method='POST', + url_suffix='explore/bulk/domaininfo', + data=domains_data + ) + + bulk_risk_response = self._http_request( + method='POST', + url_suffix='explore/bulk/domain/riskscore', + data=domains_data + ) + + domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) + risk_score_list = bulk_risk_response.get('response', []) + domain_info_dict = {item['domain']: item for item in domain_info_list} + risk_score_dict = {item['domain']: item for item in risk_score_list} + + combined_results = [] + for domain in domain_list: + domain_data = domain_info_dict.get(domain, {}) + risk_data = risk_score_dict.get(domain, {}) + + combined_results.append({ + 'domain': domain, + **domain_data, + 'sp_risk_score': risk_data.get('sp_risk_score'), + 'sp_risk_score_explain': risk_data.get('sp_risk_score_explain') + }) + + return {'domains': combined_results} + + except Exception as e: + raise DemistoException(f'Failed to fetch bulk domain information: {str(e)}') + + def get_domain_certificates(self, domain: str) -> dict: """ Fetches SSL/TLS certificate data for a given domain. - + If the job is not completed, it polls the job status periodically. + Args: domain (str): The domain to fetch certificate information for. - + Returns: dict: A dictionary containing certificate information fetched from the API. """ demisto.debug(f'Fetching certificate information for domain: {domain}') + + url_suffix = f'explore/domain/certificates/{domain}' - return self._http_request('GET', url_suffix) + response = self._http_request('GET', url_suffix, params={ + 'limit': 100, + 'skip': 0, + 'with_metadata': 0 + }) + + + job_status_url = response.get('response', {}).get('job_status', {}).get('get') + if not job_status_url: + demisto.error('Job status URL not found in the response') + return response + + + job_complete = False + while not job_complete: + demisto.debug(f'Checking job status at {job_status_url}') + + + job_response = self._http_request('GET', job_status_url) + job_status = job_response.get('response', {}).get('job_status', {}).get('status') + + if job_status == 'COMPLETED': + job_complete = True + demisto.debug('Job completed, fetching certificates.') + + certificate_data = job_response.get('response', {}).get('domain_certificates', []) + return certificate_data + elif job_status == 'FAILED': + demisto.error('Job failed to complete.') + return {'error': 'Job failed'} + else: + + demisto.debug('Job is still in progress. Retrying...') + time.sleep(5) + + return {} + def search_domains(self, - query: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - risk_score_min: Optional[int] = None, - risk_score_max: Optional[int] = None, - limit: int = 100) -> dict: + query: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + risk_score_min: Optional[int] = None, + risk_score_max: Optional[int] = None, + limit: int = 100) -> dict: """ Search for domains with optional filters. @@ -157,9 +265,9 @@ def search_domains(self, """ demisto.debug(f'Searching domains with query: {query}') url_suffix = 'explore/domain/search' - + params = {k: v for k, v in { - 'query': query, + 'domain': query, 'start_date': start_date, 'end_date': end_date, 'risk_score_min': risk_score_min, @@ -167,8 +275,11 @@ def search_domains(self, 'limit': limit }.items() if v is not None} - return self._http_request('GET', url_suffix, params=params) - + try: + return self._http_request('GET', url_suffix, params=params) + except Exception as e: + demisto.error(f"Error in search_domains API request: {str(e)}") + 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: """ 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, """ demisto.debug(f'Fetching infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') - # Loop through the domains to create individual requests + results = {} for domain in domains: url = f'https://api.silentpush.com/api/v1/merge-api/explore/domain/infratag/{domain}' @@ -196,7 +307,8 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False, 'as_of': as_of } try: - response = self._http_request('GET', url, params=data) # Assuming GET method for this endpoint + + response = self._http_request('GET', url, params=data) results[domain] = response except Exception as e: demisto.error(f"Error fetching infratags for domain {domain}: {str(e)}") @@ -204,6 +316,8 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False, return results + + def test_module(client: Client) -> str: """ Tests connectivity to the SilentPush API and checks the authentication status. @@ -232,36 +346,56 @@ def test_module(client: Client) -> str: ''' COMMAND FUNCTIONS ''' -def list_domain_information_command(client: Client, args: dict) -> CommandResults: +def list_domain_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ Command handler for fetching domain information. - This function processes the command for 'silentpush-list-domain-information', retrieves the - domain information using the client, and formats it for XSOAR output. - Args: - client (Client): The client instance to fetch the data. - args (dict): The arguments passed to the command, including the domain. - + client (Client): The client instance to fetch the data + args (dict): Command arguments + Returns: - CommandResults: The command results containing readable output and the raw response. + CommandResults: XSOAR command results """ - domain = args.get('domain', 'silentpush.com') - demisto.debug(f'Processing domain: {domain}') - raw_response = client.list_domain_information(domain) + + domains_arg = args.get('domains', args.get('domain')) + + if not domains_arg: + raise DemistoException('No domains provided. Please provide domains using either the "domain" or "domains" argument.') + + + if isinstance(domains_arg, str): + domains = [d.strip() for d in domains_arg.split(',')] + else: + domains = domains_arg + + demisto.debug(f'Processing domain(s): {domains}') + + + raw_response = client.list_domain_information(domains) demisto.debug(f'Response from API: {raw_response}') - - readable_output = tableToMarkdown('Domain Information', raw_response) - + + + markdown = [] + for domain_info in raw_response.get('domains', []): + markdown.append(f"### Domain: {domain_info.get('domain')}") + markdown.append("#### Domain Information:") + markdown.append(f"- Age: {domain_info.get('age', 'N/A')} days") + markdown.append(f"- Registrar: {domain_info.get('registrar', 'N/A')}") + markdown.append(f"- Created Date: {domain_info.get('whois_created_date', 'N/A')}") + markdown.append(f"- Risk Score: {domain_info.get('sp_risk_score', 'N/A')}") + markdown.append("\n") + + readable_output = '\n'.join(markdown) + return CommandResults( outputs_prefix='SilentPush.Domain', outputs_key_field='domain', - outputs=raw_response, + outputs=raw_response.get('domains', []), readable_output=readable_output, raw_response=raw_response ) - def get_domain_certificates_command(client: Client, args: dict) -> CommandResults: """ Command handler for fetching domain certificate information. @@ -269,12 +403,18 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult domain = args.get('domain', 'silentpush.com') demisto.debug(f'Processing certificates for domain: {domain}') - - demisto.debug('Entering get_domain_certificates_command function') - raw_response = client.get_domain_certificates(domain) demisto.debug(f'Response from API: {raw_response}') + if 'error' in raw_response: + return CommandResults( + outputs_prefix='SilentPush.Certificates', + outputs_key_field='domain', + outputs=raw_response, + readable_output=f"Error: {raw_response['error']}", + raw_response=raw_response + ) + readable_output = tableToMarkdown('Domain Certificates', raw_response) return CommandResults( @@ -284,10 +424,9 @@ def get_domain_certificates_command(client: Client, args: dict) -> CommandResult readable_output=readable_output, raw_response=raw_response ) - + def search_domains_command(client: Client, args: dict) -> CommandResults: - query = args.get('query') start_date = args.get('start_date') end_date = args.get('end_date') @@ -297,16 +436,30 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: demisto.debug(f'Searching domains with query: {query}') - raw_response = client.search_domains( - query=query, - start_date=start_date, - end_date=end_date, - risk_score_min=risk_score_min, - risk_score_max=risk_score_max, - limit=limit - ) + try: + raw_response = client.search_domains( + query=query, + start_date=start_date, + end_date=end_date, + risk_score_min=risk_score_min, + risk_score_max=risk_score_max, + limit=limit + ) + except Exception as e: + return CommandResults( + readable_output=f"Error: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error' + ) - readable_output = tableToMarkdown('Domain Search Results', raw_response.get('results', [])) + + if raw_response.get('response') and 'records' in raw_response['response']: + records = raw_response['response']['records'] + else: + records = [] + + readable_output = tableToMarkdown('Domain Search Results', records) return CommandResults( outputs_prefix='SilentPush.SearchResults', diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 32accbf685fe..d924cba47cc9 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -1,131 +1,124 @@ commonfields: id: SilentPush - version: 1.0.0 + version: -1 + type: python + subtype: python3 + tags: [] + name: SilentPush + description: Integration with SilentPush API to retrieve domain information and risk scores. + enabled: true + author: Your Name + comments: Integration for fetching domain information, certificates, and more from SilentPush. + +scripts: + - id: list-domain-information + name: List Domain Information + description: Fetches information about domains, including WHOIS data and risk scores. + type: python + args: + - default: false + isArray: true + isRequired: false + name: domains + description: Comma-separated list of domains to fetch information for. + command: | + list_domain_information_command(client=client, args=args) + + - id: get-domain-certificates + name: Get Domain Certificates + description: Fetches SSL/TLS certificates for the specified domain. + type: python + args: + - default: true + isArray: false + isRequired: true + name: domain + description: The domain to fetch certificate data for. + command: | + get_domain_certificates_command(client=client, args=args) -name: SilentPush -display: SilentPush -category: Data Enrichment & Threat Intelligence -description: Integration with SilentPush API for domain intelligence and analysis. -configuration: - - display: Server URL - name: url - defaultvalue: https://api.silentpush.com - type: 0 - required: true - - - display: API Key - name: credentials - type: 9 - required: true - - - display: Trust any certificate (not secure) - name: insecure - type: 8 - required: false - - - display: Use system proxy settings - name: proxy - type: 8 - required: false + - id: search-domains + name: Search Domains + description: Searches for domains with optional filters like risk score, registration date, etc. + type: python + args: + - default: false + isArray: false + isRequired: false + name: query + description: Optional search query for domains. + - default: false + isArray: false + isRequired: false + name: start_date + description: Optional start date for filtering. + - default: false + isArray: false + isRequired: false + name: end_date + description: Optional end date for filtering. + - default: false + isArray: false + isRequired: false + name: risk_score_min + description: Minimum risk score filter. + - default: false + isArray: false + isRequired: false + name: risk_score_max + description: Maximum risk score filter. + - default: false + isArray: false + isRequired: false + name: limit + description: Limit the number of results returned. + command: | + search_domains_command(client=client, args=args) -script: - script: '' +test: + id: test-module + name: Test Module + description: Tests the connectivity to the SilentPush API and verifies the API key. type: python - commands: - - name: silentpush-list-domain-information - description: Fetches domain information such as WHOIS data, domain age, and risk scores - arguments: - - name: domain - description: The domain to fetch information for - required: true - default: false - outputs: - - contextPath: SilentPush.Domain - description: Domain information retrieved from SilentPush - type: unknown - - contextPath: SilentPush.Domain.domain - description: The domain name - type: string - - - name: silentpush-get-domain-certificates - description: Fetches SSL/TLS certificate data for a given domain - arguments: - - name: domain - description: The domain to fetch certificate information for - required: true - default: false - outputs: - - contextPath: SilentPush.Certificates - description: Certificate information for the domain - type: unknown - - contextPath: SilentPush.Certificates.domain - description: The domain name - type: string - - - name: silentpush-search-domains - description: Search for domains with optional filters - arguments: - - name: query - description: Search query string (e.g., domain pattern, keywords) - required: false - default: false - - name: start_date - description: Start date for domain registration (ISO8601 format) - required: false - default: false - - name: end_date - description: End date for domain registration (ISO8601 format) - required: false - default: false - - name: risk_score_min - description: Minimum risk score filter - required: false - default: false - - name: risk_score_max - description: Maximum risk score filter - required: false - default: false - - name: limit - description: Maximum number of results to return - required: false - default: true - defaultValue: "100" - outputs: - - contextPath: SilentPush.SearchResults - description: Search results from the domain query - type: unknown - - contextPath: SilentPush.SearchResults.domain - description: The domain name in the search results - type: string + command: | + test_module(client=client) + +configurations: + - id: SilentPush_Config + name: SilentPush Configuration + description: Configuration for connecting to SilentPush API. + type: python + enabled: true + isArray: false + isRequired: true + fields: + - id: api_key + name: API Key + description: API Key for SilentPush. + isArray: false + isRequired: true + - id: base_url + name: Base URL + description: The base URL for the SilentPush API. + isArray: false + isRequired: true + - id: verify_ssl + name: Verify SSL + description: Whether to verify SSL certificates. + isArray: false + isRequired: false + - id: proxy + name: Use Proxy + description: Whether to use a proxy server. + isArray: false + isRequired: false - - name: silentpush-list-domain-infratags - description: Fetches infratag information for a given domain - arguments: - - name: domain - description: The domain to fetch infratags for - required: true - default: false - - name: mode - description: The mode for fetching infratags (live or padns) - required: false - default: "live" - - name: match - description: How to handle self-hosted infrastructure (self or full) - required: false - default: "self" - - name: as_of - description: The date or epoch time to use for fetching infratags from PADNS data - required: false - default: false - outputs: - - contextPath: SilentPush.Infratags - description: Infratag information for the domain - type: unknown - - contextPath: SilentPush.Infratags.domain - description: The domain name - type: string - -dockerimage: demisto/python3:3.10 -fromversion: 6.0.0 -tests: - - No tests +outputs: + - id: SilentPush.Domain + contextPath: SilentPush.Domain + type: array + description: Domain information and risk scores. + - id: SilentPush.Certificates + contextPath: SilentPush.Certificates + type: array + description: SSL/TLS certificate data. From b87d7fd702158dfbcf906c672942ab4f0e9b7758 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Sat, 11 Jan 2025 12:24:20 +0530 Subject: [PATCH 06/19] added code for get_enrichment_data --- .../Integrations/SilentPush/SilentPush.py | 147 +++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 59ef7ce46748..d48da8204d5c 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -312,9 +312,51 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False, results[domain] = response except Exception as e: demisto.error(f"Error fetching infratags for domain {domain}: {str(e)}") - results[domain] = {"error": str(e)} + + + def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = False, scan_data: bool = False) -> Dict: + """ + Retrieve comprehensive enrichment information for a given resource (domain, IPv4, or IPv6). + + Args: + resource (str): The resource identifier (domain name, IPv4 address, or IPv6 address) + resource_type (str): Type of resource ('domain', 'ipv4', 'ipv6') + explain (bool, optional): Whether to show details of data used to calculate scores (default: False) + scan_data (bool, optional): Whether to show details of data collected from scanning (default: False) + + Returns: + Dict: The enrichment data response from the API + + Raises: + ValueError: If resource_type is not one of 'domain', 'ipv4', 'ipv6' + DemistoException: If the API request fails + """ + if resource_type not in {'domain', 'ipv4', 'ipv6'}: + raise ValueError("resource_type must be one of: 'domain', 'ipv4', 'ipv6'") + + demisto.debug(f'Fetching enrichment data for {resource_type}: {resource}') + url_suffix = f'explore/enrich/{resource_type}/{resource}' + + params = { + 'explain': 1 if explain else 0, + 'scan_data': 1 if scan_data else 0 + } + + try: + response = self._http_request( + method='GET', + url_suffix=url_suffix, + params=params + ) + demisto.debug(f'Enrichment response: {response}') + return response + + except Exception as e: + raise DemistoException(f'Failed to fetch enrichment data for {resource_type} {resource}: {str(e)}') + + + - return results @@ -510,6 +552,106 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: ) +def get_enrichment_data_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + Command handler for fetching enrichment data for a resource (domain, IPv4, or IPv6). + + Args: + client (Client): The client instance to use for API calls + args (Dict[str, Any]): Command arguments + - resource (str): The resource to get enrichment data for (domain name, IPv4, or IPv6) + - resource_type (str): Type of resource ('domain', 'ipv4', 'ipv6') + - explain (bool, optional): Whether to show calculation details + - scan_data (bool, optional): Whether to include scan data + + Returns: + CommandResults: The formatted results for XSOAR + """ + resource = args.get('resource') + resource_type = args.get('resource_type') + + if not resource or not resource_type: + raise ValueError('"resource" and "resource_type" arguments are required') + + + if resource_type not in {'domain', 'ipv4', 'ipv6'}: + raise ValueError("'resource_type' must be one of: 'domain', 'ipv4', 'ipv6'") + + explain = argToBoolean(args.get('explain', False)) + scan_data = argToBoolean(args.get('scan_data', False)) + + demisto.debug(f'Processing enrichment data for {resource_type}: {resource}') + + try: + raw_response = client.get_enrichment_data( + resource=resource, + resource_type=resource_type, + explain=explain, + scan_data=scan_data + ) + + if not raw_response: + return CommandResults( + readable_output=f'No enrichment data found for {resource_type}: {resource}', + outputs_prefix='SilentPush.Enrichment', + outputs_key_field='resource', + outputs={ + 'resource': resource, + 'type': resource_type, + 'found': False + } + ) + + markdown = [] + markdown.append(f'## Enrichment Data for {resource_type}: {resource}\n') + + enrichment_data = raw_response.get('response', {}) + + + overview = { + 'Resource': resource, + 'Type': resource_type + } + + if 'risk_score' in enrichment_data: + overview['Risk Score'] = enrichment_data['risk_score'] + if 'first_seen' in enrichment_data: + overview['First Seen'] = enrichment_data['first_seen'] + if 'last_seen' in enrichment_data: + overview['Last Seen'] = enrichment_data['last_seen'] + + markdown.append(tableToMarkdown('Overview', [overview])) + + if scan_data and enrichment_data.get('scan_data'): + scan_info = enrichment_data['scan_data'] + markdown.append('\n### Scan Data') + markdown.append(tableToMarkdown('', [scan_info])) + + if explain and enrichment_data.get('explanation'): + explain_info = enrichment_data['explanation'] + markdown.append('\n### Score Explanation') + markdown.append(tableToMarkdown('', [explain_info])) + + readable_output = '\n'.join(markdown) + + outputs = { + 'resource': resource, + 'type': resource_type, + 'found': True, + 'data': enrichment_data + } + + return CommandResults( + outputs_prefix='SilentPush.Enrichment', + outputs_key_field='resource', + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + demisto.error(f'Failed to get enrichment data: {str(e)}') + raise ''' MAIN FUNCTION ''' @@ -552,6 +694,7 @@ def main(): 'silentpush-get-domain-certificates': get_domain_certificates_command, 'silentpush-search-domains': search_domains_command, 'silentpush-list-domain-infratags': list_domain_infratags_command, + 'silentpush-get-enrichment-data' : get_enrichment_data_command } if command in command_handlers: From 67eae2dec4523b421c3023ce5fd6623b6a6c7dfb Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Sat, 11 Jan 2025 22:19:47 +0530 Subject: [PATCH 07/19] added code for ist-ip-information --- .../Integrations/SilentPush/SilentPush.py | 145 +++++++++- .../Integrations/SilentPush/SilentPush.yml | 264 ++++++++++-------- 2 files changed, 293 insertions(+), 116 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index d48da8204d5c..be53a323b764 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -355,7 +355,66 @@ def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = raise DemistoException(f'Failed to fetch enrichment data for {resource_type} {resource}: {str(e)}') - + def list_ip_information(self, ips: Union[str, List[str]], explain: bool = False, scan_data: bool = False, sparse: Optional[str] = None) -> Dict: + """ + Fetches information for both IPv4 and IPv6 addresses. + + Args: + ips: Either a single IP string or a list of IP strings + explain: Whether to show details of data used to calculate scores + scan_data: Whether to include scan data (IPv4 only) + sparse: Optional specific data to return ('asn', 'asname', or 'sp_risk_score') + + Returns: + Dict: Combined results for all IP addresses + """ + if isinstance(ips, str): + ip_list = [ip.strip() for ip in ips.split(',')] + else: + ip_list = ips + + if len(ip_list) > 100: + raise DemistoException("Maximum of 100 IPs can be submitted in a single request") + + results = [] + + for ip in ip_list: + try: + # Determine if IPv4 or IPv6 based on presence of colons + is_ipv6 = ':' in ip + + # Build parameters + params = { + 'explain': 1 if explain else 0 + } + if sparse: + params['sparse'] = sparse + if not is_ipv6 and scan_data: + params['scan_data'] = 1 + + url_suffix = f"explore/{'ipv6' if is_ipv6 else 'ipv4'}/ipv{6 if is_ipv6 else 4}info/{ip}" + + response = self._http_request( + method='GET', + url_suffix=url_suffix, + params=params + ) + + ip_info = response.get('response', {}).get('ip2asn', [{}])[0] + + ip_info['ip_type'] = 'ipv6' if is_ipv6 else 'ipv4' + + results.append(ip_info) + + except Exception as e: + demisto.error(f"Error fetching information for IP {ip}: {str(e)}") + results.append({ + 'ip': ip, + 'ip_type': 'ipv6' if ':' in ip else 'ipv4', + 'error': str(e) + }) + + return {'ips': results} @@ -652,6 +711,87 @@ def get_enrichment_data_command(client: Client, args: Dict[str, Any]) -> Command except Exception as e: demisto.error(f'Failed to get enrichment data: {str(e)}') raise + + +def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + Command handler for fetching IP information. + + Args: + client (Client): The client instance to fetch the data + args (dict): Command arguments including: + - ips (str): Comma-separated list of IP addresses + - explain (bool): Whether to show calculation details + - scan_data (bool): Whether to include scan data (IPv4 only) + - sparse (str): Optional specific data to return + + Returns: + CommandResults: XSOAR command results + """ + + ips = args.get('ips') + if not ips: + raise DemistoException('No IPs provided. Please provide IPs using the "ips" argument.') + + explain = argToBoolean(args.get('explain', False)) + scan_data = argToBoolean(args.get('scan_data', False)) + sparse = args.get('sparse') + + if sparse and sparse not in ['asn', 'asname', 'sp_risk_score']: + raise DemistoException('Invalid sparse value. Must be one of: asn, asname, sp_risk_score') + + try: + + raw_response = client.list_ip_information(ips, explain, scan_data, sparse) + ip_data = raw_response.get('ips', []) + + markdown = ['### IP Information Results\n'] + + for ip_info in ip_data: + if 'error' in ip_info: + markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} (Error)\n") + markdown.append(f"Error: {ip_info['error']}\n") + continue + + markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} ({ip_info.get('ip_type', 'unknown').upper()})") + + basic_info = { + 'ASN': ip_info.get('asn', 'N/A'), + 'AS Name': ip_info.get('asname', 'N/A'), + 'Risk Score': ip_info.get('sp_risk_score', 'N/A'), + 'Subnet': ip_info.get('subnet', 'N/A') + } + markdown.append(tableToMarkdown('Basic Information', [basic_info], headers=basic_info.keys())) + + if location_info := ip_info.get('ip_location', {}): + location_data = { + 'Country': location_info.get('country_name', 'N/A'), + 'Continent': location_info.get('continent_name', 'N/A'), + 'EU Member': 'Yes' if location_info.get('country_is_in_european_union') else 'No' + } + markdown.append(tableToMarkdown('Location Information', [location_data], headers=location_data.keys())) + + if ip_info.get('ip_type') == 'ipv4': + additional_info = { + 'PTR Record': ip_info.get('ip_ptr', 'N/A'), + 'Is TOR Exit Node': 'Yes' if ip_info.get('ip_is_tor_exit_node') else 'No', + 'Is DSL/Dynamic': 'Yes' if ip_info.get('ip_is_dsl_dynamic') else 'No' + } + markdown.append(tableToMarkdown('Additional Information', [additional_info], headers=additional_info.keys())) + + markdown.append('\n') + + return CommandResults( + outputs_prefix='SilentPush.IP', + outputs_key_field='ip', + outputs=ip_data, + readable_output='\n'.join(markdown), + raw_response=raw_response + ) + + except Exception as e: + demisto.error(f"Error in list_ip_information_command: {str(e)}") + raise ''' MAIN FUNCTION ''' @@ -694,7 +834,8 @@ def main(): 'silentpush-get-domain-certificates': get_domain_certificates_command, 'silentpush-search-domains': search_domains_command, 'silentpush-list-domain-infratags': list_domain_infratags_command, - 'silentpush-get-enrichment-data' : get_enrichment_data_command + 'silentpush-get-enrichment-data' : get_enrichment_data_command, + 'silentpush-list-ip-information' : list_ip_information_command } if command in command_handlers: diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index d924cba47cc9..2d6c91c5c8d9 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -1,124 +1,160 @@ commonfields: id: SilentPush version: -1 +name: SilentPush +display: SilentPush +category: Data Enrichment & Threat Intelligence +description: Integration with SilentPush API for domain and IP intelligence +configuration: + - display: API Key + name: credentials + type: 9 + required: true + - display: Base URL + name: url + defaultvalue: https://api.silentpush.com + type: 0 + required: true + - display: Trust any certificate (not secure) + name: insecure + type: 8 + required: false + - display: Use system proxy settings + name: proxy + type: 8 + required: false +script: type: python subtype: python3 - tags: [] - name: SilentPush - description: Integration with SilentPush API to retrieve domain information and risk scores. - enabled: true - author: Your Name - comments: Integration for fetching domain information, certificates, and more from SilentPush. - -scripts: - - id: list-domain-information - name: List Domain Information - description: Fetches information about domains, including WHOIS data and risk scores. - type: python - args: - - default: false - isArray: true - isRequired: false - name: domains - description: Comma-separated list of domains to fetch information for. - command: | - list_domain_information_command(client=client, args=args) + script: '' + commands: + - name: silentpush-list-domain-information + arguments: + - name: domains + description: Comma-separated list of domains to fetch information for + required: true + - name: domain + description: Single domain to fetch information for (alternative to domains) + required: false + outputs: + - contextPath: SilentPush.Domain + description: Domain information including WHOIS data and risk scores + type: unknown + description: Fetches domain information including WHOIS data and risk scores + execution: false - - id: get-domain-certificates - name: Get Domain Certificates - description: Fetches SSL/TLS certificates for the specified domain. - type: python - args: - - default: true - isArray: false - isRequired: true - name: domain - description: The domain to fetch certificate data for. - command: | - get_domain_certificates_command(client=client, args=args) + - name: silentpush-get-domain-certificates + arguments: + - name: domain + description: Domain to fetch certificate information for + required: true + outputs: + - contextPath: SilentPush.Certificates + description: Certificate information for the domain + type: unknown + description: Fetches SSL/TLS certificate data for a given domain + execution: false - - id: search-domains - name: Search Domains - description: Searches for domains with optional filters like risk score, registration date, etc. - type: python - args: - - default: false - isArray: false - isRequired: false - name: query - description: Optional search query for domains. - - default: false - isArray: false - isRequired: false - name: start_date - description: Optional start date for filtering. - - default: false - isArray: false - isRequired: false - name: end_date - description: Optional end date for filtering. - - default: false - isArray: false - isRequired: false - name: risk_score_min - description: Minimum risk score filter. - - default: false - isArray: false - isRequired: false - name: risk_score_max - description: Maximum risk score filter. - - default: false - isArray: false - isRequired: false - name: limit - description: Limit the number of results returned. - command: | - search_domains_command(client=client, args=args) + - name: silentpush-search-domains + arguments: + - name: query + description: Search query string (e.g., domain pattern, keywords) + required: false + - name: start_date + description: Start date for domain registration (ISO8601 format) + required: false + - name: end_date + description: End date for domain registration (ISO8601 format) + required: false + - name: risk_score_min + description: Minimum risk score filter + required: false + - name: risk_score_max + description: Maximum risk score filter + required: false + - name: limit + description: Maximum number of results to return (default 100) + required: false + outputs: + - contextPath: SilentPush.SearchResults + description: Domain search results + type: unknown + description: Search for domains with optional filters + execution: false -test: - id: test-module - name: Test Module - description: Tests the connectivity to the SilentPush API and verifies the API key. - type: python - command: | - test_module(client=client) + - name: silentpush-list-domain-infratags + arguments: + - name: domains + description: List of domains to fetch infratags for + required: true + - name: cluster + description: Whether to cluster the results + required: false + - name: mode + description: Mode for lookup (live/padns) + required: false + - name: match + description: Handling of self-hosted infrastructure (self/full) + required: false + - name: as_of + description: Date or timestamp for filtering data + required: false + outputs: + - contextPath: SilentPush.InfraTags + description: Infratags for the provided domains + type: unknown + description: Get infratags for multiple domains + execution: false + + - name: silentpush-get-enrichment-data + arguments: + - name: resource + description: Resource to get enrichment data for (domain name, IPv4, or IPv6) + required: true + - name: resource_type + description: Type of resource (domain/ipv4/ipv6) + required: true + - name: explain + description: Whether to show calculation details + required: false + - name: scan_data + description: Whether to include scan data + required: false + outputs: + - contextPath: SilentPush.Enrichment + description: Enrichment data for the resource + type: unknown + description: Retrieve enrichment data for a resource + execution: false + + - name: silentpush-list-ip-information + arguments: + - name: ips + description: Comma-separated list of IP addresses + required: true + - name: explain + description: Whether to show calculation details + required: false + - name: scan_data + description: Whether to include scan data (IPv4 only) + required: false + - name: sparse + description: Specific data to return (asn/asname/sp_risk_score) + required: false + outputs: + - contextPath: SilentPush.IP + description: IP information and analysis + type: unknown + description: Fetches information for IPv4 and IPv6 addresses + execution: false -configurations: - - id: SilentPush_Config - name: SilentPush Configuration - description: Configuration for connecting to SilentPush API. - type: python - enabled: true - isArray: false - isRequired: true - fields: - - id: api_key - name: API Key - description: API Key for SilentPush. - isArray: false - isRequired: true - - id: base_url - name: Base URL - description: The base URL for the SilentPush API. - isArray: false - isRequired: true - - id: verify_ssl - name: Verify SSL - description: Whether to verify SSL certificates. - isArray: false - isRequired: false - - id: proxy - name: Use Proxy - description: Whether to use a proxy server. - isArray: false - isRequired: false + - name: test-module + description: Validates the API connection and authentication + execution: false -outputs: - - id: SilentPush.Domain - contextPath: SilentPush.Domain - type: array - description: Domain information and risk scores. - - id: SilentPush.Certificates - contextPath: SilentPush.Certificates - type: array - description: SSL/TLS certificate data. + dockerimage: demisto/python3:3.9 + runonce: false + isfetch: false + longRunning: false + longRunningPort: false + feed: false \ No newline at end of file From 41d05c36b85fb359d48b6923f48a7f2bf79fb4a4 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 17 Jan 2025 14:44:04 +0530 Subject: [PATCH 08/19] updated commands --- .../Integrations/SilentPush/SilentPush.py | 520 ++++++++---------- 1 file changed, 229 insertions(+), 291 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index be53a323b764..da29c0bdbb78 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -104,91 +104,75 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: demisto.error(f'Error in API call: {str(e)}') raise - def list_domain_information(self, domains: Union[str, List[str]]) -> Dict: + def list_domain_information(self, domains: List[str]) -> Dict: """ - Fetches domain information including WHOIS data and risk scores for multiple domains. - + Fetches domain information including WHOIS data, risk scores, and live WHOIS for multiple domains. + Args: - domains: Either a single domain string or a list of domain strings - + domains: List of domain strings + Returns: - Dict: A dictionary containing combined domain information and risk scores + Dict: A dictionary containing combined domain information, risk scores, and live WHOIS information """ - demisto.debug(f'Fetching domain information for: {domains}') - domain_list = [domains] if isinstance(domains, str) else domains - - if len(domain_list) > 100: + if len(domains) > 100: raise DemistoException("Maximum of 100 domains can be submitted in a single request") - - if len(domain_list) == 1: - domain = domain_list[0] - try: + try: + + domains_data = {'domains': domains} - domain_info_response = self._http_request( - method='GET', - url_suffix=f'explore/domain/domaininfo/{domain}' - ) - domain_info = domain_info_response.get('response', {}).get('domaininfo', {}) - - risk_score_response = self._http_request( + + bulk_info_response = self._http_request( + method='POST', + url_suffix='explore/bulk/domaininfo', + data=domains_data + ) + + + bulk_risk_response = self._http_request( + method='POST', + url_suffix='explore/bulk/domain/riskscore', + data=domains_data + ) + + + live_whois_info = {} + for domain in domains: + live_whois_response = self._http_request( method='GET', - url_suffix=f'explore/domain/riskscore/{domain}' + url_suffix=f'explore/domain/whoislive/{domain}' ) - risk_info = risk_score_response.get('response', {}) + live_whois_info[domain] = live_whois_response.get('response', {}) + + domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) + risk_score_list = bulk_risk_response.get('response', []) + + + domain_info_dict = {item['domain']: item for item in domain_info_list} + risk_score_dict = {item['domain']: item for item in risk_score_list} + + + combined_results = [] + for domain in domains: + domain_data = domain_info_dict.get(domain, {}) + risk_data = risk_score_dict.get(domain, {}) + whois_data = live_whois_info.get(domain, {}) + - combined_info = { + combined_results.append({ 'domain': domain, - **domain_info, - 'sp_risk_score': risk_info.get('sp_risk_score'), - 'sp_risk_score_explain': risk_info.get('sp_risk_score_explain') - } - - return {'domains': [combined_info]} - - except Exception as e: - raise DemistoException(f'Failed to fetch information for domain {domain}: {str(e)}') - - else: + **domain_data, + 'sp_risk_score': risk_data.get('sp_risk_score'), + 'sp_risk_score_explain': risk_data.get('sp_risk_score_explain'), + 'whois_info': whois_data + }) + + return {'domains': combined_results} + + except Exception as e: + raise DemistoException(f'Failed to fetch bulk domain information: {str(e)}') - try: - - domains_data = {'domains': domain_list} - bulk_info_response = self._http_request( - method='POST', - url_suffix='explore/bulk/domaininfo', - data=domains_data - ) - - bulk_risk_response = self._http_request( - method='POST', - url_suffix='explore/bulk/domain/riskscore', - data=domains_data - ) - - domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) - risk_score_list = bulk_risk_response.get('response', []) - domain_info_dict = {item['domain']: item for item in domain_info_list} - risk_score_dict = {item['domain']: item for item in risk_score_list} - - combined_results = [] - for domain in domain_list: - domain_data = domain_info_dict.get(domain, {}) - risk_data = risk_score_dict.get(domain, {}) - - combined_results.append({ - 'domain': domain, - **domain_data, - 'sp_risk_score': risk_data.get('sp_risk_score'), - 'sp_risk_score_explain': risk_data.get('sp_risk_score_explain') - }) - - return {'domains': combined_results} - - except Exception as e: - raise DemistoException(f'Failed to fetch bulk domain information: {str(e)}') - def get_domain_certificates(self, domain: str) -> dict: """ Fetches SSL/TLS certificate data for a given domain. @@ -279,8 +263,8 @@ def search_domains(self, return self._http_request('GET', url_suffix, params=params) except Exception as e: demisto.error(f"Error in search_domains API request: {str(e)}") - - 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: + + def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str = 'live', match: str = 'self', as_of: Optional[str] = None) -> dict: """ Get infratags for multiple domains with optional clustering and additional filtering options. @@ -296,23 +280,36 @@ def list_domain_infratags(self, domains: list, cluster: Optional[bool] = False, """ demisto.debug(f'Fetching infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') - - results = {} - for domain in domains: - url = f'https://api.silentpush.com/api/v1/merge-api/explore/domain/infratag/{domain}' - data = { - 'cluster': cluster, - 'mode': mode, - 'match': match, - 'as_of': as_of - } - try: - - response = self._http_request('GET', url, params=data) - results[domain] = response - except Exception as e: - demisto.error(f"Error fetching infratags for domain {domain}: {str(e)}") - + + url = 'explore/bulk/domain/infratags' + + + payload = { + 'domains': domains + } + params = { + 'mode': mode, + 'match': match, + 'clusters': 1 if cluster else 0 + } + + if as_of: + params['as_of'] = as_of + + try: + + response = self._http_request( + method='POST', + url_suffix=url, + params=params, + data=payload + ) + return response + except Exception as e: + # Log error if the request fails + demisto.error(f"Error fetching infratags: {str(e)}") + raise + def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = False, scan_data: bool = False) -> Dict: """ @@ -326,14 +323,14 @@ def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = Returns: Dict: The enrichment data response from the API - + Raises: ValueError: If resource_type is not one of 'domain', 'ipv4', 'ipv6' DemistoException: If the API request fails """ if resource_type not in {'domain', 'ipv4', 'ipv6'}: raise ValueError("resource_type must be one of: 'domain', 'ipv4', 'ipv6'") - + demisto.debug(f'Fetching enrichment data for {resource_type}: {resource}') url_suffix = f'explore/enrich/{resource_type}/{resource}' @@ -350,72 +347,45 @@ def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = ) demisto.debug(f'Enrichment response: {response}') return response - + except Exception as e: raise DemistoException(f'Failed to fetch enrichment data for {resource_type} {resource}: {str(e)}') - def list_ip_information(self, ips: Union[str, List[str]], explain: bool = False, scan_data: bool = False, sparse: Optional[str] = None) -> Dict: + def list_ip_information(self, resource: str, explain: bool = False, scan_data: bool = False, sparse: Optional[str] = None) -> Dict: """ - Fetches information for both IPv4 and IPv6 addresses. + Fetches information for an IP address or domain. Args: - ips: Either a single IP string or a list of IP strings + resource: The IP or domain resource to query explain: Whether to show details of data used to calculate scores scan_data: Whether to include scan data (IPv4 only) sparse: Optional specific data to return ('asn', 'asname', or 'sp_risk_score') Returns: - Dict: Combined results for all IP addresses + Dict: Results for the requested IP or domain """ - if isinstance(ips, str): - ip_list = [ip.strip() for ip in ips.split(',')] - else: - ip_list = ips - - if len(ip_list) > 100: - raise DemistoException("Maximum of 100 IPs can be submitted in a single request") - results = [] + params = { + 'ips': [resource], + 'explain': 1 if explain else 0, + 'scan_data': 1 if scan_data else 0, + 'sparse': sparse if sparse else '' + } - for ip in ip_list: - try: - # Determine if IPv4 or IPv6 based on presence of colons - is_ipv6 = ':' in ip - - # Build parameters - params = { - 'explain': 1 if explain else 0 - } - if sparse: - params['sparse'] = sparse - if not is_ipv6 and scan_data: - params['scan_data'] = 1 - - url_suffix = f"explore/{'ipv6' if is_ipv6 else 'ipv4'}/ipv{6 if is_ipv6 else 4}info/{ip}" - - response = self._http_request( - method='GET', - url_suffix=url_suffix, - params=params - ) - - ip_info = response.get('response', {}).get('ip2asn', [{}])[0] - - ip_info['ip_type'] = 'ipv6' if is_ipv6 else 'ipv4' - - results.append(ip_info) - - except Exception as e: - demisto.error(f"Error fetching information for IP {ip}: {str(e)}") - results.append({ - 'ip': ip, - 'ip_type': 'ipv6' if ':' in ip else 'ipv4', - 'error': str(e) - }) + url_suffix = f"explore/bulk/ip2asn/{resource}" - return {'ips': results} - + try: + response = self._http_request( + method='POST', + url_suffix=url_suffix, + data=params + ) + + return response + except Exception as e: + demisto.error(f"Error fetching information for resource {resource}: {str(e)}") + return {'error': str(e)} @@ -448,55 +418,59 @@ def test_module(client: Client) -> str: def list_domain_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching domain information. - - Args: - client (Client): The client instance to fetch the data - args (dict): Command arguments - - Returns: - CommandResults: XSOAR command results - """ - domains_arg = args.get('domains', args.get('domain')) - if not domains_arg: raise DemistoException('No domains provided. Please provide domains using either the "domain" or "domains" argument.') - - + if isinstance(domains_arg, str): domains = [d.strip() for d in domains_arg.split(',')] else: domains = domains_arg - - demisto.debug(f'Processing domain(s): {domains}') - - + + if len(domains) > 100: + raise DemistoException("Maximum of 100 domains can be submitted in a single request") + + demisto.debug(f'Processing domains in bulk: {domains}') raw_response = client.list_domain_information(domains) demisto.debug(f'Response from API: {raw_response}') - - - markdown = [] + + markdown = ['# Domain Information Results\n'] + for domain_info in raw_response.get('domains', []): - markdown.append(f"### Domain: {domain_info.get('domain')}") - markdown.append("#### Domain Information:") - markdown.append(f"- Age: {domain_info.get('age', 'N/A')} days") - markdown.append(f"- Registrar: {domain_info.get('registrar', 'N/A')}") - markdown.append(f"- Created Date: {domain_info.get('whois_created_date', 'N/A')}") - markdown.append(f"- Risk Score: {domain_info.get('sp_risk_score', 'N/A')}") - markdown.append("\n") - - readable_output = '\n'.join(markdown) - + markdown.append(f"## Domain: {domain_info.get('domain')}") + + + basic_info = { + 'Created Date': str(domain_info.get('whois_created_date', 'N/A')), + 'Registrar': str(domain_info.get('registrar', 'N/A')), + 'Age (days)': str(domain_info.get('age', 'N/A')), + 'Risk Score': str(domain_info.get('sp_risk_score', 'N/A')) + } + markdown.append(tableToMarkdown('Domain Information', [basic_info])) + + + risk_explain = str(domain_info.get('sp_risk_score_explain', 'N/A')) + if risk_explain != 'N/A': + markdown.append(f'### Risk Score Explanation\n{risk_explain}') + + + whois_info = domain_info.get('whois_info', {}) + if whois_info: + whois_info_list = [{'Key': k, 'Value': v} for k, v in whois_info.items()] + markdown.append(tableToMarkdown('WHOIS Information', whois_info_list)) + + markdown.append('\n---\n') + return CommandResults( outputs_prefix='SilentPush.Domain', outputs_key_field='domain', outputs=raw_response.get('domains', []), - readable_output=readable_output, + readable_output='\n'.join(markdown), raw_response=raw_response ) + + def get_domain_certificates_command(client: Client, args: dict) -> CommandResults: """ Command handler for fetching domain certificate information. @@ -569,7 +543,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: readable_output=readable_output, raw_response=raw_response ) - + def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: """ Command handler for fetching infratags for multiple domains. @@ -581,27 +555,34 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: Returns: CommandResults: The command results containing readable output and the raw response. """ - + domains = argToList(args.get('domains', '')) - cluster = argToBoolean(args.get('cluster', False)) + cluster = argToBoolean(args.get('cluster', False)) mode = args.get('mode', 'live') match = args.get('match', 'self') as_of = args.get('as_of', None) + if not domains: raise ValueError('"domains" argument is required and cannot be empty.') + demisto.debug(f'Processing infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') try: + raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) demisto.debug(f'Response from API: {raw_response}') except Exception as e: + demisto.error(f'Error occurred while fetching infratags: {str(e)}') raise - readable_output = tableToMarkdown('Domain Infratags', raw_response.get('results', [])) + + infratags = raw_response.get('response', {}).get('infratags', []) + readable_output = tableToMarkdown('Domain Infratags', infratags) + return CommandResults( outputs_prefix='SilentPush.InfraTags', outputs_key_field='domain', @@ -610,109 +591,57 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) + def get_enrichment_data_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ - Command handler for fetching enrichment data for a resource (domain, IPv4, or IPv6). + Command handler for fetching enrichment data for a specific resource. Args: - client (Client): The client instance to use for API calls - args (Dict[str, Any]): Command arguments - - resource (str): The resource to get enrichment data for (domain name, IPv4, or IPv6) - - resource_type (str): Type of resource ('domain', 'ipv4', 'ipv6') - - explain (bool, optional): Whether to show calculation details - - scan_data (bool, optional): Whether to include scan data - + client (Client): The client instance to fetch the data + args (dict): Command arguments including: + - resource (str): The resource (e.g., domain, IP address, etc.) + - resource_type (str): The type of resource ('domain', 'ip', etc.) + - explain (bool): Whether to show calculation details + - scan_data (bool): Whether to include scan data (IPv4 only) + Returns: - CommandResults: The formatted results for XSOAR + CommandResults: XSOAR command results """ + resource = args.get('resource') - resource_type = args.get('resource_type') + resource_type = args.get('resource_type') if not resource or not resource_type: - raise ValueError('"resource" and "resource_type" arguments are required') - - - if resource_type not in {'domain', 'ipv4', 'ipv6'}: - raise ValueError("'resource_type' must be one of: 'domain', 'ipv4', 'ipv6'") - + raise DemistoException('Resource and resource_type are required arguments.') + explain = argToBoolean(args.get('explain', False)) scan_data = argToBoolean(args.get('scan_data', False)) - demisto.debug(f'Processing enrichment data for {resource_type}: {resource}') - try: - raw_response = client.get_enrichment_data( - resource=resource, - resource_type=resource_type, - explain=explain, - scan_data=scan_data - ) + raw_response = client.get_enrichment_data(resource, resource_type, explain, scan_data) + enrichment_data = raw_response.get('data', []) - if not raw_response: - return CommandResults( - readable_output=f'No enrichment data found for {resource_type}: {resource}', - outputs_prefix='SilentPush.Enrichment', - outputs_key_field='resource', - outputs={ - 'resource': resource, - 'type': resource_type, - 'found': False - } - ) - - markdown = [] - markdown.append(f'## Enrichment Data for {resource_type}: {resource}\n') + markdown = [f"### Enrichment Data for {resource} ({resource_type})\n"] - enrichment_data = raw_response.get('response', {}) - - - overview = { - 'Resource': resource, - 'Type': resource_type - } - - if 'risk_score' in enrichment_data: - overview['Risk Score'] = enrichment_data['risk_score'] - if 'first_seen' in enrichment_data: - overview['First Seen'] = enrichment_data['first_seen'] - if 'last_seen' in enrichment_data: - overview['Last Seen'] = enrichment_data['last_seen'] - - markdown.append(tableToMarkdown('Overview', [overview])) - - if scan_data and enrichment_data.get('scan_data'): - scan_info = enrichment_data['scan_data'] - markdown.append('\n### Scan Data') - markdown.append(tableToMarkdown('', [scan_info])) - - if explain and enrichment_data.get('explanation'): - explain_info = enrichment_data['explanation'] - markdown.append('\n### Score Explanation') - markdown.append(tableToMarkdown('', [explain_info])) - - readable_output = '\n'.join(markdown) - - outputs = { - 'resource': resource, - 'type': resource_type, - 'found': True, - 'data': enrichment_data - } + for data in enrichment_data: + markdown.append(f"#### Enrichment Data:\n") + markdown.append(f"Data: {data}\n") return CommandResults( outputs_prefix='SilentPush.Enrichment', outputs_key_field='resource', - outputs=outputs, - readable_output=readable_output, + outputs=enrichment_data, + readable_output='\n'.join(markdown), raw_response=raw_response ) except Exception as e: - demisto.error(f'Failed to get enrichment data: {str(e)}') + demisto.error(f"Error in get_enrichment_data_command: {str(e)}") raise - - + + + def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ Command handler for fetching IP information. @@ -741,45 +670,53 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command raise DemistoException('Invalid sparse value. Must be one of: asn, asname, sp_risk_score') try: - - raw_response = client.list_ip_information(ips, explain, scan_data, sparse) - ip_data = raw_response.get('ips', []) + ip_list = [ip.strip() for ip in ips.split(',')] + + results = [] markdown = ['### IP Information Results\n'] - for ip_info in ip_data: - if 'error' in ip_info: - markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} (Error)\n") - markdown.append(f"Error: {ip_info['error']}\n") - continue - - markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} ({ip_info.get('ip_type', 'unknown').upper()})") - - basic_info = { - 'ASN': ip_info.get('asn', 'N/A'), - 'AS Name': ip_info.get('asname', 'N/A'), - 'Risk Score': ip_info.get('sp_risk_score', 'N/A'), - 'Subnet': ip_info.get('subnet', 'N/A') - } - markdown.append(tableToMarkdown('Basic Information', [basic_info], headers=basic_info.keys())) - - if location_info := ip_info.get('ip_location', {}): - location_data = { - 'Country': location_info.get('country_name', 'N/A'), - 'Continent': location_info.get('continent_name', 'N/A'), - 'EU Member': 'Yes' if location_info.get('country_is_in_european_union') else 'No' - } - markdown.append(tableToMarkdown('Location Information', [location_data], headers=location_data.keys())) + + for ip in ip_list: + resource = ip + + + raw_response = client.list_ip_information(resource, explain, scan_data, sparse) + ip_data = raw_response.get('ips', []) - if ip_info.get('ip_type') == 'ipv4': - additional_info = { - 'PTR Record': ip_info.get('ip_ptr', 'N/A'), - 'Is TOR Exit Node': 'Yes' if ip_info.get('ip_is_tor_exit_node') else 'No', - 'Is DSL/Dynamic': 'Yes' if ip_info.get('ip_is_dsl_dynamic') else 'No' + for ip_info in ip_data: + if 'error' in ip_info: + markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} (Error)\n") + markdown.append(f"Error: {ip_info['error']}\n") + continue + + markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} ({ip_info.get('ip_type', 'unknown').upper()})") + + basic_info = { + 'ASN': ip_info.get('asn', 'N/A'), + 'AS Name': ip_info.get('asname', 'N/A'), + 'Risk Score': ip_info.get('sp_risk_score', 'N/A'), + 'Subnet': ip_info.get('subnet', 'N/A') } - markdown.append(tableToMarkdown('Additional Information', [additional_info], headers=additional_info.keys())) - - markdown.append('\n') + markdown.append(tableToMarkdown('Basic Information', [basic_info], headers=basic_info.keys())) + + if location_info := ip_info.get('ip_location', {}): + location_data = { + 'Country': location_info.get('country_name', 'N/A'), + 'Continent': location_info.get('continent_name', 'N/A'), + 'EU Member': 'Yes' if location_info.get('country_is_in_european_union') else 'No' + } + markdown.append(tableToMarkdown('Location Information', [location_data], headers=location_data.keys())) + + if ip_info.get('ip_type') == 'ipv4': + additional_info = { + 'PTR Record': ip_info.get('ip_ptr', 'N/A'), + 'Is TOR Exit Node': 'Yes' if ip_info.get('ip_is_tor_exit_node') else 'No', + 'Is DSL/Dynamic': 'Yes' if ip_info.get('ip_is_dsl_dynamic') else 'No' + } + markdown.append(tableToMarkdown('Additional Information', [additional_info], headers=additional_info.keys())) + + markdown.append('\n') return CommandResults( outputs_prefix='SilentPush.IP', @@ -793,6 +730,7 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command demisto.error(f"Error in list_ip_information_command: {str(e)}") raise + ''' MAIN FUNCTION ''' From 1b7776f003aee524c09aea7df5b4fa40fdec90d9 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Mon, 20 Jan 2025 10:17:53 +0530 Subject: [PATCH 09/19] updated code --- .../Integrations/SilentPush/SilentPush.py | 566 ++++++++++++++---- 1 file changed, 438 insertions(+), 128 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index da29c0bdbb78..53587a05e794 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -104,135 +104,156 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: demisto.error(f'Error in API call: {str(e)}') raise - def list_domain_information(self, domains: List[str]) -> Dict: + def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: """ - Fetches domain information including WHOIS data, risk scores, and live WHOIS for multiple domains. + Fetches domain information, including WHOIS data, risk scores, and live WHOIS for multiple domains. Args: - domains: List of domain strings + domains: List of domain strings. + fetch_risk_score: Whether to fetch risk scores (default: False). + fetch_whois_info: Whether to fetch live WHOIS information (default: False). Returns: - Dict: A dictionary containing combined domain information, risk scores, and live WHOIS information + Dict: A dictionary containing combined domain information, risk scores, and live WHOIS information. """ if len(domains) > 100: - raise DemistoException("Maximum of 100 domains can be submitted in a single request") + raise DemistoException("Maximum of 100 domains can be submitted in a single request.") try: domains_data = {'domains': domains} - + bulk_info_response = self._http_request( method='POST', url_suffix='explore/bulk/domaininfo', data=domains_data ) - - bulk_risk_response = self._http_request( - method='POST', - url_suffix='explore/bulk/domain/riskscore', - data=domains_data - ) + + domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) + domain_info_dict = {item['domain']: item for item in domain_info_list} + combined_results = [] - live_whois_info = {} - for domain in domains: - live_whois_response = self._http_request( - method='GET', - url_suffix=f'explore/domain/whoislive/{domain}' + risk_score_dict = {} + if fetch_risk_score: + bulk_risk_response = self._http_request( + method='POST', + url_suffix='explore/bulk/domain/riskscore', + data=domains_data ) - live_whois_info[domain] = live_whois_response.get('response', {}) - - domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) - risk_score_list = bulk_risk_response.get('response', []) + risk_score_list = bulk_risk_response.get('response', []) + risk_score_dict = {item['domain']: item for item in risk_score_list} - domain_info_dict = {item['domain']: item for item in domain_info_list} - risk_score_dict = {item['domain']: item for item in risk_score_list} + live_whois_info = {} + if fetch_whois_info: + for domain in domains: + try: + live_whois_response = self._http_request( + method='GET', + url_suffix=f'explore/domain/whoislive/{domain}' + ) + live_whois_info[domain] = live_whois_response.get('response', {}) + except Exception as e: + live_whois_info[domain] = {'error': f"Failed to fetch WHOIS data: {str(e)}"} - combined_results = [] for domain in domains: - domain_data = domain_info_dict.get(domain, {}) - risk_data = risk_score_dict.get(domain, {}) - whois_data = live_whois_info.get(domain, {}) - - combined_results.append({ 'domain': domain, - **domain_data, - 'sp_risk_score': risk_data.get('sp_risk_score'), - 'sp_risk_score_explain': risk_data.get('sp_risk_score_explain'), - 'whois_info': whois_data + **domain_info_dict.get(domain, {}), + 'sp_risk_score': risk_score_dict.get(domain, {}).get('sp_risk_score', 'N/A'), + 'sp_risk_score_explain': risk_score_dict.get(domain, {}).get('sp_risk_score_explain', 'N/A'), + 'whois_info': live_whois_info.get(domain, 'N/A') }) return {'domains': combined_results} except Exception as e: - raise DemistoException(f'Failed to fetch bulk domain information: {str(e)}') + raise DemistoException(f"Failed to fetch bulk domain information: {str(e)}") - def get_domain_certificates(self, domain: str) -> dict: + def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = None, certificate_issuer: Optional[str] = None, + date_min: Optional[str] = None, date_max: Optional[str] = None, prefer: Optional[str] = None, + max_wait: Optional[int] = None, with_metadata: Optional[bool] = False, skip: Optional[int] = 0, + limit: Optional[int] = 100) -> dict: """ Fetches SSL/TLS certificate data for a given domain. If the job is not completed, it polls the job status periodically. Args: domain (str): The domain to fetch certificate information for. + domain_regex (Optional[str]): Regular expression to match domains. + certificate_issuer (Optional[str]): The name of the certificate issuer. + date_min (Optional[str]): Filter certificates issued on or after this date. + date_max (Optional[str]): Filter certificates issued on or before this date. + prefer (Optional[str]): Prefer to wait for longer queries. + max_wait (Optional[int]): Maximum wait time in seconds. + with_metadata (Optional[bool]): Whether to include metadata. + skip (Optional[int]): Number of results to skip. + limit (Optional[int]): Maximum number of results. Returns: dict: A dictionary containing certificate information fetched from the API. """ demisto.debug(f'Fetching certificate information for domain: {domain}') - url_suffix = f'explore/domain/certificates/{domain}' - response = self._http_request('GET', url_suffix, params={ - 'limit': 100, - 'skip': 0, - 'with_metadata': 0 - }) + params = { + 'limit': limit, + 'skip': skip, + 'with_metadata': with_metadata, + 'domain_regex': domain_regex, + 'certificate_issuer': certificate_issuer, + 'date_min': date_min, + 'date_max': date_max, + 'prefer': prefer, + 'max_wait': max_wait + } + + # Remove keys with None values + params = {k: v for k, v in params.items() if v is not None} + + response = self._http_request('GET', url_suffix, params=params) - job_status_url = response.get('response', {}).get('job_status', {}).get('get') if not job_status_url: demisto.error('Job status URL not found in the response') return response - job_complete = False while not job_complete: demisto.debug(f'Checking job status at {job_status_url}') - job_response = self._http_request('GET', job_status_url) job_status = job_response.get('response', {}).get('job_status', {}).get('status') if job_status == 'COMPLETED': job_complete = True demisto.debug('Job completed, fetching certificates.') - + certificate_data = job_response.get('response', {}).get('domain_certificates', []) return certificate_data elif job_status == 'FAILED': demisto.error('Job failed to complete.') return {'error': 'Job failed'} else: - demisto.debug('Job is still in progress. Retrying...') time.sleep(5) - return {} + return {} + def search_domains(self, - query: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - risk_score_min: Optional[int] = None, - risk_score_max: Optional[int] = None, - limit: int = 100) -> dict: + query: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + risk_score_min: Optional[int] = None, + risk_score_max: Optional[int] = None, + limit: int = 100) -> dict: """ Search for domains with optional filters. @@ -260,9 +281,18 @@ def search_domains(self, }.items() if v is not None} try: - return self._http_request('GET', url_suffix, params=params) + response = self._http_request('GET', url_suffix, params=params) + + # Log job status if available + job_status = response.get('response', {}).get('job_status', {}) + if job_status: + demisto.debug(f"Job Status: {job_status.get('status', 'Unknown')}") + + return response except Exception as e: - demisto.error(f"Error in search_domains API request: {str(e)}") + demisto.error(f"Error in search_domains API request: {str(e)}") + return {'error': str(e)} + def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str = 'live', match: str = 'self', as_of: Optional[str] = None) -> dict: """ @@ -280,24 +310,21 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str """ demisto.debug(f'Fetching infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') - url = 'explore/bulk/domain/infratags' - payload = { 'domains': domains } params = { 'mode': mode, 'match': match, - 'clusters': 1 if cluster else 0 + 'clusters': cluster # Use boolean value directly } if as_of: params['as_of'] = as_of try: - response = self._http_request( method='POST', url_suffix=url, @@ -310,6 +337,7 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str demisto.error(f"Error fetching infratags: {str(e)}") raise + def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = False, scan_data: bool = False) -> Dict: """ @@ -373,7 +401,7 @@ def list_ip_information(self, resource: str, explain: bool = False, scan_data: b 'sparse': sparse if sparse else '' } - url_suffix = f"explore/bulk/ip2asn/{resource}" + url_suffix = "explore/bulk/ip2asn" try: response = self._http_request( @@ -387,6 +415,79 @@ def list_ip_information(self, resource: str, explain: bool = False, scan_data: b demisto.error(f"Error fetching information for resource {resource}: {str(e)}") return {'error': str(e)} + def get_asn_reputation(self, asn: str) -> Dict: + """ + Retrieve reputation information for an Autonomous System Number (ASN). + + Args: + asn (str): The ASN to lookup (can be with or without 'AS' prefix) + + Returns: + Dict: The reputation information response from the API + + Raises: + ValueError: If ASN is invalid + DemistoException: If the API request fails + """ + if not asn: + raise ValueError("ASN cannot be empty") + + # Strip 'AS' prefix if present and validate ASN format + asn_number = asn.upper().replace('AS', '') + if not asn_number.isdigit(): + raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") + + demisto.debug(f'Fetching reputation for ASN: {asn_number}') + + try: + url_suffix = f'explore/ipreputation/history/asn/{asn_number}' + response = self._http_request( + method='GET', + url_suffix=url_suffix + ) + + return response + + except Exception as e: + raise DemistoException(f'Failed to fetch ASN reputation for {asn}: {str(e)}') + + def get_asn_takedown_reputation(self, asn: str) -> Dict: + """ + Retrieve takedown reputation information for an Autonomous System Number (ASN). + + Args: + asn (str): The ASN to lookup (can be with or without 'AS' prefix) + + Returns: + Dict: The takedown reputation information response from the API + + Raises: + ValueError: If ASN is invalid + DemistoException: If the API request fails + """ + if not asn: + raise ValueError("ASN cannot be empty") + + # Strip 'AS' prefix if present and validate ASN format + asn_number = asn.upper().replace('AS', '') + if not asn_number.isdigit(): + raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") + + demisto.debug(f'Fetching takedown reputation for ASN: {asn_number}') + + try: + url_suffix = f'explore/ipreputation/takedown/asn/{asn_number}' + response = self._http_request( + method='GET', + url_suffix=url_suffix + ) + + return response + + except Exception as e: + raise DemistoException(f'Failed to fetch ASN takedown reputation for {asn}: {str(e)}') + + def test_module(client: Client) -> str: @@ -418,85 +519,147 @@ def test_module(client: Client) -> str: def list_domain_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: - domains_arg = args.get('domains', args.get('domain')) - if not domains_arg: - raise DemistoException('No domains provided. Please provide domains using either the "domain" or "domains" argument.') + """ + Command handler for the 'silentpush-list-domain-information' command. - if isinstance(domains_arg, str): - domains = [d.strip() for d in domains_arg.split(',')] - else: - domains = domains_arg + Args: + client (Client): The client instance for API requests. + args (Dict[str, Any]): Command arguments passed from XSOAR. + + Returns: + CommandResults: Formatted results for XSOAR. + """ + # Extract and validate domains + domains_arg = args.get('domains') or args.get('domain') + if not domains_arg: + raise DemistoException('No domains provided. Use the "domain" or "domains" argument.') + domains = [domain.strip() for domain in domains_arg.split(',') if domain.strip()] if len(domains) > 100: - raise DemistoException("Maximum of 100 domains can be submitted in a single request") + raise DemistoException("A maximum of 100 domains can be submitted in a single request.") - demisto.debug(f'Processing domains in bulk: {domains}') - raw_response = client.list_domain_information(domains) - demisto.debug(f'Response from API: {raw_response}') + # Extract optional parameters + fetch_risk_score = argToBoolean(args.get('fetch_risk_score', False)) + fetch_whois_info = argToBoolean(args.get('fetch_whois_info', False)) - markdown = ['# Domain Information Results\n'] + # Log input for debugging + demisto.debug(f"Fetching domain information for: {domains} " + f"with fetch_risk_score={fetch_risk_score}, fetch_whois_info={fetch_whois_info}") + + # Call the client method to fetch domain information + raw_response = client.list_domain_information(domains, fetch_risk_score, fetch_whois_info) + demisto.debug(f"API response: {raw_response}") + # Prepare readable output + markdown = ['# Domain Information Results\n'] for domain_info in raw_response.get('domains', []): - markdown.append(f"## Domain: {domain_info.get('domain')}") + markdown.append(f"## Domain: {domain_info.get('domain', 'N/A')}") - + # Add basic domain information basic_info = { - 'Created Date': str(domain_info.get('whois_created_date', 'N/A')), - 'Registrar': str(domain_info.get('registrar', 'N/A')), - 'Age (days)': str(domain_info.get('age', 'N/A')), - 'Risk Score': str(domain_info.get('sp_risk_score', 'N/A')) + 'Created Date': domain_info.get('whois_created_date', 'N/A'), + 'Registrar': domain_info.get('registrar', 'N/A'), + 'Age (days)': domain_info.get('age', 'N/A'), + 'Risk Score': domain_info.get('sp_risk_score', 'N/A'), } markdown.append(tableToMarkdown('Domain Information', [basic_info])) - - risk_explain = str(domain_info.get('sp_risk_score_explain', 'N/A')) - if risk_explain != 'N/A': + # Add risk score explanation if available + if risk_explain := domain_info.get('sp_risk_score_explain'): markdown.append(f'### Risk Score Explanation\n{risk_explain}') - + # Add WHOIS data if available whois_info = domain_info.get('whois_info', {}) - if whois_info: - whois_info_list = [{'Key': k, 'Value': v} for k, v in whois_info.items()] - markdown.append(tableToMarkdown('WHOIS Information', whois_info_list)) + if isinstance(whois_info, dict): + whois_table = [{'Key': k, 'Value': v} for k, v in whois_info.items()] + markdown.append(tableToMarkdown('WHOIS Information', whois_table)) + + markdown.append('\n---\n') - markdown.append('\n---\n') + readable_output = '\n'.join(markdown) + # Return command results return CommandResults( outputs_prefix='SilentPush.Domain', outputs_key_field='domain', outputs=raw_response.get('domains', []), - readable_output='\n'.join(markdown), + readable_output=readable_output, raw_response=raw_response ) - -def get_domain_certificates_command(client: Client, args: dict) -> CommandResults: +def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ - Command handler for fetching domain certificate information. - """ - domain = args.get('domain', 'silentpush.com') - demisto.debug(f'Processing certificates for domain: {domain}') - - raw_response = client.get_domain_certificates(domain) - demisto.debug(f'Response from API: {raw_response}') + Command handler for fetching SSL/TLS certificate data for a given domain. - if 'error' in raw_response: - return CommandResults( - outputs_prefix='SilentPush.Certificates', - outputs_key_field='domain', - outputs=raw_response, - readable_output=f"Error: {raw_response['error']}", - raw_response=raw_response - ) + Args: + client (Client): The client instance. + args (Dict[str, Any]): The arguments passed to the command (including the domain). - readable_output = tableToMarkdown('Domain Certificates', raw_response) + Returns: + CommandResults: The formatted result for XSOAR. + """ + domain = args.get('domain') + if not domain: + raise DemistoException('Domain argument is required.') + + # Call the client function to get the domain certificates. + demisto.debug(f'Fetching certificates for domain: {domain}') + certificate_data = client.get_domain_certificates(domain) + + if not certificate_data: + raise DemistoException(f'No certificate data found for domain: {domain}') + + # Prepare the markdown output + markdown = [f'# SSL/TLS Certificate Information for Domain: {domain}\n'] + + # Add certificate details to markdown + if isinstance(certificate_data, list) and certificate_data: + for cert in certificate_data: + markdown.append(f"## Certificate for {domain}") + cert_info = { + 'Issuer': cert.get('issuer', 'N/A'), + 'Issued On': str(cert.get('issued_on', 'N/A')), + 'Expires On': str(cert.get('expires_on', 'N/A')), + 'Common Name': cert.get('common_name', 'N/A'), + 'Subject Alternative Names': ', '.join(cert.get('subject_alt_names', [])), + } + markdown.append(tableToMarkdown('Certificate Information', [cert_info])) + + # Add metadata if available + metadata = cert.get('metadata', {}) + if metadata: + markdown.append(f"### Metadata: {metadata}") + else: + markdown.append(f'No certificate data available for domain: {domain}') + + # Add metadata and job status to the response + metadata = { + 'job_id': certificate_data.get('response', {}).get('metadata', {}).get('job_id'), + 'query_name': certificate_data.get('response', {}).get('metadata', {}).get('query_name'), + 'results_returned': certificate_data.get('response', {}).get('metadata', {}).get('results_returned'), + 'results_total_at_least': certificate_data.get('response', {}).get('metadata', {}).get('results_total_at_least') + } + + job_status = certificate_data.get('response', {}).get('job_status', {}) + job_status_url = job_status.get('get') + job_status_status = job_status.get('status', 'N/A') + + # Prepare the raw response + raw_response = { + 'certificate_data': certificate_data, + 'metadata': metadata, + 'job_status': { + 'url': job_status_url, + 'status': job_status_status + } + } return CommandResults( - outputs_prefix='SilentPush.Certificates', + outputs_prefix='SilentPush.Certificate', outputs_key_field='domain', - outputs=raw_response, - readable_output=readable_output, + outputs={'domain': domain, 'certificates': certificate_data, 'metadata': metadata}, + readable_output='\n'.join(markdown), raw_response=raw_response ) @@ -528,14 +691,30 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - - if raw_response.get('response') and 'records' in raw_response['response']: - records = raw_response['response']['records'] - else: - records = [] - + # Check for response errors + if raw_response.get('error'): + return CommandResults( + readable_output=f"Error: {raw_response['error']}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error' + ) + + # Extract records from the response + records = raw_response.get('response', {}).get('records', []) + + if not records: + return CommandResults( + readable_output="No domains found.", + raw_response=raw_response, + outputs_prefix='SilentPush.SearchResults', + outputs_key_field='domain', + outputs=raw_response + ) + + # Format records into a readable markdown table readable_output = tableToMarkdown('Domain Search Results', records) - + return CommandResults( outputs_prefix='SilentPush.SearchResults', outputs_key_field='domain', @@ -555,34 +734,31 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: Returns: CommandResults: The command results containing readable output and the raw response. """ - domains = argToList(args.get('domains', '')) cluster = argToBoolean(args.get('cluster', False)) mode = args.get('mode', 'live') match = args.get('match', 'self') as_of = args.get('as_of', None) - if not domains: raise ValueError('"domains" argument is required and cannot be empty.') - demisto.debug(f'Processing infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') try: - raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) demisto.debug(f'Response from API: {raw_response}') except Exception as e: - demisto.error(f'Error occurred while fetching infratags: {str(e)}') raise - infratags = raw_response.get('response', {}).get('infratags', []) + tag_clusters = raw_response.get('response', {}).get('tag_clusters', []) + readable_output = tableToMarkdown('Domain Infratags', infratags) + if tag_clusters: + readable_output += tableToMarkdown('Domain Tag Clusters', tag_clusters) - return CommandResults( outputs_prefix='SilentPush.InfraTags', outputs_key_field='domain', @@ -641,7 +817,6 @@ def get_enrichment_data_command(client: Client, args: Dict[str, Any]) -> Command raise - def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ Command handler for fetching IP information. @@ -670,17 +845,13 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command raise DemistoException('Invalid sparse value. Must be one of: asn, asname, sp_risk_score') try: - ip_list = [ip.strip() for ip in ips.split(',')] results = [] markdown = ['### IP Information Results\n'] - for ip in ip_list: resource = ip - - raw_response = client.list_ip_information(resource, explain, scan_data, sparse) ip_data = raw_response.get('ips', []) @@ -729,6 +900,143 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command except Exception as e: demisto.error(f"Error in list_ip_information_command: {str(e)}") raise + +def get_asn_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + Command handler for fetching ASN reputation information. + + Args: + client (Client): The client instance to fetch the data + args (dict): Command arguments including: + - asn (str): The ASN to lookup + + Returns: + CommandResults: XSOAR command results + """ + asn = args.get('asn') + if not asn: + raise DemistoException('ASN is a required argument') + + try: + raw_response = client.get_asn_reputation(asn) + reputation_data = raw_response.get('response', {}) + + # Create a readable output + markdown = [f"### ASN Reputation Information for {asn}\n"] + + # Basic reputation information + if basic_info := reputation_data.get('reputation', {}): + reputation_table = { + 'Risk Score': basic_info.get('risk_score', 'N/A'), + 'First Seen': basic_info.get('first_seen', 'N/A'), + 'Last Seen': basic_info.get('last_seen', 'N/A'), + 'Total Reports': basic_info.get('total_reports', 'N/A') + } + markdown.append(tableToMarkdown('Reputation Overview', [reputation_table])) + + # Historical data if available + if history := reputation_data.get('history', []): + history_table = [] + for entry in history: + history_table.append({ + 'Date': entry.get('date', 'N/A'), + 'Risk Score': entry.get('risk_score', 'N/A'), + 'Reports': entry.get('reports', 'N/A') + }) + if history_table: + markdown.append('\n### Historical Reputation Data') + markdown.append(tableToMarkdown('', history_table)) + + # Additional metadata if available + if metadata := reputation_data.get('metadata', {}): + metadata_table = {k: str(v) for k, v in metadata.items()} + if metadata_table: + markdown.append('\n### Additional Information') + markdown.append(tableToMarkdown('', [metadata_table])) + + return CommandResults( + outputs_prefix='SilentPush.ASNReputation', + outputs_key_field='asn', + outputs={ + 'asn': asn, + 'reputation': reputation_data + }, + readable_output='\n'.join(markdown), + raw_response=raw_response + ) + + except Exception as e: + demisto.error(f"Error in get_asn_reputation_command: {str(e)}") + raise + +def get_asn_takedown_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + Command handler for fetching ASN takedown reputation information. + + Args: + client (Client): The client instance to fetch the data + args (dict): Command arguments including: + - asn (str): The ASN to lookup + + Returns: + CommandResults: XSOAR command results + """ + asn = args.get('asn') + if not asn: + raise DemistoException('ASN is a required argument') + + try: + raw_response = client.get_asn_takedown_reputation(asn) + takedown_data = raw_response.get('response', {}) + + + markdown = [f"### ASN Takedown Reputation Information for {asn}\n"] + + + if basic_info := takedown_data.get('takedown', {}): + takedown_table = { + 'Risk Score': basic_info.get('risk_score', 'N/A'), + 'First Seen': basic_info.get('first_seen', 'N/A'), + 'Last Seen': basic_info.get('last_seen', 'N/A'), + 'Total Reports': basic_info.get('total_reports', 'N/A') + } + markdown.append(tableToMarkdown('Takedown Reputation Overview', [takedown_table])) + + + if history := takedown_data.get('history', []): + history_table = [] + for entry in history: + history_table.append({ + 'Date': entry.get('date', 'N/A'), + 'Risk Score': entry.get('risk_score', 'N/A'), + 'Reports': entry.get('reports', 'N/A') + }) + if history_table: + markdown.append('\n### Historical Takedown Reputation Data') + markdown.append(tableToMarkdown('', history_table)) + + + if metadata := takedown_data.get('metadata', {}): + metadata_table = {k: str(v) for k, v in metadata.items()} + if metadata_table: + markdown.append('\n### Additional Information') + markdown.append(tableToMarkdown('', [metadata_table])) + + return CommandResults( + outputs_prefix='SilentPush.ASNTakedownReputation', + outputs_key_field='asn', + outputs={ + 'asn': asn, + 'takedown_reputation': takedown_data + }, + readable_output='\n'.join(markdown), + raw_response=raw_response + ) + + except Exception as e: + demisto.error(f"Error in get_asn_takedown_reputation_command: {str(e)}") + raise + ''' MAIN FUNCTION ''' @@ -773,7 +1081,9 @@ def main(): 'silentpush-search-domains': search_domains_command, 'silentpush-list-domain-infratags': list_domain_infratags_command, 'silentpush-get-enrichment-data' : get_enrichment_data_command, - 'silentpush-list-ip-information' : list_ip_information_command + 'silentpush-list-ip-information' : list_ip_information_command, + 'silentpush-get-asn-reputation' : get_asn_reputation_command, + 'silentpush-get-asn-takedown-reputation' : get_asn_takedown_reputation_command } if command in command_handlers: From 0492dc212141dc9c671d17ef39c15db3bab6d3ec Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Mon, 20 Jan 2025 10:23:36 +0530 Subject: [PATCH 10/19] updated code --- .../Integrations/SilentPush/SilentPush.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 53587a05e794..e7fc90ed177e 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -213,7 +213,7 @@ def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = Non 'max_wait': max_wait } - # Remove keys with None values + params = {k: v for k, v in params.items() if v is not None} response = self._http_request('GET', url_suffix, params=params) @@ -283,7 +283,7 @@ def search_domains(self, try: response = self._http_request('GET', url_suffix, params=params) - # Log job status if available + job_status = response.get('response', {}).get('job_status', {}) if job_status: demisto.debug(f"Job Status: {job_status.get('status', 'Unknown')}") @@ -318,7 +318,7 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str params = { 'mode': mode, 'match': match, - 'clusters': cluster # Use boolean value directly + 'clusters': cluster } if as_of: @@ -333,7 +333,7 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str ) return response except Exception as e: - # Log error if the request fails + demisto.error(f"Error fetching infratags: {str(e)}") raise @@ -432,7 +432,7 @@ def get_asn_reputation(self, asn: str) -> Dict: if not asn: raise ValueError("ASN cannot be empty") - # Strip 'AS' prefix if present and validate ASN format + asn_number = asn.upper().replace('AS', '') if not asn_number.isdigit(): raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") @@ -468,7 +468,7 @@ def get_asn_takedown_reputation(self, asn: str) -> Dict: if not asn: raise ValueError("ASN cannot be empty") - # Strip 'AS' prefix if present and validate ASN format + asn_number = asn.upper().replace('AS', '') if not asn_number.isdigit(): raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") @@ -529,7 +529,7 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com Returns: CommandResults: Formatted results for XSOAR. """ - # Extract and validate domains + domains_arg = args.get('domains') or args.get('domain') if not domains_arg: raise DemistoException('No domains provided. Use the "domain" or "domains" argument.') @@ -538,24 +538,24 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com if len(domains) > 100: raise DemistoException("A maximum of 100 domains can be submitted in a single request.") - # Extract optional parameters + fetch_risk_score = argToBoolean(args.get('fetch_risk_score', False)) fetch_whois_info = argToBoolean(args.get('fetch_whois_info', False)) - # Log input for debugging + demisto.debug(f"Fetching domain information for: {domains} " f"with fetch_risk_score={fetch_risk_score}, fetch_whois_info={fetch_whois_info}") - # Call the client method to fetch domain information + raw_response = client.list_domain_information(domains, fetch_risk_score, fetch_whois_info) demisto.debug(f"API response: {raw_response}") - # Prepare readable output + markdown = ['# Domain Information Results\n'] for domain_info in raw_response.get('domains', []): markdown.append(f"## Domain: {domain_info.get('domain', 'N/A')}") - # Add basic domain information + basic_info = { 'Created Date': domain_info.get('whois_created_date', 'N/A'), 'Registrar': domain_info.get('registrar', 'N/A'), @@ -564,11 +564,11 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com } markdown.append(tableToMarkdown('Domain Information', [basic_info])) - # Add risk score explanation if available + if risk_explain := domain_info.get('sp_risk_score_explain'): markdown.append(f'### Risk Score Explanation\n{risk_explain}') - # Add WHOIS data if available + whois_info = domain_info.get('whois_info', {}) if isinstance(whois_info, dict): whois_table = [{'Key': k, 'Value': v} for k, v in whois_info.items()] @@ -578,7 +578,7 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com readable_output = '\n'.join(markdown) - # Return command results + return CommandResults( outputs_prefix='SilentPush.Domain', outputs_key_field='domain', @@ -603,17 +603,17 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com if not domain: raise DemistoException('Domain argument is required.') - # Call the client function to get the domain certificates. + demisto.debug(f'Fetching certificates for domain: {domain}') certificate_data = client.get_domain_certificates(domain) if not certificate_data: raise DemistoException(f'No certificate data found for domain: {domain}') - # Prepare the markdown output + markdown = [f'# SSL/TLS Certificate Information for Domain: {domain}\n'] - # Add certificate details to markdown + if isinstance(certificate_data, list) and certificate_data: for cert in certificate_data: markdown.append(f"## Certificate for {domain}") @@ -626,14 +626,14 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com } markdown.append(tableToMarkdown('Certificate Information', [cert_info])) - # Add metadata if available + metadata = cert.get('metadata', {}) if metadata: markdown.append(f"### Metadata: {metadata}") else: markdown.append(f'No certificate data available for domain: {domain}') - # Add metadata and job status to the response + metadata = { 'job_id': certificate_data.get('response', {}).get('metadata', {}).get('job_id'), 'query_name': certificate_data.get('response', {}).get('metadata', {}).get('query_name'), @@ -645,7 +645,7 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com job_status_url = job_status.get('get') job_status_status = job_status.get('status', 'N/A') - # Prepare the raw response + raw_response = { 'certificate_data': certificate_data, 'metadata': metadata, @@ -691,7 +691,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - # Check for response errors + if raw_response.get('error'): return CommandResults( readable_output=f"Error: {raw_response['error']}", @@ -700,7 +700,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - # Extract records from the response + records = raw_response.get('response', {}).get('records', []) if not records: @@ -712,7 +712,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs=raw_response ) - # Format records into a readable markdown table + readable_output = tableToMarkdown('Domain Search Results', records) return CommandResults( @@ -921,10 +921,10 @@ def get_asn_reputation_command(client: Client, args: Dict[str, Any]) -> CommandR raw_response = client.get_asn_reputation(asn) reputation_data = raw_response.get('response', {}) - # Create a readable output + markdown = [f"### ASN Reputation Information for {asn}\n"] - # Basic reputation information + if basic_info := reputation_data.get('reputation', {}): reputation_table = { 'Risk Score': basic_info.get('risk_score', 'N/A'), @@ -934,7 +934,7 @@ def get_asn_reputation_command(client: Client, args: Dict[str, Any]) -> CommandR } markdown.append(tableToMarkdown('Reputation Overview', [reputation_table])) - # Historical data if available + if history := reputation_data.get('history', []): history_table = [] for entry in history: @@ -947,7 +947,7 @@ def get_asn_reputation_command(client: Client, args: Dict[str, Any]) -> CommandR markdown.append('\n### Historical Reputation Data') markdown.append(tableToMarkdown('', history_table)) - # Additional metadata if available + if metadata := reputation_data.get('metadata', {}): metadata_table = {k: str(v) for k, v in metadata.items()} if metadata_table: From e412ebe1913f3d866e5417814da7c08f31923ce5 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Mon, 20 Jan 2025 12:36:37 +0530 Subject: [PATCH 11/19] updated commands and it's yml --- .../Integrations/SilentPush/SilentPush.py | 196 +++++++------ .../Integrations/SilentPush/SilentPush.yml | 275 +++++++++--------- 2 files changed, 256 insertions(+), 215 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index e7fc90ed177e..093d3f640787 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -18,12 +18,6 @@ # Disable insecure warnings urllib3.disable_warnings() - -def mock_debug(message): - """Print debug messages to the XSOAR logs""" - print(f"DEBUG: {message}") -demisto.debug = mock_debug - ''' CONSTANTS ''' DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' # ISO8601 format with UTC, default in XSOAR @@ -58,7 +52,7 @@ def __init__(self, base_url: str, api_key: str, verify: bool = True, proxy: bool 'X-API-Key': api_key, 'Content-Type': 'application/json' } - demisto.debug(f'Initialized client with base URL: {self.base_url}') + def _http_request(self, method: str, url_suffix: str, params: dict = None, data: dict = None) -> Any: """ @@ -81,9 +75,7 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: """ full_url = f'{self.base_url}{url_suffix}' masked_headers = {k: v if k != 'X-API-Key' else '****' for k, v in self._headers.items()} - demisto.debug(f'Headers: {masked_headers}') - demisto.debug(f'Params: {params}') - demisto.debug(f'Data: {data}') + try: response = requests.request( @@ -94,8 +86,7 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: params=params, json=data ) - demisto.debug(f'Response status code: {response.status_code}') - demisto.debug(f'Response body: {response.text}') + if response.status_code not in {200, 201}: raise DemistoException(f'Error in API call [{response.status_code}] - {response.text}') @@ -198,7 +189,7 @@ def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = Non Returns: dict: A dictionary containing certificate information fetched from the API. """ - demisto.debug(f'Fetching certificate information for domain: {domain}') + url_suffix = f'explore/domain/certificates/{domain}' params = { @@ -225,14 +216,14 @@ def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = Non job_complete = False while not job_complete: - demisto.debug(f'Checking job status at {job_status_url}') + job_response = self._http_request('GET', job_status_url) job_status = job_response.get('response', {}).get('job_status', {}).get('status') if job_status == 'COMPLETED': job_complete = True - demisto.debug('Job completed, fetching certificates.') + certificate_data = job_response.get('response', {}).get('domain_certificates', []) return certificate_data @@ -240,7 +231,7 @@ def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = Non demisto.error('Job failed to complete.') return {'error': 'Job failed'} else: - demisto.debug('Job is still in progress. Retrying...') + time.sleep(5) return {} @@ -248,50 +239,81 @@ def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = Non def search_domains(self, - query: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - risk_score_min: Optional[int] = None, - risk_score_max: Optional[int] = None, - limit: int = 100) -> dict: - """ - Search for domains with optional filters. - - Args: - query (str, optional): Search query string (e.g., domain pattern, keywords) - start_date (str, optional): Start date for domain registration (ISO8601 format) - end_date (str, optional): End date for domain registration (ISO8601 format) - risk_score_min (int, optional): Minimum risk score filter - risk_score_max (int, optional): Maximum risk score filter - limit (int, optional): Maximum number of results to return (default: 100) - - Returns: - dict: A dictionary containing the search results - """ - demisto.debug(f'Searching domains with query: {query}') - url_suffix = 'explore/domain/search' - - params = {k: v for k, v in { - 'domain': query, - 'start_date': start_date, - 'end_date': end_date, - 'risk_score_min': risk_score_min, - 'risk_score_max': risk_score_max, - 'limit': limit - }.items() if v is not None} - - try: - response = self._http_request('GET', url_suffix, params=params) + query: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + risk_score_min: Optional[int] = None, + risk_score_max: Optional[int] = None, + limit: int = 100, + domain_regex: Optional[str] = None, + name_server: Optional[str] = None, + asnum: Optional[int] = None, + asname: Optional[str] = None, + min_ip_diversity: Optional[int] = None, + registrar: Optional[str] = None, + min_asn_diversity: Optional[int] = None, + certificate_issuer: Optional[str] = None, + whois_date_after: Optional[str] = None, + skip: Optional[int] = None) -> dict: + """ + Searches for domains with optional filters and returns relevant information. + If filters are not specified, it performs a broad search. The function + supports additional optional parameters to narrow down the search results. + + Args: + query (Optional[str]): Search query string to match domains (e.g., domain pattern or keywords). + start_date (Optional[str]): Filter for domain registration dates on or after this date (ISO8601 format). + end_date (Optional[str]): Filter for domain registration dates on or before this date (ISO8601 format). + risk_score_min (Optional[int]): Minimum risk score for filtering domains. + risk_score_max (Optional[int]): Maximum risk score for filtering domains. + domain_regex (Optional[str]): A valid RE2 regular expression to match domain patterns. + name_server (Optional[str]): Name or wildcard pattern of name servers used by domains. + asnum (Optional[int]): Autonomous System (AS) number to filter domains. + asname (Optional[str]): Filter domains where the AS name begins with this string. + min_ip_diversity (Optional[int]): Filter domains with a minimum IP diversity limit. + registrar (Optional[str]): Name or partial name of the registrar for filtering domains. + min_asn_diversity (Optional[int]): Filter domains with a minimum ASN diversity limit. + certificate_issuer (Optional[str]): SSL certificate issuer name to filter domains. + whois_date_after (Optional[str]): Filter domains with a Whois created date after this date (ISO8601 format). + skip (Optional[int]): Number of results to skip for pagination. + limit (int): Maximum number of results to return (default: 100). + + Returns: + dict: A dictionary containing the search results, including the matched domains and related metadata. + + Raises: + Exception: If the API request fails or an error occurs during the process. + """ + + + url_suffix = 'explore/domain/search' - - job_status = response.get('response', {}).get('job_status', {}) - if job_status: - demisto.debug(f"Job Status: {job_status.get('status', 'Unknown')}") + params = {k: v for k, v in { + 'domain': query, + 'start_date': start_date, + 'end_date': end_date, + 'risk_score_min': risk_score_min, + 'risk_score_max': risk_score_max, + 'limit': limit, + 'domain_regex': domain_regex, + 'name_server': name_server, + 'asnum': asnum, + 'asname': asname, + 'min_ip_diversity': min_ip_diversity, + 'registrar': registrar, + 'min_asn_diversity': min_asn_diversity, + 'certificate_issuer': certificate_issuer, + 'whois_date_after': whois_date_after, + 'skip': skip, + }.items() if v is not None} - return response - except Exception as e: - demisto.error(f"Error in search_domains API request: {str(e)}") - return {'error': str(e)} + try: + response = self._http_request('GET', url_suffix, params=params) + return response + except Exception as e: + demisto.error(f"Error in search_domains API request: {str(e)}") + return {'error': str(e)} + def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str = 'live', match: str = 'self', as_of: Optional[str] = None) -> dict: @@ -308,7 +330,7 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str Returns: dict: A dictionary containing infratags for the provided domains. """ - demisto.debug(f'Fetching infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') + url = 'explore/bulk/domain/infratags' @@ -359,7 +381,7 @@ def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = if resource_type not in {'domain', 'ipv4', 'ipv6'}: raise ValueError("resource_type must be one of: 'domain', 'ipv4', 'ipv6'") - demisto.debug(f'Fetching enrichment data for {resource_type}: {resource}') + url_suffix = f'explore/enrich/{resource_type}/{resource}' params = { @@ -373,7 +395,7 @@ def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = url_suffix=url_suffix, params=params ) - demisto.debug(f'Enrichment response: {response}') + return response except Exception as e: @@ -437,7 +459,7 @@ def get_asn_reputation(self, asn: str) -> Dict: if not asn_number.isdigit(): raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") - demisto.debug(f'Fetching reputation for ASN: {asn_number}') + try: url_suffix = f'explore/ipreputation/history/asn/{asn_number}' @@ -473,7 +495,7 @@ def get_asn_takedown_reputation(self, asn: str) -> Dict: if not asn_number.isdigit(): raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") - demisto.debug(f'Fetching takedown reputation for ASN: {asn_number}') + try: url_suffix = f'explore/ipreputation/takedown/asn/{asn_number}' @@ -503,13 +525,13 @@ def test_module(client: Client) -> str: Returns: str: 'ok' if the connection is successful, otherwise returns an error message. """ - demisto.debug('Running test module...') + try: client.list_domain_information('silentpush.com') - demisto.debug('Test module completed successfully') + return 'ok' except DemistoException as e: - demisto.debug(f'Test module failed: {str(e)}') + if 'Forbidden' in str(e) or 'Authorization' in str(e): return 'Authorization Error: make sure API Key is correctly set' raise e @@ -542,13 +564,9 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com fetch_risk_score = argToBoolean(args.get('fetch_risk_score', False)) fetch_whois_info = argToBoolean(args.get('fetch_whois_info', False)) - - demisto.debug(f"Fetching domain information for: {domains} " - f"with fetch_risk_score={fetch_risk_score}, fetch_whois_info={fetch_whois_info}") - raw_response = client.list_domain_information(domains, fetch_risk_score, fetch_whois_info) - demisto.debug(f"API response: {raw_response}") + markdown = ['# Domain Information Results\n'] @@ -604,7 +622,6 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com raise DemistoException('Domain argument is required.') - demisto.debug(f'Fetching certificates for domain: {domain}') certificate_data = client.get_domain_certificates(domain) if not certificate_data: @@ -671,8 +688,16 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: risk_score_min = arg_to_number(args.get('risk_score_min')) risk_score_max = arg_to_number(args.get('risk_score_max')) limit = arg_to_number(args.get('limit', 100)) - - demisto.debug(f'Searching domains with query: {query}') + domain_regex = args.get('domain_regex') + name_server = args.get('name_server') + asnum = arg_to_number(args.get('asnum')) + asname = args.get('asname') + min_ip_diversity = arg_to_number(args.get('min_ip_diversity')) + registrar = args.get('registrar') + min_asn_diversity = arg_to_number(args.get('min_asn_diversity')) + certificate_issuer = args.get('certificate_issuer') + whois_date_after = args.get('whois_date_after') + skip = arg_to_number(args.get('skip')) try: raw_response = client.search_domains( @@ -681,7 +706,17 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: end_date=end_date, risk_score_min=risk_score_min, risk_score_max=risk_score_max, - limit=limit + limit=limit, + domain_regex=domain_regex, + name_server=name_server, + asnum=asnum, + asname=asname, + min_ip_diversity=min_ip_diversity, + registrar=registrar, + min_asn_diversity=min_asn_diversity, + certificate_issuer=certificate_issuer, + whois_date_after=whois_date_after, + skip=skip ) except Exception as e: return CommandResults( @@ -691,7 +726,6 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - if raw_response.get('error'): return CommandResults( readable_output=f"Error: {raw_response['error']}", @@ -700,7 +734,6 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - records = raw_response.get('response', {}).get('records', []) if not records: @@ -712,7 +745,6 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs=raw_response ) - readable_output = tableToMarkdown('Domain Search Results', records) return CommandResults( @@ -723,6 +755,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) + def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: """ Command handler for fetching infratags for multiple domains. @@ -743,11 +776,8 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: if not domains: raise ValueError('"domains" argument is required and cannot be empty.') - demisto.debug(f'Processing infratags for domains: {domains} with cluster={cluster}, mode={mode}, match={match}, as_of={as_of}') - try: raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) - demisto.debug(f'Response from API: {raw_response}') except Exception as e: demisto.error(f'Error occurred while fetching infratags: {str(e)}') raise @@ -1060,9 +1090,6 @@ def main(): verify_ssl = not params.get('insecure', False) proxy = params.get('proxy', False) - demisto.debug(f'Base URL: {base_url}') - demisto.debug('Initializing client...') - client = Client( base_url=base_url, api_key=api_key, @@ -1071,7 +1098,6 @@ def main(): ) command = demisto.command() - demisto.debug(f'Command being called is {command}') command_handlers = { diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 2d6c91c5c8d9..cd6f5e89bd72 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -1,160 +1,175 @@ commonfields: id: SilentPush version: -1 + name: SilentPush display: SilentPush category: Data Enrichment & Threat Intelligence -description: Integration with SilentPush API for domain and IP intelligence +description: SilentPush integration for domain and IP intelligence + configuration: - display: API Key name: credentials type: 9 required: true + - display: Base URL name: url defaultvalue: https://api.silentpush.com type: 0 required: true + - display: Trust any certificate (not secure) name: insecure type: 8 required: false + - display: Use system proxy settings name: proxy type: 8 required: false + script: - type: python - subtype: python3 script: '' + type: python commands: - - name: silentpush-list-domain-information - arguments: - - name: domains - description: Comma-separated list of domains to fetch information for - required: true - - name: domain - description: Single domain to fetch information for (alternative to domains) - required: false - outputs: - - contextPath: SilentPush.Domain - description: Domain information including WHOIS data and risk scores - type: unknown - description: Fetches domain information including WHOIS data and risk scores - execution: false - - - name: silentpush-get-domain-certificates - arguments: - - name: domain - description: Domain to fetch certificate information for - required: true - outputs: - - contextPath: SilentPush.Certificates - description: Certificate information for the domain - type: unknown - description: Fetches SSL/TLS certificate data for a given domain - execution: false + - name: test-module + description: Validates the API credentials. + + - name: silentpush-list-domain-information + description: Retrieves information about specified domains. + arguments: + - name: domains + description: Comma-separated list of domains to query. + required: true + - name: fetch_risk_score + description: Whether to fetch risk scores for the domains. + required: false + auto: true + default: false + - name: fetch_whois_info + description: Whether to fetch WHOIS information for the domains. + required: false + auto: true + default: false + + - name: silentpush-get-domain-certificates + description: Retrieves SSL/TLS certificate information for a domain. + arguments: + - name: domain + description: The domain to query certificates for. + required: true + - name: domain_regex + description: Regular expression to match domains. + required: false + - name: certificate_issuer + description: Filter by certificate issuer. + required: false + - name: date_min + description: Filter certificates issued on or after this date. + required: false + - name: date_max + description: Filter certificates issued on or before this date. + required: false + + - name: silentpush-search-domains + description: Search for domains with optional filters. + arguments: + - name: query + description: Search query string. + required: false + - name: start_date + description: Filter domains registered after this date. + required: false + - name: end_date + description: Filter domains registered before this date. + required: false + - name: risk_score_min + description: Minimum risk score filter. + required: false + - name: risk_score_max + description: Maximum risk score filter. + required: false + - name: limit + description: Maximum number of results to return. + required: false + defaultValue: "100" + + - name: silentpush-list-domain-infratags + description: Get infrastructure tags for multiple domains. + arguments: + - name: domains + description: Comma-separated list of domains. + required: true + - name: cluster + description: Whether to cluster the results. + required: false + auto: true + default: false + - name: mode + description: Mode for lookup (live/padns). + required: false + defaultValue: live + - name: match + description: Handling of self-hosted infrastructure. + required: false + defaultValue: self + + - name: silentpush-get-enrichment-data + description: Get enrichment data for a resource. + arguments: + - name: resource + description: The resource to query (domain/IP). + required: true + - name: resource_type + description: Type of resource (domain/ipv4/ipv6). + required: true + - name: explain + description: Include explanation of data calculations. + required: false + auto: true + - name: scan_data + description: Include scan data (IPv4 only). + required: false + auto: true + + - name: silentpush-list-ip-information + description: Get information about IP addresses. + arguments: + - name: ips + description: Comma-separated list of IP addresses. + required: true + - name: explain + description: Include explanation of calculations. + required: false + auto: true + - name: scan_data + description: Include scan data (IPv4 only). + required: false + auto: true + - name: sparse + description: Specific data to return (asn/asname/sp_risk_score). + required: false + + - name: silentpush-get-asn-reputation + description: Get reputation information for an ASN. + arguments: + - name: asn + description: The ASN to lookup. + required: true + + - name: silentpush-get-asn-takedown-reputation + description: Get takedown reputation for an ASN. + arguments: + - name: asn + description: The ASN to lookup. + required: true - - name: silentpush-search-domains - arguments: - - name: query - description: Search query string (e.g., domain pattern, keywords) - required: false - - name: start_date - description: Start date for domain registration (ISO8601 format) - required: false - - name: end_date - description: End date for domain registration (ISO8601 format) - required: false - - name: risk_score_min - description: Minimum risk score filter - required: false - - name: risk_score_max - description: Maximum risk score filter - required: false - - name: limit - description: Maximum number of results to return (default 100) - required: false - outputs: - - contextPath: SilentPush.SearchResults - description: Domain search results - type: unknown - description: Search for domains with optional filters - execution: false - - - name: silentpush-list-domain-infratags - arguments: - - name: domains - description: List of domains to fetch infratags for - required: true - - name: cluster - description: Whether to cluster the results - required: false - - name: mode - description: Mode for lookup (live/padns) - required: false - - name: match - description: Handling of self-hosted infrastructure (self/full) - required: false - - name: as_of - description: Date or timestamp for filtering data - required: false - outputs: - - contextPath: SilentPush.InfraTags - description: Infratags for the provided domains - type: unknown - description: Get infratags for multiple domains - execution: false - - - name: silentpush-get-enrichment-data - arguments: - - name: resource - description: Resource to get enrichment data for (domain name, IPv4, or IPv6) - required: true - - name: resource_type - description: Type of resource (domain/ipv4/ipv6) - required: true - - name: explain - description: Whether to show calculation details - required: false - - name: scan_data - description: Whether to include scan data - required: false - outputs: - - contextPath: SilentPush.Enrichment - description: Enrichment data for the resource - type: unknown - description: Retrieve enrichment data for a resource - execution: false - - - name: silentpush-list-ip-information - arguments: - - name: ips - description: Comma-separated list of IP addresses - required: true - - name: explain - description: Whether to show calculation details - required: false - - name: scan_data - description: Whether to include scan data (IPv4 only) - required: false - - name: sparse - description: Specific data to return (asn/asname/sp_risk_score) - required: false - outputs: - - contextPath: SilentPush.IP - description: IP information and analysis - type: unknown - description: Fetches information for IPv4 and IPv6 addresses - execution: false + dockerimage: demisto/python3:3.10 + runonce: false + subtype: python3 - - name: test-module - description: Validates the API connection and authentication - execution: false +tests: + - No tests (auto formatted) - dockerimage: demisto/python3:3.9 - runonce: false - isfetch: false - longRunning: false - longRunningPort: false - feed: false \ No newline at end of file +fromversion: 6.0.0 \ No newline at end of file From 1ae9f23a40b5c930a31311310bb03f9b6eaef3d4 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Thu, 23 Jan 2025 01:40:36 +0530 Subject: [PATCH 12/19] updated the code --- .../Integrations/SilentPush/SilentPush.py | 1025 +++++------------ 1 file changed, 265 insertions(+), 760 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 093d3f640787..657d1d75b9fb 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -1,5 +1,4 @@ -import demistomock as demisto # noqa: F401 -from CommonServerPython import * # noqa: F401 + """Base Integration for Cortex XSOAR (aka Demisto) This is an integration to interact with the SilentPush API and provide functionality within XSOAR. @@ -9,13 +8,11 @@ Linting: https://xsoar.pan.dev/docs/integrations/linting """ -from CommonServerUserPython import * # noqa import requests import urllib3 from typing import Any, Optional, Dict -# Disable insecure warnings urllib3.disable_warnings() ''' CONSTANTS ''' @@ -24,7 +21,6 @@ ''' CLIENT CLASS ''' - class Client(BaseClient): """Client class to interact with the SilentPush API @@ -37,7 +33,7 @@ class Client(BaseClient): def __init__(self, base_url: str, api_key: str, verify: bool = True, proxy: bool = False): """ Initializes the client with the necessary parameters. - + Args: base_url (str): The base URL for the SilentPush API. api_key (str): The API key for authentication. @@ -52,30 +48,28 @@ def __init__(self, base_url: str, api_key: str, verify: bool = True, proxy: bool 'X-API-Key': api_key, 'Content-Type': 'application/json' } - def _http_request(self, method: str, url_suffix: str, params: dict = None, data: dict = None) -> Any: """ Handles the HTTP requests to the SilentPush API. - + This function builds the request URL, adds the necessary headers, and sends a request to the API. It returns the response in JSON format. - + Args: method (str): The HTTP method (GET, POST, etc.). url_suffix (str): The specific endpoint to be appended to the base URL. params (dict, optional): The URL parameters to be sent with the request. data (dict, optional): The data to be sent with the request. - + Returns: Any: The JSON response from the API. - + Raises: DemistoException: If there is an error in the API response. """ full_url = f'{self.base_url}{url_suffix}' masked_headers = {k: v if k != 'X-API-Key' else '****' for k, v in self._headers.items()} - try: response = requests.request( @@ -86,7 +80,6 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: params=params, json=data ) - if response.status_code not in {200, 201}: raise DemistoException(f'Error in API call [{response.status_code}] - {response.text}') @@ -94,543 +87,258 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: except Exception as e: demisto.error(f'Error in API call: {str(e)}') raise - - def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: - """ - Fetches domain information, including WHOIS data, risk scores, and live WHOIS for multiple domains. - - Args: - domains: List of domain strings. - fetch_risk_score: Whether to fetch risk scores (default: False). - fetch_whois_info: Whether to fetch live WHOIS information (default: False). - Returns: - Dict: A dictionary containing combined domain information, risk scores, and live WHOIS information. - """ + def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: if len(domains) > 100: raise DemistoException("Maximum of 100 domains can be submitted in a single request.") - try: - - domains_data = {'domains': domains} - - - bulk_info_response = self._http_request( - method='POST', - url_suffix='explore/bulk/domaininfo', - data=domains_data - ) + domains_data = {'domains': domains} + bulk_info_response = self._http_request('POST', 'explore/bulk/domaininfo', data=domains_data) - - domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) - domain_info_dict = {item['domain']: item for item in domain_info_list} - combined_results = [] - - - risk_score_dict = {} - if fetch_risk_score: - bulk_risk_response = self._http_request( - method='POST', - url_suffix='explore/bulk/domain/riskscore', - data=domains_data - ) - risk_score_list = bulk_risk_response.get('response', []) - risk_score_dict = {item['domain']: item for item in risk_score_list} - - - live_whois_info = {} - if fetch_whois_info: - for domain in domains: - try: - live_whois_response = self._http_request( - method='GET', - url_suffix=f'explore/domain/whoislive/{domain}' - ) - live_whois_info[domain] = live_whois_response.get('response', {}) - except Exception as e: - live_whois_info[domain] = {'error': f"Failed to fetch WHOIS data: {str(e)}"} - - - for domain in domains: - combined_results.append({ - 'domain': domain, - **domain_info_dict.get(domain, {}), - 'sp_risk_score': risk_score_dict.get(domain, {}).get('sp_risk_score', 'N/A'), - 'sp_risk_score_explain': risk_score_dict.get(domain, {}).get('sp_risk_score_explain', 'N/A'), - 'whois_info': live_whois_info.get(domain, 'N/A') - }) - - return {'domains': combined_results} - - except Exception as e: - raise DemistoException(f"Failed to fetch bulk domain information: {str(e)}") - - - def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = None, certificate_issuer: Optional[str] = None, - date_min: Optional[str] = None, date_max: Optional[str] = None, prefer: Optional[str] = None, - max_wait: Optional[int] = None, with_metadata: Optional[bool] = False, skip: Optional[int] = 0, - limit: Optional[int] = 100) -> dict: - """ - Fetches SSL/TLS certificate data for a given domain. - If the job is not completed, it polls the job status periodically. + domain_info_list = bulk_info_response.get('response', {}).get('domaininfo', []) + domain_info_dict = {item['domain']: item for item in domain_info_list} - Args: - domain (str): The domain to fetch certificate information for. - domain_regex (Optional[str]): Regular expression to match domains. - certificate_issuer (Optional[str]): The name of the certificate issuer. - date_min (Optional[str]): Filter certificates issued on or after this date. - date_max (Optional[str]): Filter certificates issued on or before this date. - prefer (Optional[str]): Prefer to wait for longer queries. - max_wait (Optional[int]): Maximum wait time in seconds. - with_metadata (Optional[bool]): Whether to include metadata. - skip (Optional[int]): Number of results to skip. - limit (Optional[int]): Maximum number of results. + risk_score_dict = {} + if fetch_risk_score: + bulk_risk_response = self._http_request('POST', 'explore/bulk/domain/riskscore', data=domains_data) + risk_score_list = bulk_risk_response.get('response', []) + risk_score_dict = {item['domain']: item for item in risk_score_list} - Returns: - dict: A dictionary containing certificate information fetched from the API. - """ - - - url_suffix = f'explore/domain/certificates/{domain}' + live_whois_info = {} + if fetch_whois_info: + for domain in domains: + try: + live_whois_response = self._http_request('GET', f'explore/domain/whoislive/{domain}') + live_whois_info[domain] = live_whois_response.get('response', {}) + except Exception as e: + live_whois_info[domain] = {'error': str(e)} + + combined_results = [{ + 'domain': domain, + **domain_info_dict.get(domain, {}), + 'sp_risk_score': risk_score_dict.get(domain, {}).get('sp_risk_score', 'N/A'), + 'sp_risk_score_explain': risk_score_dict.get(domain, {}).get('sp_risk_score_explain', 'N/A'), + 'whois_info': live_whois_info.get(domain, 'N/A') + } for domain in domains] + + return {'domains': combined_results} + + def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = None, certificate_issuer: Optional[str] = None, date_min: Optional[str] = None, date_max: Optional[str] = None, prefer: Optional[str] = None, max_wait: Optional[int] = None, with_metadata: Optional[bool] = False, skip: Optional[int] = None, limit: Optional[int] = None) -> Dict: + endpoint = f"/api/v1/merge-api/explore/domain/certificates/{domain}" params = { - 'limit': limit, - 'skip': skip, - 'with_metadata': with_metadata, 'domain_regex': domain_regex, 'certificate_issuer': certificate_issuer, 'date_min': date_min, 'date_max': date_max, 'prefer': prefer, - 'max_wait': max_wait + 'max_wait': max_wait, + 'with_metadata': with_metadata, + 'skip': skip, + 'limit': limit, } - - - params = {k: v for k, v in params.items() if v is not None} - + filtered_params = {k: v for k, v in params.items() if v is not None} + response = self._http_request('GET', endpoint, params=filtered_params) + return response + + def search_domains(self, query: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, risk_score_min: Optional[int] = None, risk_score_max: Optional[int] = None, limit: int = 100, domain_regex: Optional[str] = None, name_server: Optional[str] = None, asnum: Optional[int] = None, asname: Optional[str] = None, min_ip_diversity: Optional[int] = None, registrar: Optional[str] = None, min_asn_diversity: Optional[int] = None, certificate_issuer: Optional[str] = None, whois_date_after: Optional[str] = None, skip: Optional[int] = None) -> dict: + url_suffix = 'explore/domain/search' + params = {k: v for k, v in { + 'domain': query, + 'start_date': start_date, + 'end_date': end_date, + 'risk_score_min': risk_score_min, + 'risk_score_max': risk_score_max, + 'limit': limit, + 'domain_regex': domain_regex, + 'name_server': name_server, + 'asnum': asnum, + 'asname': asname, + 'min_ip_diversity': min_ip_diversity, + 'registrar': registrar, + 'min_asn_diversity': min_asn_diversity, + 'certificate_issuer': certificate_issuer, + 'whois_date_after': whois_date_after, + 'skip': skip, + }.items() if v is not None} response = self._http_request('GET', url_suffix, params=params) + return response - job_status_url = response.get('response', {}).get('job_status', {}).get('get') - if not job_status_url: - demisto.error('Job status URL not found in the response') - return response - - job_complete = False - while not job_complete: - - - job_response = self._http_request('GET', job_status_url) - job_status = job_response.get('response', {}).get('job_status', {}).get('status') - - if job_status == 'COMPLETED': - job_complete = True - - - certificate_data = job_response.get('response', {}).get('domain_certificates', []) - return certificate_data - elif job_status == 'FAILED': - demisto.error('Job failed to complete.') - return {'error': 'Job failed'} - else: - - time.sleep(5) - - return {} - - - - def search_domains(self, - query: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - risk_score_min: Optional[int] = None, - risk_score_max: Optional[int] = None, - limit: int = 100, - domain_regex: Optional[str] = None, - name_server: Optional[str] = None, - asnum: Optional[int] = None, - asname: Optional[str] = None, - min_ip_diversity: Optional[int] = None, - registrar: Optional[str] = None, - min_asn_diversity: Optional[int] = None, - certificate_issuer: Optional[str] = None, - whois_date_after: Optional[str] = None, - skip: Optional[int] = None) -> dict: - """ - Searches for domains with optional filters and returns relevant information. - If filters are not specified, it performs a broad search. The function - supports additional optional parameters to narrow down the search results. - - Args: - query (Optional[str]): Search query string to match domains (e.g., domain pattern or keywords). - start_date (Optional[str]): Filter for domain registration dates on or after this date (ISO8601 format). - end_date (Optional[str]): Filter for domain registration dates on or before this date (ISO8601 format). - risk_score_min (Optional[int]): Minimum risk score for filtering domains. - risk_score_max (Optional[int]): Maximum risk score for filtering domains. - domain_regex (Optional[str]): A valid RE2 regular expression to match domain patterns. - name_server (Optional[str]): Name or wildcard pattern of name servers used by domains. - asnum (Optional[int]): Autonomous System (AS) number to filter domains. - asname (Optional[str]): Filter domains where the AS name begins with this string. - min_ip_diversity (Optional[int]): Filter domains with a minimum IP diversity limit. - registrar (Optional[str]): Name or partial name of the registrar for filtering domains. - min_asn_diversity (Optional[int]): Filter domains with a minimum ASN diversity limit. - certificate_issuer (Optional[str]): SSL certificate issuer name to filter domains. - whois_date_after (Optional[str]): Filter domains with a Whois created date after this date (ISO8601 format). - skip (Optional[int]): Number of results to skip for pagination. - limit (int): Maximum number of results to return (default: 100). - - Returns: - dict: A dictionary containing the search results, including the matched domains and related metadata. - - Raises: - Exception: If the API request fails or an error occurs during the process. - """ - - - url_suffix = 'explore/domain/search' - - params = {k: v for k, v in { - 'domain': query, - 'start_date': start_date, - 'end_date': end_date, - 'risk_score_min': risk_score_min, - 'risk_score_max': risk_score_max, - 'limit': limit, - 'domain_regex': domain_regex, - 'name_server': name_server, - 'asnum': asnum, - 'asname': asname, - 'min_ip_diversity': min_ip_diversity, - 'registrar': registrar, - 'min_asn_diversity': min_asn_diversity, - 'certificate_issuer': certificate_issuer, - 'whois_date_after': whois_date_after, - 'skip': skip, - }.items() if v is not None} - - try: - response = self._http_request('GET', url_suffix, params=params) - return response - except Exception as e: - demisto.error(f"Error in search_domains API request: {str(e)}") - return {'error': str(e)} - - - def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str = 'live', match: str = 'self', as_of: Optional[str] = None) -> dict: - """ - Get infratags for multiple domains with optional clustering and additional filtering options. - - Args: - domains (list): A list of domains to retrieve infratags for. - cluster (bool, optional): Whether to cluster the results. Defaults to False. - mode (str, optional): Mode for the lookup, either 'live' (default) or 'padns'. - match (str, optional): Handling of self-hosted infrastructure, either 'self' (default) or 'full'. - as_of (str, optional): Date or timestamp for filtering the data. - - Returns: - dict: A dictionary containing infratags for the provided domains. - """ - - - url = 'explore/bulk/domain/infratags' - + url = 'explore/bulk/domain/infratags' payload = { 'domains': domains } params = { 'mode': mode, 'match': match, - 'clusters': cluster + 'clusters': cluster } - + if as_of: params['as_of'] = as_of - try: - response = self._http_request( - method='POST', - url_suffix=url, - params=params, - data=payload - ) - return response - except Exception as e: - - demisto.error(f"Error fetching infratags: {str(e)}") - raise + response = self._http_request( + method='POST', + url_suffix=url, + params=params, + data=payload + ) + return response - - def get_enrichment_data(self, resource: str, resource_type: str, explain: bool = False, scan_data: bool = False) -> Dict: - """ - Retrieve comprehensive enrichment information for a given resource (domain, IPv4, or IPv6). - - Args: - resource (str): The resource identifier (domain name, IPv4 address, or IPv6 address) - resource_type (str): Type of resource ('domain', 'ipv4', 'ipv6') - explain (bool, optional): Whether to show details of data used to calculate scores (default: False) - scan_data (bool, optional): Whether to show details of data collected from scanning (default: False) - - Returns: - Dict: The enrichment data response from the API - - Raises: - ValueError: If resource_type is not one of 'domain', 'ipv4', 'ipv6' - DemistoException: If the API request fails - """ - if resource_type not in {'domain', 'ipv4', 'ipv6'}: - raise ValueError("resource_type must be one of: 'domain', 'ipv4', 'ipv6'") - - - url_suffix = f'explore/enrich/{resource_type}/{resource}' - + def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] = False, scan_data: Optional[bool] = False) -> dict: + url_suffix = f'explore/enrich/{resource}/{value}' params = { - 'explain': 1 if explain else 0, - 'scan_data': 1 if scan_data else 0 + 'explain': explain, + 'scan_data': scan_data } - + params = {k: v for k, v in params.items() if v is not None} try: - response = self._http_request( - method='GET', - url_suffix=url_suffix, - params=params - ) - + response = self._http_request('GET', url_suffix, params=params) return response - except Exception as e: - raise DemistoException(f'Failed to fetch enrichment data for {resource_type} {resource}: {str(e)}') + demisto.error(f'Error in get_enrichment_data API request: {str(e)}') + raise + def list_ip_information(self, ips: str) -> Dict[str, Any]: + if not ips: + raise ValueError("The 'ips' parameter is required and cannot be empty.") + url_suffix = "explore/bulk/ip2asn/resource" + headers = self._headers + payload = {"ips": ips} + response = self._http_request( + method="POST", + url_suffix=url_suffix, + data=payload + ) + return response + + def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) -> Dict[str, Any]: + url_suffix = f"explore/ipreputation/history/asn/{asn}" + query_params = {} + if explain: + query_params['explain'] = 'true' + if limit: + query_params['limit'] = limit + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=query_params + ) + return response - def list_ip_information(self, resource: str, explain: bool = False, scan_data: bool = False, sparse: Optional[str] = None) -> Dict: - """ - Fetches information for an IP address or domain. - - Args: - resource: The IP or domain resource to query - explain: Whether to show details of data used to calculate scores - scan_data: Whether to include scan data (IPv4 only) - sparse: Optional specific data to return ('asn', 'asname', or 'sp_risk_score') - - Returns: - Dict: Results for the requested IP or domain - """ - - params = { - 'ips': [resource], - 'explain': 1 if explain else 0, - 'scan_data': 1 if scan_data else 0, - 'sparse': sparse if sparse else '' - } - - url_suffix = "explore/bulk/ip2asn" - - try: - response = self._http_request( - method='POST', - url_suffix=url_suffix, - data=params - ) - - return response - except Exception as e: - demisto.error(f"Error fetching information for resource {resource}: {str(e)}") - return {'error': str(e)} + def get_asn_takedown_reputation(client, args): + asn = args.get('asn') + explain = argToBoolean(args.get('explain', 'false')) + limit = args.get('limit') - def get_asn_reputation(self, asn: str) -> Dict: - """ - Retrieve reputation information for an Autonomous System Number (ASN). - - Args: - asn (str): The ASN to lookup (can be with or without 'AS' prefix) - - Returns: - Dict: The reputation information response from the API - - Raises: - ValueError: If ASN is invalid - DemistoException: If the API request fails - """ - if not asn: - raise ValueError("ASN cannot be empty") - - - asn_number = asn.upper().replace('AS', '') - if not asn_number.isdigit(): - raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") - - - - try: - url_suffix = f'explore/ipreputation/history/asn/{asn_number}' - response = self._http_request( - method='GET', - url_suffix=url_suffix - ) - - return response - - except Exception as e: - raise DemistoException(f'Failed to fetch ASN reputation for {asn}: {str(e)}') - - def get_asn_takedown_reputation(self, asn: str) -> Dict: - """ - Retrieve takedown reputation information for an Autonomous System Number (ASN). - - Args: - asn (str): The ASN to lookup (can be with or without 'AS' prefix) - - Returns: - Dict: The takedown reputation information response from the API - - Raises: - ValueError: If ASN is invalid - DemistoException: If the API request fails - """ if not asn: - raise ValueError("ASN cannot be empty") - - - asn_number = asn.upper().replace('AS', '') - if not asn_number.isdigit(): - raise ValueError("Invalid ASN format. Must be a number or start with 'AS' followed by a number") - - - - try: - url_suffix = f'explore/ipreputation/takedown/asn/{asn_number}' - response = self._http_request( - method='GET', - url_suffix=url_suffix - ) - - return response - - except Exception as e: - raise DemistoException(f'Failed to fetch ASN takedown reputation for {asn}: {str(e)}') + raise ValueError('The "asn" argument is required.') + endpoint = f'/api/v1/merge-api/explore/takedownreputation/history/asn/{asn}' + params = {} + if explain: + params['explain'] = explain + if limit: + params['limit'] = limit + response = client._http_request( + method='GET', + url_suffix=endpoint, + params=params + ) + + outputs = response.get('response', {}) + readable_output = tableToMarkdown( + f'Takedown Reputation for ASN {asn}', + outputs, + headers=['asn', 'score', 'details', 'timestamp'] + ) + + return CommandResults( + outputs_prefix='SilentPush.TakedownReputation', + outputs_key_field='asn', + outputs=outputs, + readable_output=readable_output, + raw_response=response + ) def test_module(client: Client) -> str: - """ - Tests connectivity to the SilentPush API and checks the authentication status. - - This function will validate the API key and ensure that the client can successfully connect - to the API. It is called when running the 'Test' button in XSOAR. - - Args: - client (Client): The client instance to use for the connection test. - - Returns: - str: 'ok' if the connection is successful, otherwise returns an error message. - """ - try: client.list_domain_information('silentpush.com') - return 'ok' except DemistoException as e: - if 'Forbidden' in str(e) or 'Authorization' in str(e): return 'Authorization Error: make sure API Key is correctly set' raise e - - -''' COMMAND FUNCTIONS ''' - def list_domain_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for the 'silentpush-list-domain-information' command. - - Args: - client (Client): The client instance for API requests. - args (Dict[str, Any]): Command arguments passed from XSOAR. - - Returns: - CommandResults: Formatted results for XSOAR. - """ - domains_arg = args.get('domains') or args.get('domain') if not domains_arg: - raise DemistoException('No domains provided. Use the "domain" or "domains" argument.') + raise DemistoException('No domains provided') domains = [domain.strip() for domain in domains_arg.split(',') if domain.strip()] - if len(domains) > 100: - raise DemistoException("A maximum of 100 domains can be submitted in a single request.") - - fetch_risk_score = argToBoolean(args.get('fetch_risk_score', False)) fetch_whois_info = argToBoolean(args.get('fetch_whois_info', False)) - raw_response = client.list_domain_information(domains, fetch_risk_score, fetch_whois_info) - - markdown = ['# Domain Information Results\n'] for domain_info in raw_response.get('domains', []): markdown.append(f"## Domain: {domain_info.get('domain', 'N/A')}") - basic_info = { 'Created Date': domain_info.get('whois_created_date', 'N/A'), + 'Updated Date': domain_info.get('whois_updated_date', 'N/A'), + 'Expiration Date': domain_info.get('whois_expiration_date', 'N/A'), 'Registrar': domain_info.get('registrar', 'N/A'), - 'Age (days)': domain_info.get('age', 'N/A'), - 'Risk Score': domain_info.get('sp_risk_score', 'N/A'), + 'Status': domain_info.get('status', 'N/A'), + 'Name Servers': domain_info.get('nameservers', 'N/A') } markdown.append(tableToMarkdown('Domain Information', [basic_info])) - - if risk_explain := domain_info.get('sp_risk_score_explain'): - markdown.append(f'### Risk Score Explanation\n{risk_explain}') - - - whois_info = domain_info.get('whois_info', {}) - if isinstance(whois_info, dict): - whois_table = [{'Key': k, 'Value': v} for k, v in whois_info.items()] - markdown.append(tableToMarkdown('WHOIS Information', whois_table)) + if fetch_risk_score: + risk_info = { + 'Risk Score': domain_info.get('sp_risk_score', 'N/A'), + 'Risk Score Explanation': domain_info.get('sp_risk_score_explain', 'N/A') + } + markdown.append(tableToMarkdown('Risk Assessment', [risk_info])) + + if fetch_whois_info and domain_info.get('whois_info') != 'N/A': + whois_info = domain_info.get('whois_info', {}) + if isinstance(whois_info, dict): + whois_data = { + 'Registrant Name': whois_info.get('registrant_name', 'N/A'), + 'Registrant Organization': whois_info.get('registrant_organization', 'N/A'), + 'Registrant Email': whois_info.get('registrant_email', 'N/A'), + 'Admin Email': whois_info.get('admin_email', 'N/A'), + 'Tech Email': whois_info.get('tech_email', 'N/A') + } + markdown.append(tableToMarkdown('WHOIS Information', [whois_data])) markdown.append('\n---\n') - readable_output = '\n'.join(markdown) - - return CommandResults( outputs_prefix='SilentPush.Domain', outputs_key_field='domain', outputs=raw_response.get('domains', []), - readable_output=readable_output, + readable_output='\n'.join(markdown), raw_response=raw_response ) - def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching SSL/TLS certificate data for a given domain. - - Args: - client (Client): The client instance. - args (Dict[str, Any]): The arguments passed to the command (including the domain). - - Returns: - CommandResults: The formatted result for XSOAR. - """ domain = args.get('domain') if not domain: raise DemistoException('Domain argument is required.') - certificate_data = client.get_domain_certificates(domain) if not certificate_data: raise DemistoException(f'No certificate data found for domain: {domain}') - markdown = [f'# SSL/TLS Certificate Information for Domain: {domain}\n'] - if isinstance(certificate_data, list) and certificate_data: for cert in certificate_data: markdown.append(f"## Certificate for {domain}") @@ -643,26 +351,23 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com } markdown.append(tableToMarkdown('Certificate Information', [cert_info])) - metadata = cert.get('metadata', {}) if metadata: markdown.append(f"### Metadata: {metadata}") else: markdown.append(f'No certificate data available for domain: {domain}') - metadata = { 'job_id': certificate_data.get('response', {}).get('metadata', {}).get('job_id'), 'query_name': certificate_data.get('response', {}).get('metadata', {}).get('query_name'), 'results_returned': certificate_data.get('response', {}).get('metadata', {}).get('results_returned'), 'results_total_at_least': certificate_data.get('response', {}).get('metadata', {}).get('results_total_at_least') } - + job_status = certificate_data.get('response', {}).get('job_status', {}) job_status_url = job_status.get('get') job_status_status = job_status.get('status', 'N/A') - raw_response = { 'certificate_data': certificate_data, 'metadata': metadata, @@ -679,7 +384,6 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com readable_output='\n'.join(markdown), raw_response=raw_response ) - def search_domains_command(client: Client, args: dict) -> CommandResults: query = args.get('query') @@ -725,7 +429,7 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_prefix='SilentPush.Error', outputs_key_field='error' ) - + if raw_response.get('error'): return CommandResults( readable_output=f"Error: {raw_response['error']}", @@ -733,9 +437,9 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_prefix='SilentPush.Error', outputs_key_field='error' ) - + records = raw_response.get('response', {}).get('records', []) - + if not records: return CommandResults( readable_output="No domains found.", @@ -744,9 +448,9 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: outputs_key_field='domain', outputs=raw_response ) - + readable_output = tableToMarkdown('Domain Search Results', records) - + return CommandResults( outputs_prefix='SilentPush.SearchResults', outputs_key_field='domain', @@ -755,36 +459,21 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) - def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: - """ - Command handler for fetching infratags for multiple domains. - - Args: - client (Client): The client instance to fetch the data. - args (dict): The arguments passed to the command, including domains, clustering option, and optional filters. - - Returns: - CommandResults: The command results containing readable output and the raw response. - """ domains = argToList(args.get('domains', '')) - cluster = argToBoolean(args.get('cluster', False)) + cluster = argToBoolean(args.get('cluster', False)) mode = args.get('mode', 'live') match = args.get('match', 'self') as_of = args.get('as_of', None) - + if not domains: raise ValueError('"domains" argument is required and cannot be empty.') - try: - raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) - except Exception as e: - demisto.error(f'Error occurred while fetching infratags: {str(e)}') - raise + raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of) infratags = raw_response.get('response', {}).get('infratags', []) tag_clusters = raw_response.get('response', {}).get('tag_clusters', []) - + readable_output = tableToMarkdown('Domain Infratags', infratags) if tag_clusters: readable_output += tableToMarkdown('Domain Tag Clusters', tag_clusters) @@ -797,292 +486,110 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) - - -def get_enrichment_data_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching enrichment data for a specific resource. - - Args: - client (Client): The client instance to fetch the data - args (dict): Command arguments including: - - resource (str): The resource (e.g., domain, IP address, etc.) - - resource_type (str): The type of resource ('domain', 'ip', etc.) - - explain (bool): Whether to show calculation details - - scan_data (bool): Whether to include scan data (IPv4 only) - - Returns: - CommandResults: XSOAR command results - """ - +def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: resource = args.get('resource') - resource_type = args.get('resource_type') - - if not resource or not resource_type: - raise DemistoException('Resource and resource_type are required arguments.') - - explain = argToBoolean(args.get('explain', False)) - scan_data = argToBoolean(args.get('scan_data', False)) - + value = args.get('value') + explain = args.get('explain', 'false').lower() == 'true' + scan_data = args.get('scan_data', 'false').lower() == 'true' + + if not resource or not value: + raise ValueError("The 'resource' and 'value' arguments are required.") + try: - raw_response = client.get_enrichment_data(resource, resource_type, explain, scan_data) - enrichment_data = raw_response.get('data', []) - - markdown = [f"### Enrichment Data for {resource} ({resource_type})\n"] - - for data in enrichment_data: - markdown.append(f"#### Enrichment Data:\n") - markdown.append(f"Data: {data}\n") - - return CommandResults( - outputs_prefix='SilentPush.Enrichment', - outputs_key_field='resource', - outputs=enrichment_data, - readable_output='\n'.join(markdown), - raw_response=raw_response + raw_response = client.get_enrichment_data( + resource=resource, + value=value, + explain=explain, + scan_data=scan_data ) - except Exception as e: - demisto.error(f"Error in get_enrichment_data_command: {str(e)}") - raise - + return CommandResults( + readable_output=f"Error: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error' + ) -def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching IP information. - - Args: - client (Client): The client instance to fetch the data - args (dict): Command arguments including: - - ips (str): Comma-separated list of IP addresses - - explain (bool): Whether to show calculation details - - scan_data (bool): Whether to include scan data (IPv4 only) - - sparse (str): Optional specific data to return - - Returns: - CommandResults: XSOAR command results - """ - - ips = args.get('ips') - if not ips: - raise DemistoException('No IPs provided. Please provide IPs using the "ips" argument.') - - explain = argToBoolean(args.get('explain', False)) - scan_data = argToBoolean(args.get('scan_data', False)) - sparse = args.get('sparse') - - if sparse and sparse not in ['asn', 'asname', 'sp_risk_score']: - raise DemistoException('Invalid sparse value. Must be one of: asn, asname, sp_risk_score') - - try: - ip_list = [ip.strip() for ip in ips.split(',')] - - results = [] - markdown = ['### IP Information Results\n'] - - for ip in ip_list: - resource = ip - raw_response = client.list_ip_information(resource, explain, scan_data, sparse) - ip_data = raw_response.get('ips', []) - - for ip_info in ip_data: - if 'error' in ip_info: - markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} (Error)\n") - markdown.append(f"Error: {ip_info['error']}\n") - continue - - markdown.append(f"#### IP: {ip_info.get('ip', 'N/A')} ({ip_info.get('ip_type', 'unknown').upper()})") - - basic_info = { - 'ASN': ip_info.get('asn', 'N/A'), - 'AS Name': ip_info.get('asname', 'N/A'), - 'Risk Score': ip_info.get('sp_risk_score', 'N/A'), - 'Subnet': ip_info.get('subnet', 'N/A') - } - markdown.append(tableToMarkdown('Basic Information', [basic_info], headers=basic_info.keys())) - - if location_info := ip_info.get('ip_location', {}): - location_data = { - 'Country': location_info.get('country_name', 'N/A'), - 'Continent': location_info.get('continent_name', 'N/A'), - 'EU Member': 'Yes' if location_info.get('country_is_in_european_union') else 'No' - } - markdown.append(tableToMarkdown('Location Information', [location_data], headers=location_data.keys())) - - if ip_info.get('ip_type') == 'ipv4': - additional_info = { - 'PTR Record': ip_info.get('ip_ptr', 'N/A'), - 'Is TOR Exit Node': 'Yes' if ip_info.get('ip_is_tor_exit_node') else 'No', - 'Is DSL/Dynamic': 'Yes' if ip_info.get('ip_is_dsl_dynamic') else 'No' - } - markdown.append(tableToMarkdown('Additional Information', [additional_info], headers=additional_info.keys())) - - markdown.append('\n') - + if raw_response.get('error'): return CommandResults( - outputs_prefix='SilentPush.IP', - outputs_key_field='ip', - outputs=ip_data, - readable_output='\n'.join(markdown), - raw_response=raw_response + readable_output=f"Error: {raw_response['error']}", + raw_response=raw_response, + outputs_prefix='SilentPush.Error', + outputs_key_field='error' ) - - except Exception as e: - demisto.error(f"Error in list_ip_information_command: {str(e)}") - raise - -def get_asn_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching ASN reputation information. - - Args: - client (Client): The client instance to fetch the data - args (dict): Command arguments including: - - asn (str): The ASN to lookup - - Returns: - CommandResults: XSOAR command results - """ - asn = args.get('asn') - if not asn: - raise DemistoException('ASN is a required argument') - - try: - raw_response = client.get_asn_reputation(asn) - reputation_data = raw_response.get('response', {}) - - - markdown = [f"### ASN Reputation Information for {asn}\n"] - - - if basic_info := reputation_data.get('reputation', {}): - reputation_table = { - 'Risk Score': basic_info.get('risk_score', 'N/A'), - 'First Seen': basic_info.get('first_seen', 'N/A'), - 'Last Seen': basic_info.get('last_seen', 'N/A'), - 'Total Reports': basic_info.get('total_reports', 'N/A') - } - markdown.append(tableToMarkdown('Reputation Overview', [reputation_table])) - - - if history := reputation_data.get('history', []): - history_table = [] - for entry in history: - history_table.append({ - 'Date': entry.get('date', 'N/A'), - 'Risk Score': entry.get('risk_score', 'N/A'), - 'Reports': entry.get('reports', 'N/A') - }) - if history_table: - markdown.append('\n### Historical Reputation Data') - markdown.append(tableToMarkdown('', history_table)) - - - if metadata := reputation_data.get('metadata', {}): - metadata_table = {k: str(v) for k, v in metadata.items()} - if metadata_table: - markdown.append('\n### Additional Information') - markdown.append(tableToMarkdown('', [metadata_table])) - + + enrichment_data = raw_response.get('response', {}) + + if not enrichment_data: return CommandResults( - outputs_prefix='SilentPush.ASNReputation', - outputs_key_field='asn', - outputs={ - 'asn': asn, - 'reputation': reputation_data - }, - readable_output='\n'.join(markdown), - raw_response=raw_response + readable_output="No enrichment data found.", + raw_response=raw_response, + outputs_prefix='SilentPush.EnrichmentData', + outputs_key_field='resource', + outputs=raw_response ) - - except Exception as e: - demisto.error(f"Error in get_asn_reputation_command: {str(e)}") - raise - -def get_asn_takedown_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: - """ - Command handler for fetching ASN takedown reputation information. - - Args: - client (Client): The client instance to fetch the data - args (dict): Command arguments including: - - asn (str): The ASN to lookup - - Returns: - CommandResults: XSOAR command results - """ - asn = args.get('asn') + + readable_output = tableToMarkdown('Enrichment Data', enrichment_data) + + return CommandResults( + outputs_prefix='SilentPush.EnrichmentData', + outputs_key_field='resource', + outputs=enrichment_data, + readable_output=readable_output, + raw_response=raw_response + ) + +def list_ip_information_command(client: Any, args: Dict[str, Any]) -> CommandResults: + ips = args.get("ips") + if not ips: + raise ValueError("The 'ips' parameter is required.") + + response = client.list_ip_information(ips=ips) + + outputs = response.get("response", {}) + readable_output = tableToMarkdown( + "IP Information", + outputs, + headers=["ip", "asn", "organization", "country", "risk_score"], + removeNull=True + ) + + return CommandResults( + outputs_prefix="SilentPush.IPInformation", + outputs_key_field="ip", + outputs=outputs, + readable_output=readable_output, + raw_response=response + ) + +def get_asn_reputation_command(self, args: dict) -> CommandResults: + asn = args.get("asn") + explain = argToBoolean(args.get("explain", False)) + limit = arg_to_number(args.get("limit", None)) + if not asn: - raise DemistoException('ASN is a required argument') - + raise ValueError("ASN is required.") + try: - raw_response = client.get_asn_takedown_reputation(asn) - takedown_data = raw_response.get('response', {}) - - - markdown = [f"### ASN Takedown Reputation Information for {asn}\n"] - - - if basic_info := takedown_data.get('takedown', {}): - takedown_table = { - 'Risk Score': basic_info.get('risk_score', 'N/A'), - 'First Seen': basic_info.get('first_seen', 'N/A'), - 'Last Seen': basic_info.get('last_seen', 'N/A'), - 'Total Reports': basic_info.get('total_reports', 'N/A') - } - markdown.append(tableToMarkdown('Takedown Reputation Overview', [takedown_table])) - - - if history := takedown_data.get('history', []): - history_table = [] - for entry in history: - history_table.append({ - 'Date': entry.get('date', 'N/A'), - 'Risk Score': entry.get('risk_score', 'N/A'), - 'Reports': entry.get('reports', 'N/A') - }) - if history_table: - markdown.append('\n### Historical Takedown Reputation Data') - markdown.append(tableToMarkdown('', history_table)) - - - if metadata := takedown_data.get('metadata', {}): - metadata_table = {k: str(v) for k, v in metadata.items()} - if metadata_table: - markdown.append('\n### Additional Information') - markdown.append(tableToMarkdown('', [metadata_table])) - - return CommandResults( - outputs_prefix='SilentPush.ASNTakedownReputation', - outputs_key_field='asn', - outputs={ - 'asn': asn, - 'takedown_reputation': takedown_data - }, - readable_output='\n'.join(markdown), - raw_response=raw_response - ) - - except Exception as e: - demisto.error(f"Error in get_asn_takedown_reputation_command: {str(e)}") - raise + asn_reputation_data = self.get_asn_reputation(asn, explain, limit) + command_results = CommandResults( + outputs_prefix="SilentPush.ASNReputation", + outputs_key_field="asn", + outputs=asn_reputation_data, + raw_response=asn_reputation_data + ) + return command_results -''' MAIN FUNCTION ''' + except Exception as e: + raise DemistoException(f"Error retrieving ASN reputation data: {str(e)}") +def get_asn_takedown_reputation_command(client, args): + return get_asn_takedown_reputation(client, args) def main(): - """ - Main function to initialize the client and process the commands. - - This function parses the parameters, sets up the client, and routes the command to - the appropriate function. - It handles the setup of authentication, base URL, SSL verification, and proxy configuration. - Also, it routes the `test-module` and `silentpush-list-domain-information` commands to the - corresponding functions. - """ try: params = demisto.params() api_key = params.get('credentials', {}).get('password') @@ -1100,7 +607,6 @@ def main(): command = demisto.command() command_handlers = { - 'test-module': test_module, 'silentpush-list-domain-information': list_domain_information_command, 'silentpush-get-domain-certificates': get_domain_certificates_command, @@ -1109,7 +615,7 @@ def main(): 'silentpush-get-enrichment-data' : get_enrichment_data_command, 'silentpush-list-ip-information' : list_ip_information_command, 'silentpush-get-asn-reputation' : get_asn_reputation_command, - 'silentpush-get-asn-takedown-reputation' : get_asn_takedown_reputation_command + 'silentpush-get-asn-takedown-reputation': get_asn_takedown_reputation_command } if command in command_handlers: @@ -1124,10 +630,9 @@ def main(): except Exception as e: demisto.error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') return_error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') - - + + ''' ENTRY POINT ''' - if __name__ in ('__main__', '__builtin__', 'builtins'): - main() \ No newline at end of file + main() From 6b77f9188d6d62454ae554d0753fd4d1d2060cdf Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 24 Jan 2025 10:53:12 +0530 Subject: [PATCH 13/19] added commands --- .../Integrations/SilentPush/SilentPush.py | 566 +++++++++++++----- 1 file changed, 414 insertions(+), 152 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 657d1d75b9fb..b127e14b94c4 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -8,7 +8,6 @@ Linting: https://xsoar.pan.dev/docs/integrations/linting """ - import requests import urllib3 from typing import Any, Optional, Dict @@ -50,27 +49,7 @@ def __init__(self, base_url: str, api_key: str, verify: bool = True, proxy: bool } def _http_request(self, method: str, url_suffix: str, params: dict = None, data: dict = None) -> Any: - """ - Handles the HTTP requests to the SilentPush API. - - This function builds the request URL, adds the necessary headers, and sends a request - to the API. It returns the response in JSON format. - - Args: - method (str): The HTTP method (GET, POST, etc.). - url_suffix (str): The specific endpoint to be appended to the base URL. - params (dict, optional): The URL parameters to be sent with the request. - data (dict, optional): The data to be sent with the request. - - Returns: - Any: The JSON response from the API. - - Raises: - DemistoException: If there is an error in the API response. - """ full_url = f'{self.base_url}{url_suffix}' - masked_headers = {k: v if k != 'X-API-Key' else '****' for k, v in self._headers.items()} - try: response = requests.request( method, @@ -80,13 +59,12 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: params=params, json=data ) - if response.status_code not in {200, 201}: raise DemistoException(f'Error in API call [{response.status_code}] - {response.text}') - return response.json() + return response.json() except Exception as e: - demisto.error(f'Error in API call: {str(e)}') - raise + raise DemistoException(f'Error in API call: {str(e)}') + def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: if len(domains) > 100: @@ -123,23 +101,14 @@ def list_domain_information(self, domains: List[str], fetch_risk_score: Optional return {'domains': combined_results} - def get_domain_certificates(self, domain: str, domain_regex: Optional[str] = None, certificate_issuer: Optional[str] = None, date_min: Optional[str] = None, date_max: Optional[str] = None, prefer: Optional[str] = None, max_wait: Optional[int] = None, with_metadata: Optional[bool] = False, skip: Optional[int] = None, limit: Optional[int] = None) -> Dict: - endpoint = f"/api/v1/merge-api/explore/domain/certificates/{domain}" - params = { - 'domain_regex': domain_regex, - 'certificate_issuer': certificate_issuer, - 'date_min': date_min, - 'date_max': date_max, - 'prefer': prefer, - 'max_wait': max_wait, - 'with_metadata': with_metadata, - 'skip': skip, - 'limit': limit, - } - filtered_params = {k: v for k, v in params.items() if v is not None} - response = self._http_request('GET', endpoint, params=filtered_params) + def get_domain_certificates(self, domain: str, **kwargs) -> dict: + url_suffix = f"explore/domain/certificates/{domain}" + params = {k: v for k, v in kwargs.items() if v is not None} + response = self._http_request('GET', url_suffix, params=params) + demisto.debug(f"Raw response for get_domain_certificates: {response}") return response + def search_domains(self, query: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, risk_score_min: Optional[int] = None, risk_score_max: Optional[int] = None, limit: int = 100, domain_regex: Optional[str] = None, name_server: Optional[str] = None, asnum: Optional[int] = None, asname: Optional[str] = None, min_ip_diversity: Optional[int] = None, registrar: Optional[str] = None, min_asn_diversity: Optional[int] = None, certificate_issuer: Optional[str] = None, whois_date_after: Optional[str] = None, skip: Optional[int] = None) -> dict: url_suffix = 'explore/domain/search' params = {k: v for k, v in { @@ -187,32 +156,26 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str return response def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] = False, scan_data: Optional[bool] = False) -> dict: - url_suffix = f'explore/enrich/{resource}/{value}' - params = { - 'explain': explain, - 'scan_data': scan_data - } - params = {k: v for k, v in params.items() if v is not None} - try: - response = self._http_request('GET', url_suffix, params=params) - return response - except Exception as e: - demisto.error(f'Error in get_enrichment_data API request: {str(e)}') - raise - - def list_ip_information(self, ips: str) -> Dict[str, Any]: - if not ips: - raise ValueError("The 'ips' parameter is required and cannot be empty.") - url_suffix = "explore/bulk/ip2asn/resource" - headers = self._headers - payload = {"ips": ips} + parameters = ["resource", "value"] + endpoint = "/api/v1/merge-api/explore/enrich/{{resource}}/{{value}}" + + for param in parameters: + endpoint = endpoint.replace(f"{{{param}}}", str(locals().get(param))) + + query_params = {} + if explain: + query_params["explain"] = int(explain) + if scan_data: + query_params["scan_data"] = int(scan_data) + response = self._http_request( - method="POST", - url_suffix=url_suffix, - data=payload + method="GET", + url_suffix=endpoint, + params=query_params ) return response + def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) -> Dict[str, Any]: url_suffix = f"explore/ipreputation/history/asn/{asn}" query_params = {} @@ -227,7 +190,7 @@ def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) ) return response - def get_asn_takedown_reputation(client, args): + def get_asn_takedown_reputation(self, args): asn = args.get('asn') explain = argToBoolean(args.get('explain', 'false')) limit = args.get('limit') @@ -235,7 +198,7 @@ def get_asn_takedown_reputation(client, args): if not asn: raise ValueError('The "asn" argument is required.') - endpoint = f'/api/v1/merge-api/explore/takedownreputation/history/asn/{asn}' + endpoint = f'explore/takedownreputation/history/asn/{asn}' params = {} if explain: @@ -243,7 +206,7 @@ def get_asn_takedown_reputation(client, args): if limit: params['limit'] = limit - response = client._http_request( + response = self._http_request( method='GET', url_suffix=endpoint, params=params @@ -264,6 +227,133 @@ def get_asn_takedown_reputation(client, args): raw_response=response ) + def get_ipv4_reputation(self, ipv4, explain=False, limit=None): + url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" + params = {} + if explain: + params['explain'] = 'true' + if limit is not None: + params['limit'] = limit + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_type: Optional[str] = None) -> Dict[str, Any]: + """ + Retrieve status or results of a specified job. + + Args: + job_id (str): ID of the job to retrieve status for. + max_wait (Optional[int]): Number of seconds to wait for results (0-25 seconds). + result_type (Optional[str]): Type of result to include (Status, Include Metadata, Exclude Metadata). + + Returns: + Dict[str, Any]: Job status or result information. + """ + url_suffix = f"explore/job/{job_id}" + + params = {} + if max_wait is not None: + if not isinstance(max_wait, int) or max_wait < 0 or max_wait > 25: + raise ValueError("max_wait must be an integer between 0 and 25") + params['max_wait'] = max_wait + + if result_type: + valid_result_types = ['Status', 'Include Metadata', 'Exclude Metadata'] + if result_type not in valid_result_types: + raise ValueError(f"result_type must be one of {valid_result_types}") + params['result_type'] = result_type + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: + """ + Retrieve reputation information for a nameserver. + + Args: + nameserver (str): Name of the nameserver to check. + explain (Optional[bool]): Whether to include calculation details. + limit (Optional[int]): Maximum number of reputation history entries to retrieve. + + Returns: + Dict[str, Any]: Nameserver reputation data. + """ + url_suffix = f"explore/nsreputation/history/nameserver/{nameserver}" + + params = {} + if explain: + params['explain'] = str(explain).lower() + if limit is not None: + params['limit'] = limit + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: + """ + Retrieve reputation information for a subnet. + + Args: + subnet (str): IPv4 subnet to check (e.g., 192.1.0.0/24). + explain (Optional[bool]): Whether to include calculation details. + limit (Optional[int]): Maximum number of reputation history entries to retrieve. + + Returns: + Dict[str, Any]: Subnet reputation data. + """ + url_suffix = f"explore/ipreputation/history/subnet/{subnet}" + + params = {} + if explain: + params['explain'] = str(explain).lower() + if limit is not None: + params['limit'] = limit + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: + """ + Retrieve ASNs associated with a domain's A records. + + Args: + domain (str): Domain name to search for ASNs. + + Returns: + Dict[str, Any]: ASNs seen for the domain. + """ + url_suffix = f"explore/padns/lookup/domain/asns/{domain}" + + response = self._http_request( + method="GET", + url_suffix=url_suffix + ) + + return response + + + def test_module(client: Client) -> str: try: client.list_domain_information('silentpush.com') @@ -332,59 +422,56 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com if not domain: raise DemistoException('Domain argument is required.') - certificate_data = client.get_domain_certificates(domain) - - if not certificate_data: - raise DemistoException(f'No certificate data found for domain: {domain}') - - markdown = [f'# SSL/TLS Certificate Information for Domain: {domain}\n'] + params = { + 'domain_regex': args.get('domain_regex'), + 'certificate_issuer': args.get('certificate_issuer'), + 'date_min': args.get('date_min'), + 'date_max': args.get('date_max'), + 'prefer': args.get('prefer'), + 'max_wait': arg_to_number(args.get('max_wait')), + 'with_metadata': argToBoolean(args.get('with_metadata')), + 'skip': arg_to_number(args.get('skip')), + 'limit': arg_to_number(args.get('limit')) + } - if isinstance(certificate_data, list) and certificate_data: - for cert in certificate_data: - markdown.append(f"## Certificate for {domain}") - cert_info = { - 'Issuer': cert.get('issuer', 'N/A'), - 'Issued On': str(cert.get('issued_on', 'N/A')), - 'Expires On': str(cert.get('expires_on', 'N/A')), - 'Common Name': cert.get('common_name', 'N/A'), - 'Subject Alternative Names': ', '.join(cert.get('subject_alt_names', [])), - } - markdown.append(tableToMarkdown('Certificate Information', [cert_info])) + raw_response = client.get_domain_certificates(domain, **params) - metadata = cert.get('metadata', {}) - if metadata: - markdown.append(f"### Metadata: {metadata}") + if isinstance(raw_response, list): + certificates = raw_response + metadata = {} else: - markdown.append(f'No certificate data available for domain: {domain}') - - metadata = { - 'job_id': certificate_data.get('response', {}).get('metadata', {}).get('job_id'), - 'query_name': certificate_data.get('response', {}).get('metadata', {}).get('query_name'), - 'results_returned': certificate_data.get('response', {}).get('metadata', {}).get('results_returned'), - 'results_total_at_least': certificate_data.get('response', {}).get('metadata', {}).get('results_total_at_least') - } + certificates = raw_response.get('response', {}).get('domain_certificates', []) + metadata = raw_response.get('response', {}).get('metadata', {}) - job_status = certificate_data.get('response', {}).get('job_status', {}) - job_status_url = job_status.get('get') - job_status_status = job_status.get('status', 'N/A') + if not certificates: + return CommandResults( + readable_output=f"No certificates found for domain: {domain}", + outputs_prefix='SilentPush.Certificate', + outputs_key_field='domain', + outputs={'domain': domain, 'certificates': [], 'metadata': metadata}, + raw_response=raw_response + ) - raw_response = { - 'certificate_data': certificate_data, - 'metadata': metadata, - 'job_status': { - 'url': job_status_url, - 'status': job_status_status + markdown = [f"# SSL/TLS Certificate Information for Domain: {domain}\n"] + for cert in certificates: + cert_info = { + 'Issuer': cert.get('issuer', 'N/A'), + 'Issued On': cert.get('issued_on', 'N/A'), + 'Expires On': cert.get('expires_on', 'N/A'), + 'Common Name': cert.get('common_name', 'N/A'), + 'Subject Alternative Names': ', '.join(cert.get('subject_alt_names', [])), } - } + markdown.append(tableToMarkdown('Certificate Information', [cert_info])) return CommandResults( outputs_prefix='SilentPush.Certificate', outputs_key_field='domain', - outputs={'domain': domain, 'certificates': certificate_data, 'metadata': metadata}, + outputs={'domain': domain, 'certificates': certificates, 'metadata': metadata}, readable_output='\n'.join(markdown), raw_response=raw_response ) + def search_domains_command(client: Client, args: dict) -> CommandResults: query = args.get('query') start_date = args.get('start_date') @@ -485,68 +572,59 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: readable_output=readable_output, raw_response=raw_response ) - + + def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: - resource = args.get('resource') - value = args.get('value') - explain = args.get('explain', 'false').lower() == 'true' - scan_data = args.get('scan_data', 'false').lower() == 'true' + resource = args.get("resource") + value = args.get("value") + explain = argToBoolean(args.get("explain", False)) + scan_data = argToBoolean(args.get("scan_data", False)) if not resource or not value: - raise ValueError("The 'resource' and 'value' arguments are required.") + raise ValueError("Both 'resource' and 'value' arguments are required.") - try: - raw_response = client.get_enrichment_data( - resource=resource, - value=value, - explain=explain, - scan_data=scan_data - ) - except Exception as e: - return CommandResults( - readable_output=f"Error: {str(e)}", - raw_response={}, - outputs_prefix='SilentPush.Error', - outputs_key_field='error' - ) + response = client.get_enrichment_data(resource=resource, value=value, explain=explain, scan_data=scan_data) - if raw_response.get('error'): - return CommandResults( - readable_output=f"Error: {raw_response['error']}", - raw_response=raw_response, - outputs_prefix='SilentPush.Error', - outputs_key_field='error' - ) - - enrichment_data = raw_response.get('response', {}) + error_path = "response.ip2asn" if resource == "ip" else "response.domaininfo" + enrichment_data = response.get(error_path, {}) if not enrichment_data: return CommandResults( - readable_output="No enrichment data found.", - raw_response=raw_response, - outputs_prefix='SilentPush.EnrichmentData', - outputs_key_field='resource', - outputs=raw_response + readable_output=f"No enrichment data found for resource: {value}", + outputs_prefix="SilentPush.Enrichment", + outputs_key_field="value", + outputs={"value": value, "data": {}}, + raw_response=response ) - readable_output = tableToMarkdown('Enrichment Data', enrichment_data) + headers = list(enrichment_data.keys()) + rows = [{header: enrichment_data.get(header, "N/A")} for header in headers] + readable_output = tableToMarkdown( + f"Enrichment Data for {value}", + rows, + headers=headers, + removeNull=True + ) return CommandResults( - outputs_prefix='SilentPush.EnrichmentData', - outputs_key_field='resource', - outputs=enrichment_data, + outputs_prefix="SilentPush.Enrichment", + outputs_key_field="value", + outputs={"value": value, **enrichment_data}, readable_output=readable_output, - raw_response=raw_response + raw_response=response ) -def list_ip_information_command(client: Any, args: Dict[str, Any]) -> CommandResults: + + + +def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: ips = args.get("ips") if not ips: raise ValueError("The 'ips' parameter is required.") - + response = client.list_ip_information(ips=ips) + outputs = response.get("response", {}).get("ip2asn", []) - outputs = response.get("response", {}) readable_output = tableToMarkdown( "IP Information", outputs, @@ -562,6 +640,7 @@ def list_ip_information_command(client: Any, args: Dict[str, Any]) -> CommandRes raw_response=response ) + def get_asn_reputation_command(self, args: dict) -> CommandResults: asn = args.get("asn") explain = argToBoolean(args.get("explain", False)) @@ -585,11 +664,189 @@ def get_asn_reputation_command(self, args: dict) -> CommandResults: except Exception as e: raise DemistoException(f"Error retrieving ASN reputation data: {str(e)}") -def get_asn_takedown_reputation_command(client, args): - return get_asn_takedown_reputation(client, args) +def get_asn_takedown_reputation_command(client: Client, args): + return client.get_asn_takedown_reputation(args) -def main(): +def get_ipv4_reputation_command(client: Client, args: dict) -> CommandResults: + ipv4 = args.get('ipv4') + if not ipv4: + raise ValueError("The 'ipv4' parameter is required.") + + explain = argToBoolean(args.get('explain', False)) + limit = arg_to_number(args.get('limit', None)) + + try: + raw_response = client.get_ipv4_reputation(ipv4, explain, limit) + except Exception as e: + return CommandResults( + readable_output=f"Error: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error' + ) + + readable_output = tableToMarkdown(f"IPv4 Reputation for {ipv4}", raw_response.get('response', {})) + + return CommandResults( + outputs_prefix='SilentPush.IPv4Reputation', + outputs_key_field='ipv4', + outputs={ + 'ipv4': ipv4, + 'details': raw_response.get('response', {}) + }, + readable_output=readable_output, + raw_response=raw_response + ) +def get_job_status_command(client: Client, args: dict) -> CommandResults: + job_id = args.get('job_id') + max_wait = arg_to_number(args.get('max_wait')) + result_type = args.get('result_type') + + if not job_id: + raise DemistoException("job_id is a required parameter") + + try: + raw_response = client.get_job_status(job_id, max_wait, result_type) + + job_status = raw_response.get('response', {}) + + headers = list(job_status.keys()) + rows = [job_status] + + readable_output = tableToMarkdown( + f"Job Status for Job ID: {job_id}", + rows, + headers=headers, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.JobStatus', + outputs_key_field='job_id', + outputs={'job_id': job_id, **job_status}, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error retrieving job status: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + +def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResults: + nameserver = args.get('nameserver') + explain = argToBoolean(args.get('explain', False)) + limit = arg_to_number(args.get('limit')) + + if not nameserver: + raise DemistoException("nameserver is a required parameter") + + try: + raw_response = client.get_nameserver_reputation(nameserver, explain, limit) + + ns_reputation = raw_response.get('response', {}) + readable_output = tableToMarkdown( + f"Nameserver Reputation for {nameserver}", + ns_reputation, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.NameserverReputation', + outputs_key_field='nameserver', + outputs={'nameserver': nameserver, **ns_reputation}, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error retrieving nameserver reputation: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + +def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: + subnet = args.get('subnet') + explain = argToBoolean(args.get('explain', False)) + limit = arg_to_number(args.get('limit')) + + if not subnet: + raise DemistoException("subnet is a required parameter") + + try: + raw_response = client.get_subnet_reputation(subnet, explain, limit) + + subnet_reputation = raw_response.get('response', {}) + + readable_output = tableToMarkdown( + f"Subnet Reputation for {subnet}", + subnet_reputation, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.SubnetReputation', + outputs_key_field='subnet', + outputs={'subnet': subnet, **subnet_reputation}, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error retrieving subnet reputation: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + +def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: + domain = args.get('domain') + + if not domain: + raise DemistoException("domain is a required parameter") + + try: + raw_response = client.get_asns_for_domain(domain) + + asns = raw_response.get('response', {}).get('asns', []) + + readable_output = tableToMarkdown( + f"ASNs for Domain: {domain}", + [{'ASN': asn} for asn in asns], + headers=['ASN'] + ) + + return CommandResults( + outputs_prefix='SilentPush.DomainASNs', + outputs_key_field='domain', + outputs={'domain': domain, 'asns': asns}, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error retrieving domain ASNs: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + + + +def main(): + try: params = demisto.params() api_key = params.get('credentials', {}).get('password') @@ -615,7 +872,12 @@ def main(): 'silentpush-get-enrichment-data' : get_enrichment_data_command, 'silentpush-list-ip-information' : list_ip_information_command, 'silentpush-get-asn-reputation' : get_asn_reputation_command, - 'silentpush-get-asn-takedown-reputation': get_asn_takedown_reputation_command + 'silentpush-get-asn-takedown-reputation': get_asn_takedown_reputation_command, + 'silentpush-get-ipv4-reputation': get_ipv4_reputation_command, + 'silentpush-get-job-status': get_job_status_command, + 'silentpush-get-nameserver-reputation': get_nameserver_reputation_command, + 'silentpush-get-subnet-reputation': get_subnet_reputation_command, + 'silentpush-get-asns-for-domain': get_asns_for_domain_command } if command in command_handlers: @@ -630,8 +892,8 @@ def main(): except Exception as e: demisto.error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') return_error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') - - + + ''' ENTRY POINT ''' if __name__ in ('__main__', '__builtin__', 'builtins'): From d9cebc932f451938d00215e93e816c1c026bd1b9 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 24 Jan 2025 11:39:50 +0530 Subject: [PATCH 14/19] updated get_enrichment_data --- .../Integrations/SilentPush/SilentPush.py | 76 +++++-------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index b127e14b94c4..de2f8a9c617d 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -156,24 +156,29 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str return response def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] = False, scan_data: Optional[bool] = False) -> dict: - parameters = ["resource", "value"] - endpoint = "/api/v1/merge-api/explore/enrich/{{resource}}/{{value}}" + endpoint = f"explore/enrich/{resource}/{value}" - for param in parameters: - endpoint = endpoint.replace(f"{{{param}}}", str(locals().get(param))) - query_params = {} if explain: query_params["explain"] = int(explain) if scan_data: query_params["scan_data"] = int(scan_data) - + response = self._http_request( method="GET", url_suffix=endpoint, params=query_params ) - return response + + if resource in ["ip", "ipv4", "ipv6"]: + ip2asn_data = response.get("response", {}).get("ip2asn", []) + if isinstance(ip2asn_data, list) and ip2asn_data: + return ip2asn_data[0] + else: + return {} + else: + return response.get("response", {}).get("domaininfo", {}) + def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) -> Dict[str, Any]: @@ -244,17 +249,6 @@ def get_ipv4_reputation(self, ipv4, explain=False, limit=None): return response def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_type: Optional[str] = None) -> Dict[str, Any]: - """ - Retrieve status or results of a specified job. - - Args: - job_id (str): ID of the job to retrieve status for. - max_wait (Optional[int]): Number of seconds to wait for results (0-25 seconds). - result_type (Optional[str]): Type of result to include (Status, Include Metadata, Exclude Metadata). - - Returns: - Dict[str, Any]: Job status or result information. - """ url_suffix = f"explore/job/{job_id}" params = {} @@ -278,17 +272,6 @@ def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_typ return response def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: - """ - Retrieve reputation information for a nameserver. - - Args: - nameserver (str): Name of the nameserver to check. - explain (Optional[bool]): Whether to include calculation details. - limit (Optional[int]): Maximum number of reputation history entries to retrieve. - - Returns: - Dict[str, Any]: Nameserver reputation data. - """ url_suffix = f"explore/nsreputation/history/nameserver/{nameserver}" params = {} @@ -306,17 +289,6 @@ def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = F return response def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: - """ - Retrieve reputation information for a subnet. - - Args: - subnet (str): IPv4 subnet to check (e.g., 192.1.0.0/24). - explain (Optional[bool]): Whether to include calculation details. - limit (Optional[int]): Maximum number of reputation history entries to retrieve. - - Returns: - Dict[str, Any]: Subnet reputation data. - """ url_suffix = f"explore/ipreputation/history/subnet/{subnet}" params = {} @@ -334,15 +306,6 @@ def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, li return response def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: - """ - Retrieve ASNs associated with a domain's A records. - - Args: - domain (str): Domain name to search for ASNs. - - Returns: - Dict[str, Any]: ASNs seen for the domain. - """ url_suffix = f"explore/padns/lookup/domain/asns/{domain}" response = self._http_request( @@ -583,10 +546,7 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: if not resource or not value: raise ValueError("Both 'resource' and 'value' arguments are required.") - response = client.get_enrichment_data(resource=resource, value=value, explain=explain, scan_data=scan_data) - - error_path = "response.ip2asn" if resource == "ip" else "response.domaininfo" - enrichment_data = response.get(error_path, {}) + enrichment_data = client.get_enrichment_data(resource, value, explain, scan_data) if not enrichment_data: return CommandResults( @@ -594,15 +554,12 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: outputs_prefix="SilentPush.Enrichment", outputs_key_field="value", outputs={"value": value, "data": {}}, - raw_response=response + raw_response=enrichment_data ) - headers = list(enrichment_data.keys()) - rows = [{header: enrichment_data.get(header, "N/A")} for header in headers] readable_output = tableToMarkdown( f"Enrichment Data for {value}", - rows, - headers=headers, + enrichment_data, removeNull=True ) @@ -611,11 +568,12 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: outputs_key_field="value", outputs={"value": value, **enrichment_data}, readable_output=readable_output, - raw_response=response + raw_response=enrichment_data ) + def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: ips = args.get("ips") From 2cf5dcd716311a539ebb44d0abee95c563467552 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 24 Jan 2025 12:41:16 +0530 Subject: [PATCH 15/19] fixed bugs --- .../Integrations/SilentPush/SilentPush.py | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index de2f8a9c617d..4c639d70b4fb 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -61,11 +61,15 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: ) if response.status_code not in {200, 201}: raise DemistoException(f'Error in API call [{response.status_code}] - {response.text}') - return response.json() + try: + return response.json() + except ValueError: + raise DemistoException(f"API response is not JSON: {response.text}") except Exception as e: raise DemistoException(f'Error in API call: {str(e)}') + def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: if len(domains) > 100: raise DemistoException("Maximum of 100 domains can be submitted in a single request.") @@ -203,7 +207,7 @@ def get_asn_takedown_reputation(self, args): if not asn: raise ValueError('The "asn" argument is required.') - endpoint = f'explore/takedownreputation/history/asn/{asn}' + endpoint = f'explore/takedownreputation/asn/{asn}' params = {} if explain: @@ -217,11 +221,12 @@ def get_asn_takedown_reputation(self, args): params=params ) - outputs = response.get('response', {}) + outputs = response.get('response', {}).get('takedown_reputation', {}) + readable_output = tableToMarkdown( f'Takedown Reputation for ASN {asn}', - outputs, - headers=['asn', 'score', 'details', 'timestamp'] + [outputs], + headers=['asn', 'asname', 'asn_allocation_date', 'asn_allocation_date', 'asn_takedown_reputation'] ) return CommandResults( @@ -233,7 +238,7 @@ def get_asn_takedown_reputation(self, args): ) def get_ipv4_reputation(self, ipv4, explain=False, limit=None): - url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" + url_suffix = f"explore/ipreputation/ipv4/{ipv4}" params = {} if explain: params['explain'] = 'true' @@ -391,20 +396,21 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com 'date_min': args.get('date_min'), 'date_max': args.get('date_max'), 'prefer': args.get('prefer'), - 'max_wait': arg_to_number(args.get('max_wait')), - 'with_metadata': argToBoolean(args.get('with_metadata')), - 'skip': arg_to_number(args.get('skip')), - 'limit': arg_to_number(args.get('limit')) + 'max_wait': arg_to_number(args.get('max_wait')) if args.get('max_wait') else None, + 'with_metadata': argToBoolean(args.get('with_metadata')) if args.get('with_metadata') else None, + 'skip': arg_to_number(args.get('skip')) if args.get('skip') else None, + 'limit': arg_to_number(args.get('limit')) if args.get('limit') else None } + params = {k: v for k, v in params.items() if v is not None} raw_response = client.get_domain_certificates(domain, **params) - if isinstance(raw_response, list): - certificates = raw_response - metadata = {} - else: - certificates = raw_response.get('response', {}).get('domain_certificates', []) - metadata = raw_response.get('response', {}).get('metadata', {}) + demisto.debug(f"Raw response for get_domain_certificates: {raw_response}") + if not isinstance(raw_response, dict): + raise DemistoException(f"Unexpected response format: {raw_response}") + + certificates = raw_response.get('response', {}).get('domain_certificates', []) + metadata = raw_response.get('response', {}).get('metadata', {}) if not certificates: return CommandResults( @@ -419,10 +425,12 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com for cert in certificates: cert_info = { 'Issuer': cert.get('issuer', 'N/A'), - 'Issued On': cert.get('issued_on', 'N/A'), - 'Expires On': cert.get('expires_on', 'N/A'), - 'Common Name': cert.get('common_name', 'N/A'), - 'Subject Alternative Names': ', '.join(cert.get('subject_alt_names', [])), + 'Issued On': cert.get('not_before', 'N/A'), + 'Expires On': cert.get('not_after', 'N/A'), + 'Common Name': cert.get('subject', {}).get('CN', 'N/A'), + 'Subject Alternative Names': ', '.join(cert.get('domains', [])), + 'Serial Number': cert.get('serial_number', 'N/A'), + 'Fingerprint SHA256': cert.get('fingerprint_sha256', 'N/A'), } markdown.append(tableToMarkdown('Certificate Information', [cert_info])) @@ -435,6 +443,8 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com ) + + def search_domains_command(client: Client, args: dict) -> CommandResults: query = args.get('query') start_date = args.get('start_date') @@ -643,14 +653,19 @@ def get_ipv4_reputation_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - readable_output = tableToMarkdown(f"IPv4 Reputation for {ipv4}", raw_response.get('response', {})) + ip_reputation = raw_response.get('response', {}).get('ip_reputation', []) + + if not ip_reputation: + readable_output = f"No reputation information found for IPv4: {ipv4}" + else: + readable_output = tableToMarkdown(f"IPv4 Reputation for {ipv4}", ip_reputation) return CommandResults( outputs_prefix='SilentPush.IPv4Reputation', - outputs_key_field='ipv4', + outputs_key_field='ip', outputs={ - 'ipv4': ipv4, - 'details': raw_response.get('response', {}) + 'ip': ipv4, + 'reputation_history': ip_reputation }, readable_output=readable_output, raw_response=raw_response From fa9910dd93a36f04208be73785cba2b3ed5c88e8 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 24 Jan 2025 15:37:12 +0530 Subject: [PATCH 16/19] updated commands --- .../Integrations/SilentPush/SilentPush.py | 126 ++++++++++++------ 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 4c639d70b4fb..8c86961bca79 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -10,7 +10,7 @@ import requests import urllib3 -from typing import Any, Optional, Dict +from typing import Any, Optional, Dict, List urllib3.disable_warnings() @@ -183,6 +183,23 @@ def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] else: return response.get("response", {}).get("domaininfo", {}) + def list_ip_information(self, ips: List[str]) -> Dict: + """ + Retrieve information for multiple IP addresses. + + Args: + ips (List[str]): List of IPv4 or IPv6 addresses to fetch information for. + + Returns: + Dict: API response containing IP information. + """ + if len(ips) > 100: + raise DemistoException("Maximum of 100 IPs can be submitted in a single request.") + + ip_data = {'ips': ips} + bulk_ip_response = self._http_request('POST', 'explore/bulk/ip2asn/ipv4', data=ip_data) + + return bulk_ip_response def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) -> Dict[str, Any]: @@ -238,7 +255,7 @@ def get_asn_takedown_reputation(self, args): ) def get_ipv4_reputation(self, ipv4, explain=False, limit=None): - url_suffix = f"explore/ipreputation/ipv4/{ipv4}" + url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" params = {} if explain: params['explain'] = 'true' @@ -276,11 +293,10 @@ def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_typ return response - def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: + def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = None, limit: Optional[int] = None) -> Dict[str, Any]: url_suffix = f"explore/nsreputation/history/nameserver/{nameserver}" - params = {} - if explain: + if explain is not None: params['explain'] = str(explain).lower() if limit is not None: params['limit'] = limit @@ -290,8 +306,9 @@ def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = F url_suffix=url_suffix, params=params ) - return response + + def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: url_suffix = f"explore/ipreputation/history/subnet/{subnet}" @@ -581,22 +598,29 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: raw_response=enrichment_data ) - - - def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: - ips = args.get("ips") + ips = argToList(args.get("ips", "")) if not ips: raise ValueError("The 'ips' parameter is required.") response = client.list_ip_information(ips=ips) outputs = response.get("response", {}).get("ip2asn", []) + if not outputs: + return CommandResults( + readable_output=f"No information found for IPs: {', '.join(ips)}", + outputs_prefix="SilentPush.IPInformation", + outputs_key_field="ip", + outputs=[], + raw_response=response + ) + + detailed_outputs = outputs + readable_output = tableToMarkdown( - "IP Information", - outputs, - headers=["ip", "asn", "organization", "country", "risk_score"], + "Comprehensive IP Information", + detailed_outputs, removeNull=True ) @@ -653,7 +677,7 @@ def get_ipv4_reputation_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error' ) - ip_reputation = raw_response.get('response', {}).get('ip_reputation', []) + ip_reputation = raw_response.get('response', {}).get('ip_reputation_history', []) if not ip_reputation: readable_output = f"No reputation information found for IPv4: {ipv4}" @@ -711,28 +735,31 @@ def get_job_status_command(client: Client, args: dict) -> CommandResults: outputs={'error': str(e)} ) -def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResults: +def get_nameserver_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: nameserver = args.get('nameserver') + if not nameserver: + raise DemistoException("The 'nameserver' parameter is required.") + explain = argToBoolean(args.get('explain', False)) limit = arg_to_number(args.get('limit')) - if not nameserver: - raise DemistoException("nameserver is a required parameter") - try: raw_response = client.get_nameserver_reputation(nameserver, explain, limit) - - ns_reputation = raw_response.get('response', {}) - readable_output = tableToMarkdown( - f"Nameserver Reputation for {nameserver}", - ns_reputation, - removeNull=True - ) + reputation_history = raw_response.get('response', {}).get('ns_server_reputation_history', []) + + if not reputation_history: + readable_output = f"No reputation history found for nameserver: {nameserver}" + else: + readable_output = tableToMarkdown( + f"Nameserver Reputation for {nameserver}", + reputation_history, + removeNull=True + ) return CommandResults( outputs_prefix='SilentPush.NameserverReputation', outputs_key_field='nameserver', - outputs={'nameserver': nameserver, **ns_reputation}, + outputs={'nameserver': nameserver, 'reputation_history': reputation_history}, readable_output=readable_output, raw_response=raw_response ) @@ -745,7 +772,9 @@ def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResu outputs_key_field='error', outputs={'error': str(e)} ) - + + + def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: subnet = args.get('subnet') explain = argToBoolean(args.get('explain', False)) @@ -757,18 +786,21 @@ def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: try: raw_response = client.get_subnet_reputation(subnet, explain, limit) - subnet_reputation = raw_response.get('response', {}) + subnet_reputation = raw_response.get('response', {}).get('subnet_reputation_history', []) - readable_output = tableToMarkdown( - f"Subnet Reputation for {subnet}", - subnet_reputation, - removeNull=True - ) + if not subnet_reputation: + readable_output = f"No reputation history found for subnet: {subnet}" + else: + readable_output = tableToMarkdown( + f"Subnet Reputation for {subnet}", + subnet_reputation, + removeNull=True + ) return CommandResults( outputs_prefix='SilentPush.SubnetReputation', outputs_key_field='subnet', - outputs={'subnet': subnet, **subnet_reputation}, + outputs={'subnet': subnet, 'reputation_history': subnet_reputation}, readable_output=readable_output, raw_response=raw_response ) @@ -791,18 +823,30 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: try: raw_response = client.get_asns_for_domain(domain) - asns = raw_response.get('response', {}).get('asns', []) + records = raw_response.get('response', {}).get('records', []) - readable_output = tableToMarkdown( - f"ASNs for Domain: {domain}", - [{'ASN': asn} for asn in asns], - headers=['ASN'] - ) + if not records or 'domain_asns' not in records[0]: + readable_output = f"No ASNs found for domain: {domain}" + asns = [] + else: + domain_asns = records[0]['domain_asns'] + + asns = [{'ASN': asn, 'Description': description} + for asn, description in domain_asns.items()] + + readable_output = tableToMarkdown( + f"ASNs for Domain: {domain}", + asns, + headers=['ASN', 'Description'] + ) return CommandResults( outputs_prefix='SilentPush.DomainASNs', outputs_key_field='domain', - outputs={'domain': domain, 'asns': asns}, + outputs={ + 'domain': domain, + 'asns': asns + }, readable_output=readable_output, raw_response=raw_response ) From eb1158090f3f09b743a843ea21fe9fa5450f73f0 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Fri, 24 Jan 2025 18:01:18 +0530 Subject: [PATCH 17/19] updated the code --- .../Integrations/SilentPush/SilentPush.py | 182 ++++++++++-------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 8c86961bca79..2ac8da84313e 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -59,14 +59,25 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: params=params, json=data ) - if response.status_code not in {200, 201}: - raise DemistoException(f'Error in API call [{response.status_code}] - {response.text}') - try: - return response.json() - except ValueError: - raise DemistoException(f"API response is not JSON: {response.text}") + if response.headers.get('Content-Type', '').startswith('application/json'): + return response.json() + else: + return response.text except Exception as e: raise DemistoException(f'Error in API call: {str(e)}') + + def parse_subject(self,subject: Any) -> Dict[str, Any]: + if isinstance(subject, dict): + return subject + elif isinstance(subject, str): + try: + return json.loads(subject.replace("'", '"')) + except (json.JSONDecodeError, TypeError): + demisto.debug(f"Failed to parse subject: {subject}") + return {'CN': subject} + else: + return {'CN': 'N/A'} + @@ -105,14 +116,20 @@ def list_domain_information(self, domains: List[str], fetch_risk_score: Optional return {'domains': combined_results} - def get_domain_certificates(self, domain: str, **kwargs) -> dict: + def get_domain_certificates(self, domain: str, **kwargs) -> Dict[str, Any]: url_suffix = f"explore/domain/certificates/{domain}" params = {k: v for k, v in kwargs.items() if v is not None} - response = self._http_request('GET', url_suffix, params=params) - demisto.debug(f"Raw response for get_domain_certificates: {response}") + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + demisto.debug(f"Raw response from API: {response}") return response + + def search_domains(self, query: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, risk_score_min: Optional[int] = None, risk_score_max: Optional[int] = None, limit: int = 100, domain_regex: Optional[str] = None, name_server: Optional[str] = None, asnum: Optional[int] = None, asname: Optional[str] = None, min_ip_diversity: Optional[int] = None, registrar: Optional[str] = None, min_asn_diversity: Optional[int] = None, certificate_issuer: Optional[str] = None, whois_date_after: Optional[str] = None, skip: Optional[int] = None) -> dict: url_suffix = 'explore/domain/search' params = {k: v for k, v in { @@ -161,13 +178,13 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] = False, scan_data: Optional[bool] = False) -> dict: endpoint = f"explore/enrich/{resource}/{value}" - + query_params = {} if explain: query_params["explain"] = int(explain) if scan_data: query_params["scan_data"] = int(scan_data) - + response = self._http_request( method="GET", url_suffix=endpoint, @@ -176,10 +193,10 @@ def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] if resource in ["ip", "ipv4", "ipv6"]: ip2asn_data = response.get("response", {}).get("ip2asn", []) - if isinstance(ip2asn_data, list) and ip2asn_data: - return ip2asn_data[0] + if isinstance(ip2asn_data, list) and ip2asn_data: + return ip2asn_data[0] else: - return {} + return {} else: return response.get("response", {}).get("domaininfo", {}) @@ -239,7 +256,7 @@ def get_asn_takedown_reputation(self, args): ) outputs = response.get('response', {}).get('takedown_reputation', {}) - + readable_output = tableToMarkdown( f'Takedown Reputation for ASN {asn}', [outputs], @@ -255,7 +272,7 @@ def get_asn_takedown_reputation(self, args): ) def get_ipv4_reputation(self, ipv4, explain=False, limit=None): - url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" + url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" params = {} if explain: params['explain'] = 'true' @@ -269,16 +286,16 @@ def get_ipv4_reputation(self, ipv4, explain=False, limit=None): ) return response - + def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_type: Optional[str] = None) -> Dict[str, Any]: url_suffix = f"explore/job/{job_id}" - + params = {} if max_wait is not None: if not isinstance(max_wait, int) or max_wait < 0 or max_wait > 25: raise ValueError("max_wait must be an integer between 0 and 25") params['max_wait'] = max_wait - + if result_type: valid_result_types = ['Status', 'Include Metadata', 'Exclude Metadata'] if result_type not in valid_result_types: @@ -292,7 +309,7 @@ def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_typ ) return response - + def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = None, limit: Optional[int] = None) -> Dict[str, Any]: url_suffix = f"explore/nsreputation/history/nameserver/{nameserver}" params = {} @@ -308,11 +325,11 @@ def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = N ) return response - - + + def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: url_suffix = f"explore/ipreputation/history/subnet/{subnet}" - + params = {} if explain: params['explain'] = str(explain).lower() @@ -326,7 +343,7 @@ def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, li ) return response - + def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: url_suffix = f"explore/padns/lookup/domain/asns/{domain}" @@ -405,8 +422,7 @@ def list_domain_information_command(client: Client, args: Dict[str, Any]) -> Com def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> CommandResults: domain = args.get('domain') if not domain: - raise DemistoException('Domain argument is required.') - + raise DemistoException("The 'domain' parameter is required.") params = { 'domain_regex': args.get('domain_regex'), 'certificate_issuer': args.get('certificate_issuer'), @@ -414,50 +430,52 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com 'date_max': args.get('date_max'), 'prefer': args.get('prefer'), 'max_wait': arg_to_number(args.get('max_wait')) if args.get('max_wait') else None, - 'with_metadata': argToBoolean(args.get('with_metadata')) if args.get('with_metadata') else None, + 'with_metadata': argToBoolean(args.get('with_metadata')) if 'with_metadata' in args else None, 'skip': arg_to_number(args.get('skip')) if args.get('skip') else None, 'limit': arg_to_number(args.get('limit')) if args.get('limit') else None } params = {k: v for k, v in params.items() if v is not None} - raw_response = client.get_domain_certificates(domain, **params) - - demisto.debug(f"Raw response for get_domain_certificates: {raw_response}") - if not isinstance(raw_response, dict): - raise DemistoException(f"Unexpected response format: {raw_response}") + try: + raw_response = client.get_domain_certificates(domain, **params) + + certificates = raw_response.get('response', {}).get('domain_certificates', []) + metadata = raw_response.get('response', {}).get('metadata', {}) + + if not certificates: + return CommandResults( + readable_output=f"No certificates found for domain: {domain}", + outputs_prefix='SilentPush.Certificate', + outputs_key_field='domain', + outputs={'domain': domain, 'certificates': [], 'metadata': metadata}, + raw_response=raw_response + ) - certificates = raw_response.get('response', {}).get('domain_certificates', []) - metadata = raw_response.get('response', {}).get('metadata', {}) + markdown = [f"# SSL/TLS Certificate Information for Domain: {domain}\n"] + for cert in certificates: + subject = client.parse_subject(cert.get('subject', {})) + cert_info = { + 'Issuer': cert.get('issuer', 'N/A'), + 'Issued On': cert.get('not_before', 'N/A'), + 'Expires On': cert.get('not_after', 'N/A'), + 'Common Name': subject.get('CN', 'N/A'), + 'Subject Alternative Names': ', '.join(cert.get('domains', [])), + 'Serial Number': cert.get('serial_number', 'N/A'), + 'Fingerprint SHA256': cert.get('fingerprint_sha256', 'N/A'), + } + markdown.append(tableToMarkdown('Certificate Information', [cert_info])) - if not certificates: return CommandResults( - readable_output=f"No certificates found for domain: {domain}", outputs_prefix='SilentPush.Certificate', outputs_key_field='domain', - outputs={'domain': domain, 'certificates': [], 'metadata': metadata}, + outputs={'domain': domain, 'certificates': certificates, 'metadata': metadata}, + readable_output='\n'.join(markdown), raw_response=raw_response ) - markdown = [f"# SSL/TLS Certificate Information for Domain: {domain}\n"] - for cert in certificates: - cert_info = { - 'Issuer': cert.get('issuer', 'N/A'), - 'Issued On': cert.get('not_before', 'N/A'), - 'Expires On': cert.get('not_after', 'N/A'), - 'Common Name': cert.get('subject', {}).get('CN', 'N/A'), - 'Subject Alternative Names': ', '.join(cert.get('domains', [])), - 'Serial Number': cert.get('serial_number', 'N/A'), - 'Fingerprint SHA256': cert.get('fingerprint_sha256', 'N/A'), - } - markdown.append(tableToMarkdown('Certificate Information', [cert_info])) + except Exception as e: + raise DemistoException(f"Error retrieving certificates for domain '{domain}': {str(e)}") - return CommandResults( - outputs_prefix='SilentPush.Certificate', - outputs_key_field='domain', - outputs={'domain': domain, 'certificates': certificates, 'metadata': metadata}, - readable_output='\n'.join(markdown), - raw_response=raw_response - ) @@ -562,8 +580,8 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: readable_output=readable_output, raw_response=raw_response ) - - + + def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: resource = args.get("resource") value = args.get("value") @@ -598,12 +616,12 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: raw_response=enrichment_data ) - + def list_ip_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: ips = argToList(args.get("ips", "")) if not ips: raise ValueError("The 'ips' parameter is required.") - + response = client.list_ip_information(ips=ips) outputs = response.get("response", {}).get("ip2asn", []) @@ -616,7 +634,7 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command raw_response=response ) - detailed_outputs = outputs + detailed_outputs = outputs readable_output = tableToMarkdown( "Comprehensive IP Information", @@ -694,7 +712,7 @@ def get_ipv4_reputation_command(client: Client, args: dict) -> CommandResults: readable_output=readable_output, raw_response=raw_response ) - + def get_job_status_command(client: Client, args: dict) -> CommandResults: job_id = args.get('job_id') max_wait = arg_to_number(args.get('max_wait')) @@ -705,16 +723,16 @@ def get_job_status_command(client: Client, args: dict) -> CommandResults: try: raw_response = client.get_job_status(job_id, max_wait, result_type) - + job_status = raw_response.get('response', {}) - + headers = list(job_status.keys()) rows = [job_status] - + readable_output = tableToMarkdown( - f"Job Status for Job ID: {job_id}", - rows, - headers=headers, + f"Job Status for Job ID: {job_id}", + rows, + headers=headers, removeNull=True ) @@ -734,7 +752,7 @@ def get_job_status_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error', outputs={'error': str(e)} ) - + def get_nameserver_reputation_command(client: Client, args: Dict[str, Any]) -> CommandResults: nameserver = args.get('nameserver') if not nameserver: @@ -785,15 +803,15 @@ def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: try: raw_response = client.get_subnet_reputation(subnet, explain, limit) - + subnet_reputation = raw_response.get('response', {}).get('subnet_reputation_history', []) - + if not subnet_reputation: readable_output = f"No reputation history found for subnet: {subnet}" else: readable_output = tableToMarkdown( - f"Subnet Reputation for {subnet}", - subnet_reputation, + f"Subnet Reputation for {subnet}", + subnet_reputation, removeNull=True ) @@ -813,7 +831,7 @@ def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error', outputs={'error': str(e)} ) - + def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: domain = args.get('domain') @@ -822,21 +840,21 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: try: raw_response = client.get_asns_for_domain(domain) - + records = raw_response.get('response', {}).get('records', []) - + if not records or 'domain_asns' not in records[0]: readable_output = f"No ASNs found for domain: {domain}" asns = [] else: domain_asns = records[0]['domain_asns'] - - asns = [{'ASN': asn, 'Description': description} + + asns = [{'ASN': asn, 'Description': description} for asn, description in domain_asns.items()] - + readable_output = tableToMarkdown( - f"ASNs for Domain: {domain}", - asns, + f"ASNs for Domain: {domain}", + asns, headers=['ASN', 'Description'] ) @@ -844,7 +862,7 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: outputs_prefix='SilentPush.DomainASNs', outputs_key_field='domain', outputs={ - 'domain': domain, + 'domain': domain, 'asns': asns }, readable_output=readable_output, From 484b72bf798e1b4eaaa602b704c68da5b5d77a85 Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Sun, 26 Jan 2025 13:03:12 +0530 Subject: [PATCH 18/19] resolved github pr comments --- .../Integrations/SilentPush/SilentPush.py | 297 +++++++++++++++--- 1 file changed, 259 insertions(+), 38 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 2ac8da84313e..2ccfdb119f46 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -1,4 +1,7 @@ - +import demistomock as demisto # noqa: F401 +from CommonServerPython import * # noqa: F401 +import ipaddress +import re """Base Integration for Cortex XSOAR (aka Demisto) This is an integration to interact with the SilentPush API and provide functionality within XSOAR. @@ -49,6 +52,21 @@ def __init__(self, base_url: str, api_key: str, verify: bool = True, proxy: bool } def _http_request(self, method: str, url_suffix: str, params: dict = None, data: dict = None) -> Any: + """ + Perform an HTTP request to the SilentPush API. + + Args: + method (str): The HTTP method to use (e.g., 'GET', 'POST'). + url_suffix (str): The endpoint suffix to append to the base URL. + params (dict, optional): Query parameters to include in the request. Defaults to None. + data (dict, optional): JSON data to send in the request body. Defaults to None. + + Returns: + Any: The JSON response from the API or text response if not JSON. + + Raises: + DemistoException: If there's an error during the API call. + """ full_url = f'{self.base_url}{url_suffix}' try: response = requests.request( @@ -67,6 +85,16 @@ def _http_request(self, method: str, url_suffix: str, params: dict = None, data: raise DemistoException(f'Error in API call: {str(e)}') def parse_subject(self,subject: Any) -> Dict[str, Any]: + """ + Parse the subject of a certificate or domain record. + + Args: + subject (Any): The subject to parse, which can be a dictionary, string, or other type. + + Returns: + Dict[str, Any]: A dictionary representation of the subject, + with a fallback to {'CN': subject} or {'CN': 'N/A'} if parsing fails. + """ if isinstance(subject, dict): return subject elif isinstance(subject, str): @@ -77,11 +105,63 @@ def parse_subject(self,subject: Any) -> Dict[str, Any]: return {'CN': subject} else: return {'CN': 'N/A'} + + def validate_ip_address(ip: str, allow_ipv6: bool = True) -> bool: + """ + Validate an IP address. + Args: + ip (str): IP address to validate. + allow_ipv6 (bool, optional): Whether to allow IPv6 addresses. Defaults to True. + + Returns: + bool: True if valid IP address, False otherwise. + """ + try: + ip = ip.strip() + ip_obj = ipaddress.ip_address(ip) + + return not (not allow_ipv6 and ip_obj.version == 6) + except ValueError: + return False + + def validate_ip_inputs(ips: list[str], allow_ipv6: bool = True) -> list[str]: + """ + Validate a list of IP addresses. + + Args: + ips (list[str]): List of IP addresses to validate. + allow_ipv6 (bool, optional): Whether to allow IPv6 addresses. Defaults to True. + + Returns: + list[str]: List of valid IP addresses. + Raises: + DemistoException: If no valid IP addresses are found. + """ + valid_ips = [ip.strip() for ip in ips if validate_ip_address(ip, allow_ipv6)] + + if not valid_ips: + raise DemistoException(f"No valid {'IPv4 and IPv6' if allow_ipv6 else 'IPv4'} addresses found.") + + return valid_ips def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict: + """ + Retrieve comprehensive information for multiple domains. + + Args: + domains (List[str]): List of domains to fetch information for. + fetch_risk_score (bool, optional): Whether to retrieve risk scores. Defaults to False. + fetch_whois_info (bool, optional): Whether to retrieve WHOIS information. Defaults to False. + + Returns: + Dict: A dictionary containing domain information, including optional risk scores and WHOIS data. + + Raises: + DemistoException: If more than 100 domains are submitted. + """ if len(domains) > 100: raise DemistoException("Maximum of 100 domains can be submitted in a single request.") @@ -117,6 +197,16 @@ def list_domain_information(self, domains: List[str], fetch_risk_score: Optional return {'domains': combined_results} def get_domain_certificates(self, domain: str, **kwargs) -> Dict[str, Any]: + """ + Retrieve SSL/TLS certificates for a given domain. + + Args: + domain (str): The domain to retrieve certificates for. + **kwargs: Additional optional parameters for filtering certificates. + + Returns: + Dict[str, Any]: A dictionary containing domain certificate information. + """ url_suffix = f"explore/domain/certificates/{domain}" params = {k: v for k, v in kwargs.items() if v is not None} response = self._http_request( @@ -131,6 +221,30 @@ def get_domain_certificates(self, domain: str, **kwargs) -> Dict[str, Any]: def search_domains(self, query: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, risk_score_min: Optional[int] = None, risk_score_max: Optional[int] = None, limit: int = 100, domain_regex: Optional[str] = None, name_server: Optional[str] = None, asnum: Optional[int] = None, asname: Optional[str] = None, min_ip_diversity: Optional[int] = None, registrar: Optional[str] = None, min_asn_diversity: Optional[int] = None, certificate_issuer: Optional[str] = None, whois_date_after: Optional[str] = None, skip: Optional[int] = None) -> dict: + """ + Search for domains based on various filtering criteria. + + Args: + query (str, optional): Domain search query. + start_date (str, optional): Start date for domain search. + end_date (str, optional): End date for domain search. + risk_score_min (int, optional): Minimum risk score filter. + risk_score_max (int, optional): Maximum risk score filter. + limit (int, optional): Maximum number of results to return. Defaults to 100. + domain_regex (str, optional): Regular expression to filter domains. + name_server (str, optional): Name server filter. + asnum (int, optional): Autonomous System Number filter. + asname (str, optional): Autonomous System Name filter. + min_ip_diversity (int, optional): Minimum IP diversity filter. + registrar (str, optional): Domain registrar filter. + min_asn_diversity (int, optional): Minimum ASN diversity filter. + certificate_issuer (str, optional): Certificate issuer filter. + whois_date_after (str, optional): WHOIS date filter. + skip (int, optional): Number of results to skip. + + Returns: + dict: Search results matching the specified criteria. + """ url_suffix = 'explore/domain/search' params = {k: v for k, v in { 'domain': query, @@ -154,6 +268,19 @@ def search_domains(self, query: Optional[str] = None, start_date: Optional[str] return response def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str = 'live', match: str = 'self', as_of: Optional[str] = None) -> dict: + """ + Retrieve infrastructure tags for specified domains. + + Args: + domains (list): List of domains to fetch infrastructure tags for. + cluster (bool, optional): Whether to cluster tags. Defaults to False. + mode (str, optional): Tag retrieval mode. Defaults to 'live'. + match (str, optional): Matching criteria. Defaults to 'self'. + as_of (str, optional): Specific timestamp for tag retrieval. Defaults to None. + + Returns: + dict: Infrastructure tags and optional tag clusters for the domains. + """ url = 'explore/bulk/domain/infratags' payload = { 'domains': domains @@ -177,13 +304,23 @@ def list_domain_infratags(self, domains: list, cluster: bool = False, mode: str return response def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] = False, scan_data: Optional[bool] = False) -> dict: + """ + Retrieve enrichment data for a specific resource. + + Args: + resource (str): Type of resource (e.g., 'ip', 'domain'). + value (str): The specific value to enrich. + explain (bool, optional): Whether to include detailed explanations. Defaults to False. + scan_data (bool, optional): Whether to include scan data. Defaults to False. + + Returns: + dict: Enrichment data for the specified resource. + """ endpoint = f"explore/enrich/{resource}/{value}" query_params = {} - if explain: - query_params["explain"] = int(explain) - if scan_data: - query_params["scan_data"] = int(scan_data) + query_params["explain"] = int(explain) if explain else query_params.get("explain", 0) + query_params["scan_data"] = int(scan_data) if scan_data else query_params.get("scan_data", 0) response = self._http_request( method="GET", @@ -193,10 +330,7 @@ def get_enrichment_data(self, resource: str, value: str, explain: Optional[bool] if resource in ["ip", "ipv4", "ipv6"]: ip2asn_data = response.get("response", {}).get("ip2asn", []) - if isinstance(ip2asn_data, list) and ip2asn_data: - return ip2asn_data[0] - else: - return {} + return ip2asn_data[0] if isinstance(ip2asn_data, list) and ip2asn_data else {} else: return response.get("response", {}).get("domaininfo", {}) @@ -220,6 +354,17 @@ def list_ip_information(self, ips: List[str]) -> Dict: def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) -> Dict[str, Any]: + """ + Retrieve reputation history for a specific Autonomous System Number (ASN). + + Args: + asn (int): The Autonomous System Number to query. + explain (bool, optional): Whether to include detailed explanations. Defaults to False. + limit (int, optional): Maximum number of results to return. Defaults to None. + + Returns: + Dict[str, Any]: ASN reputation history information. + """ url_suffix = f"explore/ipreputation/history/asn/{asn}" query_params = {} if explain: @@ -234,6 +379,18 @@ def get_asn_reputation(self, asn: int, explain: bool = False, limit: int = None) return response def get_asn_takedown_reputation(self, args): + """ + Retrieve takedown reputation for a specific Autonomous System Number (ASN). + + Args: + args (dict): Arguments containing ASN, optional explain flag, and optional result limit. + + Returns: + dict: Takedown reputation information for the specified ASN. + + Raises: + ValueError: If ASN is not provided. + """ asn = args.get('asn') explain = argToBoolean(args.get('explain', 'false')) limit = args.get('limit') @@ -255,23 +412,20 @@ def get_asn_takedown_reputation(self, args): params=params ) - outputs = response.get('response', {}).get('takedown_reputation', {}) + return response.get('response', {}).get('takedown_reputation', {}) - readable_output = tableToMarkdown( - f'Takedown Reputation for ASN {asn}', - [outputs], - headers=['asn', 'asname', 'asn_allocation_date', 'asn_allocation_date', 'asn_takedown_reputation'] - ) + def get_ipv4_reputation(self, ipv4, explain=False, limit=None): + """ + Retrieve reputation history for a specific IPv4 address. - return CommandResults( - outputs_prefix='SilentPush.TakedownReputation', - outputs_key_field='asn', - outputs=outputs, - readable_output=readable_output, - raw_response=response - ) + Args: + ipv4 (str): The IPv4 address to query. + explain (bool, optional): Whether to include detailed explanations. Defaults to False. + limit (int, optional): Maximum number of results to return. Defaults to None. - def get_ipv4_reputation(self, ipv4, explain=False, limit=None): + Returns: + dict: IPv4 reputation history information. + """ url_suffix = f"explore/ipreputation/history/ipv4/{ipv4}" params = {} if explain: @@ -288,6 +442,20 @@ def get_ipv4_reputation(self, ipv4, explain=False, limit=None): return response def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_type: Optional[str] = None) -> Dict[str, Any]: + """ + Retrieve the status of a specific job. + + Args: + job_id (str): The unique identifier of the job to check. + max_wait (int, optional): Maximum wait time in seconds. Must be between 0 and 25. Defaults to None. + result_type (str, optional): Type of result to retrieve. Defaults to None. + + Returns: + Dict[str, Any]: Job status information. + + Raises: + ValueError: If max_wait is invalid or result_type is not in allowed values. + """ url_suffix = f"explore/job/{job_id}" params = {} @@ -311,6 +479,17 @@ def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_typ return response def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = None, limit: Optional[int] = None) -> Dict[str, Any]: + """ + Retrieve reputation history for a specific nameserver. + + Args: + nameserver (str): The nameserver to query. + explain (bool, optional): Whether to include detailed explanations. Defaults to None. + limit (int, optional): Maximum number of results to return. Defaults to None. + + Returns: + Dict[str, Any]: Nameserver reputation history information. + """ url_suffix = f"explore/nsreputation/history/nameserver/{nameserver}" params = {} if explain is not None: @@ -325,9 +504,18 @@ def get_nameserver_reputation(self, nameserver: str, explain: Optional[bool] = N ) return response + def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: + """ + Retrieve reputation history for a specific subnet. + Args: + subnet (str): The subnet to query. + explain (bool, optional): Whether to include detailed explanations. Defaults to False. + limit (int, optional): Maximum number of results to return. Defaults to None. - def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, limit: Optional[int] = None) -> Dict[str, Any]: + Returns: + Dict[str, Any]: Subnet reputation history information. + """ url_suffix = f"explore/ipreputation/history/subnet/{subnet}" params = {} @@ -345,6 +533,15 @@ def get_subnet_reputation(self, subnet: str, explain: Optional[bool] = False, li return response def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: + """ + Retrieve Autonomous System Numbers (ASNs) associated with a domain. + + Args: + domain (str): The domain to retrieve ASNs for. + + Returns: + Dict[str, Any]: Domain ASN information. + """ url_suffix = f"explore/padns/lookup/domain/asns/{domain}" response = self._http_request( @@ -476,10 +673,6 @@ def get_domain_certificates_command(client: Client, args: Dict[str, Any]) -> Com except Exception as e: raise DemistoException(f"Error retrieving certificates for domain '{domain}': {str(e)}") - - - - def search_domains_command(client: Client, args: dict) -> CommandResults: query = args.get('query') start_date = args.get('start_date') @@ -591,6 +784,10 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: if not resource or not value: raise ValueError("Both 'resource' and 'value' arguments are required.") + # Simplified IP validation with a single if statement + if resource in ["ip", "ipv4", "ipv6"] and not validate_ip_address(value, allow_ipv6=(resource != "ipv4")): + raise DemistoException(f"Invalid {resource.upper()} address: {value}") + enrichment_data = client.get_enrichment_data(resource, value, explain, scan_data) if not enrichment_data: @@ -622,23 +819,31 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command if not ips: raise ValueError("The 'ips' parameter is required.") - response = client.list_ip_information(ips=ips) + try: + valid_ips = validate_ip_inputs(ips) + except DemistoException as e: + return CommandResults( + readable_output=str(e), + outputs_prefix="SilentPush.Error", + outputs_key_field="error", + outputs={"error": str(e)} + ) + + response = client.list_ip_information(valid_ips) outputs = response.get("response", {}).get("ip2asn", []) if not outputs: return CommandResults( - readable_output=f"No information found for IPs: {', '.join(ips)}", + readable_output=f"No information found for IPs: {', '.join(valid_ips)}", outputs_prefix="SilentPush.IPInformation", outputs_key_field="ip", outputs=[], raw_response=response ) - detailed_outputs = outputs - readable_output = tableToMarkdown( "Comprehensive IP Information", - detailed_outputs, + outputs, removeNull=True ) @@ -650,7 +855,6 @@ def list_ip_information_command(client: Client, args: Dict[str, Any]) -> Command raw_response=response ) - def get_asn_reputation_command(self, args: dict) -> CommandResults: asn = args.get("asn") explain = argToBoolean(args.get("explain", False)) @@ -675,13 +879,32 @@ def get_asn_reputation_command(self, args: dict) -> CommandResults: raise DemistoException(f"Error retrieving ASN reputation data: {str(e)}") def get_asn_takedown_reputation_command(client: Client, args): - return client.get_asn_takedown_reputation(args) + + takedown_reputation = client.get_asn_takedown_reputation(args) + asn = args.get('asn') + + readable_output = tableToMarkdown( + f'Takedown Reputation for ASN {asn}', + [takedown_reputation], + headers=['asn', 'asname', 'asn_allocation_date', 'asn_takedown_reputation'] + ) + + return CommandResults( + outputs_prefix='SilentPush.TakedownReputation', + outputs_key_field='asn', + outputs=takedown_reputation, + readable_output=readable_output, + raw_response=takedown_reputation + ) def get_ipv4_reputation_command(client: Client, args: dict) -> CommandResults: ipv4 = args.get('ipv4') if not ipv4: raise ValueError("The 'ipv4' parameter is required.") + if not validate_ip_address(ipv4, allow_ipv6=False): + raise DemistoException(f"Invalid IPv4 address: {ipv4}") + explain = argToBoolean(args.get('explain', False)) limit = arg_to_number(args.get('limit', None)) @@ -791,15 +1014,13 @@ def get_nameserver_reputation_command(client: Client, args: Dict[str, Any]) -> C outputs={'error': str(e)} ) - - def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: subnet = args.get('subnet') explain = argToBoolean(args.get('explain', False)) limit = arg_to_number(args.get('limit')) if not subnet: - raise DemistoException("subnet is a required parameter") + raise DemistoException("Subnet is a required parameter.") try: raw_response = client.get_subnet_reputation(subnet, explain, limit) From b7b830cba97884b9a97566d5a2ee4977583c352e Mon Sep 17 00:00:00 2001 From: Karan Deep Date: Sun, 26 Jan 2025 13:24:57 +0530 Subject: [PATCH 19/19] added 3 more commands that neds to test in xsoar --- .../Integrations/SilentPush/SilentPush.py | 308 +++++++++++++++++- 1 file changed, 306 insertions(+), 2 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 2ccfdb119f46..4ac0f32686dd 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -550,7 +550,78 @@ def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: ) return response + + def forward_padns_lookup(self, qtype: str, qname: str, **kwargs) -> Dict[str, Any]: + """ + Perform a forward PADNS lookup using various filtering parameters. + + Args: + qtype (str): Type of DNS record. + qname (str): The DNS record name to lookup. + **kwargs: Optional parameters for filtering and pagination. + + Returns: + Dict[str, Any]: PADNS lookup results. + """ + url_suffix = f"explore/padns/lookup/query/{qtype}/{qname}" + + params = {k: v for k, v in kwargs.items() if v is not None} + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def reverse_padns_lookup(self, qtype: str, qname: str, **kwargs) -> Dict[str, Any]: + """ + Perform a reverse PADNS lookup using various filtering parameters. + Args: + qtype (str): Type of DNS record. + qname (str): The DNS record name to lookup. + **kwargs: Optional parameters for filtering and pagination. + + Returns: + Dict[str, Any]: Reverse PADNS lookup results. + """ + url_suffix = f"explore/padns/lookup/answer/{qtype}/{qname}" + + params = {k: v for k, v in kwargs.items() if v is not None} + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response + + def density_lookup(self, qtype: str, query: str, **kwargs) -> Dict[str, Any]: + """ + Perform a density lookup based on various query types and parameters. + + Args: + qtype (str): Query type (nssrv, mxsrv, nshash, mxhash, ipv4, ipv6, asn, chv) + query (str): Value to lookup + **kwargs: Optional parameters for filtering and scoping + + Returns: + Dict[str, Any]: Density lookup results + """ + url_suffix = f"explore/padns/lookup/density/{qtype}/{query}" + + params = {k: v for k, v in kwargs.items() if v is not None} + + response = self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) + + return response def test_module(client: Client) -> str: @@ -784,7 +855,6 @@ def get_enrichment_data_command(client: Client, args: dict) -> CommandResults: if not resource or not value: raise ValueError("Both 'resource' and 'value' arguments are required.") - # Simplified IP validation with a single if statement if resource in ["ip", "ipv4", "ipv6"] and not validate_ip_address(value, allow_ipv6=(resource != "ipv4")): raise DemistoException(f"Invalid {resource.upper()} address: {value}") @@ -1098,7 +1168,238 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: outputs_key_field='error', outputs={'error': str(e)} ) + +def forward_padns_lookup_command(client: Client, args: dict) -> CommandResults: + """ + Command function to perform forward PADNS lookup. + + Args: + client (Client): SilentPush API client. + args (dict): Command arguments. + + Returns: + CommandResults: Formatted results of the PADNS lookup. + """ + qtype = args.get('qtype') + qname = args.get('qname') + + if not qtype or not qname: + raise DemistoException("Both 'qtype' and 'qname' are required parameters.") + + netmask = args.get('netmask') + subdomains = argToBoolean(args.get('subdomains')) if 'subdomains' in args else None + regex = args.get('regex') + match = args.get('match') + first_seen_after = args.get('first_seen_after') + first_seen_before = args.get('first_seen_before') + last_seen_after = args.get('last_seen_after') + last_seen_before = args.get('last_seen_before') + as_of = args.get('as_of') + sort = args.get('sort') + output_format = args.get('output_format') + prefer = args.get('prefer') + with_metadata = argToBoolean(args.get('with_metadata')) if 'with_metadata' in args else None + max_wait = arg_to_number(args.get('max_wait')) + skip = arg_to_number(args.get('skip')) + limit = arg_to_number(args.get('limit')) + + try: + raw_response = client.forward_padns_lookup( + qtype=qtype, + qname=qname, + netmask=netmask, + subdomains=subdomains, + regex=regex, + match=match, + first_seen_after=first_seen_after, + first_seen_before=first_seen_before, + last_seen_after=last_seen_after, + last_seen_before=last_seen_before, + as_of=as_of, + sort=sort, + output_format=output_format, + prefer=prefer, + with_metadata=with_metadata, + max_wait=max_wait, + skip=skip, + limit=limit + ) + + records = raw_response.get('response', {}).get('records', []) + + if not records: + readable_output = f"No records found for {qtype} {qname}" + else: + readable_output = tableToMarkdown( + f"PADNS Lookup Results for {qtype} {qname}", + records, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.PADNSLookup', + outputs_key_field='qname', + outputs={ + 'qtype': qtype, + 'qname': qname, + 'records': records + }, + readable_output=readable_output, + raw_response=raw_response + ) + except Exception as e: + return CommandResults( + readable_output=f"Error performing PADNS lookup: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + +def reverse_padns_lookup_command(client: Client, args: dict) -> CommandResults: + """ + Command function to perform reverse PADNS lookup. + + Args: + client (Client): SilentPush API client. + args (dict): Command arguments. + + Returns: + CommandResults: Formatted results of the reverse PADNS lookup. + """ + qtype = args.get('qtype') + qname = args.get('qname') + + if not qtype or not qname: + raise DemistoException("Both 'qtype' and 'qname' are required parameters.") + + netmask = args.get('netmask') + subdomains = argToBoolean(args.get('subdomains')) if 'subdomains' in args else None + regex = args.get('regex') + first_seen_after = args.get('first_seen_after') + first_seen_before = args.get('first_seen_before') + last_seen_after = args.get('last_seen_after') + last_seen_before = args.get('last_seen_before') + as_of = args.get('as_of') + sort = args.get('sort') + output_format = args.get('output_format') + prefer = args.get('prefer') + with_metadata = argToBoolean(args.get('with_metadata')) if 'with_metadata' in args else None + max_wait = arg_to_number(args.get('max_wait')) + skip = arg_to_number(args.get('skip')) + limit = arg_to_number(args.get('limit')) + + try: + raw_response = client.reverse_padns_lookup( + qtype=qtype, + qname=qname, + netmask=netmask, + subdomains=subdomains, + regex=regex, + first_seen_after=first_seen_after, + first_seen_before=first_seen_before, + last_seen_after=last_seen_after, + last_seen_before=last_seen_before, + as_of=as_of, + sort=sort, + output_format=output_format, + prefer=prefer, + with_metadata=with_metadata, + max_wait=max_wait, + skip=skip, + limit=limit + ) + + records = raw_response.get('response', {}).get('records', []) + + if not records: + readable_output = f"No records found for {qtype} {qname}" + else: + readable_output = tableToMarkdown( + f"Reverse PADNS Lookup Results for {qtype} {qname}", + records, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.ReversePADNSLookup', + outputs_key_field='qname', + outputs={ + 'qtype': qtype, + 'qname': qname, + 'records': records + }, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error performing reverse PADNS lookup: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) + +def density_lookup_command(client: Client, args: dict) -> CommandResults: + """ + Command function to perform density lookup. + + Args: + client (Client): SilentPush API client. + args (dict): Command arguments. + + Returns: + CommandResults: Formatted results of the density lookup. + """ + qtype = args.get('qtype') + query = args.get('query') + + if not qtype or not query: + raise DemistoException("Both 'qtype' and 'query' are required parameters.") + + scope = args.get('scope') + + try: + raw_response = client.density_lookup( + qtype=qtype, + query=query, + scope=scope + ) + + records = raw_response.get('response', {}).get('records', []) + + if not records: + readable_output = f"No density records found for {qtype} {query}" + else: + readable_output = tableToMarkdown( + f"Density Lookup Results for {qtype} {query}", + records, + removeNull=True + ) + + return CommandResults( + outputs_prefix='SilentPush.DensityLookup', + outputs_key_field='query', + outputs={ + 'qtype': qtype, + 'query': query, + 'records': records + }, + readable_output=readable_output, + raw_response=raw_response + ) + + except Exception as e: + return CommandResults( + readable_output=f"Error performing density lookup: {str(e)}", + raw_response={}, + outputs_prefix='SilentPush.Error', + outputs_key_field='error', + outputs={'error': str(e)} + ) def main(): @@ -1133,7 +1434,10 @@ def main(): 'silentpush-get-job-status': get_job_status_command, 'silentpush-get-nameserver-reputation': get_nameserver_reputation_command, 'silentpush-get-subnet-reputation': get_subnet_reputation_command, - 'silentpush-get-asns-for-domain': get_asns_for_domain_command + 'silentpush-get-asns-for-domain': get_asns_for_domain_command, + 'silentpush-forward-padns-lookup': forward_padns_lookup_command, + 'silentpush-reverse-padns-lookup': reverse_padns_lookup_command, + 'silentpush-density-lookup': density_lookup_command } if command in command_handlers: