From b6a451c30c4a9ff9e45137f6db31e0237aebb536 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 18:52:02 +0530 Subject: [PATCH 1/7] Adding 3 silent push commands. --- .../Integrations/SilentPush/SilentPush.py | 535 ++++++++++++++---- .../Integrations/SilentPush/SilentPush.yml | 252 +++++---- .../SilentPush/SilentPush_test.py | 14 +- .../test_data/baseintegration-dummy.json | 3 - Packs/SilentPush/pack_metadata.json | 9 +- 5 files changed, 570 insertions(+), 243 deletions(-) delete mode 100644 Packs/SilentPush/Integrations/SilentPush/test_data/baseintegration-dummy.json diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index fc56db7e38c2..6051c393096b 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -1,36 +1,139 @@ -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. - -Developer Documentation: https://xsoar.pan.dev/docs/welcome -Code Conventions: https://xsoar.pan.dev/docs/integrations/code-conventions -Linting: https://xsoar.pan.dev/docs/integrations/linting -""" - -from CommonServerUserPython import * # noqa +import demistomock as demisto +from CommonServerPython import * +from CommonServerUserPython import * +import enum +import json import urllib3 -from typing import Any +import dateparser +import traceback +from typing import Any, Dict, List, Optional, Union # 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 +DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + +# API ENDPOINTS +JOB_STATUS = "explore/job" +NAMESERVER_REPUTATION = "explore/nsreputation/nameserver" +SUBNET_REPUTATION = "explore/ipreputation/history/subnet" + +''' COMMANDS INPUTS ''' + +JOB_STATUS_INPUTS = [ + InputArgument(name='job_id', # option 1 + description='ID of the job returned by Silent Push actions.', + required=True), + InputArgument(name='max_wait', + description='Number of seconds to wait for results (0-25 seconds).'), + InputArgument(name='result_type', + description='Type of result to include in the response.') + ] +NAMESERVER_REPUTATION_INPUTS = [ + InputArgument(name='nameserver', + description='Nameserver name for which information needs to be retrieved', + required=True), + InputArgument(name='explain', + description='Show the information used to calculate the reputation score'), + InputArgument(name='limit', + description='The maximum number of reputation history to retrieve') + ] +SUBNET_REPUTATION_INPUTS = [ + InputArgument( + name='subnet', + description='IPv4 subnet for which reputation information needs to be retrieved.', + required=True + ), + InputArgument( + name='explain', + description='Show the detailed information used to calculate the reputation score.' + ), + InputArgument( + name='limit', + description='Maximum number of reputation history entries to retrieve.' + ) + ] + + +''' COMMANDS OUTPUTS ''' + +JOB_STATUS_OUTPUTS = [ + OutputArgument(name='get', output_type=str, description='URL to retrieve the job status.'), + OutputArgument(name='job_id', output_type=str, description='Unique identifier for the job.'), + OutputArgument(name='status', output_type=str, description='Current status of the job.') + ] + +NAMESERVER_REPUTATION_OUTPUTS = [ + OutputArgument(name='date', output_type=int, description='Date of the reputation history entry (in YYYYMMDD format).'), + OutputArgument(name='ns_server', output_type=str, description='Name of the nameserver associated with the reputation history entry.'), + OutputArgument(name='ns_server_reputation', output_type=int, description='Reputation score of the nameserver on the specified date.'), + OutputArgument(name='ns_server_reputation_explain', output_type=dict, description='Explanation of the reputation score, including domain density and listed domains.'), + OutputArgument(name='ns_server_domain_density', output_type=int, description='Number of domains associated with the nameserver.'), + OutputArgument(name='ns_server_domains_listed', output_type=int, description='Number of domains listed in reputation databases.') + ] +SUBNET_REPUTATION_OUTPUTS = [ + OutputArgument(name='date', output_type=int, description='The date of the subnet reputation record.'), + OutputArgument(name='subnet', output_type=str, description='The subnet associated with the reputation record.'), + OutputArgument(name='subnet_reputation', output_type=int, description='The reputation score of the subnet.'), + OutputArgument(name='ips_in_subnet', output_type=int, description='Total number of IPs in the subnet.'), + OutputArgument(name='ips_num_active', output_type=int, description='Number of active IPs in the subnet.'), + OutputArgument(name='ips_num_listed', output_type=int, description='Number of listed IPs in the subnet.') + ] + + +metadata_collector = YMLMetadataCollector( + integration_name="SilentPush", + description=( + "The Silent Push Platform uses first-party data and a proprietary scanning engine to enrich global DNS data " + "with risk and reputation scoring, giving security teams the ability to join the dots across the entire IPv4 and IPv6 range, " + "and identify adversary infrastructure before an attack is launched. The content pack integrates with the Silent Push system " + "to gain insights into domain/IP information, reputations, enrichment, and infratag-related details. It also provides " + "functionality to live-scan URLs and take screenshots of them. Additionally, it allows fetching future attack feeds " + "from the Silent Push system." + ), + display="SilentPush", + category="Data Enrichment & Threat Intelligence", + docker_image="demisto/python3:3.11.10.116949", + is_fetch=False, + long_running=False, + long_running_port=False, + is_runonce=False, + integration_subtype="python3", + integration_type="python", + fromversion="5.0.0", + conf=[ + ConfKey( + name="url", + display="Base URL", + required=True, + default_value="https://api.silentpush.com" + ), + ConfKey( + name="credentials", + display="API Key", + required=False, + key_type=ParameterTypes.TEXT_AREA_ENCRYPTED, + ), + ConfKey( + name="insecure", + display="Trust any certificate (not secure)", + required=False, + key_type=ParameterTypes.BOOLEAN + ), + ConfKey( + name="proxy", + display="Use system proxy settings", + required=False, + key_type=ParameterTypes.BOOLEAN + ) + ] +) -''' CLIENT CLASS ''' +''' CLIENT CLASS ''' class Client(BaseClient): """Client class to interact with the SilentPush API @@ -44,7 +147,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. @@ -59,32 +162,25 @@ 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: """ - 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. - + Perform an HTTP request to the SilentPush API. + 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. - + 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. - + Any: The JSON response from the API or text response if not JSON. + Raises: - DemistoException: If there is an error in the API response. + DemistoException: If there's an error during the API call. """ - 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}') + base_url = demisto.params().get('url', 'https://api.silentpush.com') if url_suffix.startswith("/api/v2/") else self.base_url + full_url = f'{base_url}{url_suffix}' try: response = requests.request( @@ -95,104 +191,301 @@ 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}') - return response.json() + if response.headers.get('Content-Type', '').startswith('application/json'): + return response.json() + else: + return response.text 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, domain: str) -> dict: + + def get_job_status(self, job_id: str, max_wait: Optional[int] = None, result_type: Optional[str] = None) -> Dict[str, Any]: """ - Fetches domain information such as WHOIS data, domain age, and risk scores. + 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"{JOB_STATUS}/{job_id}" + params = {} + + if max_wait is not None: + if not (0 <= max_wait <= 25): + raise ValueError("max_wait must be an integer between 0 and 25") + params['max_wait'] = max_wait + + valid_result_types = {'Status', 'Include Metadata', 'Exclude Metadata'} + if result_type and result_type not in valid_result_types: + raise ValueError(f"result_type must be one of {valid_result_types}") + if result_type: + params['result_type'] = result_type + + return self._http_request(method="GET", url_suffix=url_suffix, params=params) + + def get_nameserver_reputation(self, nameserver: str, explain: bool = False, limit: int = None): + """ + Retrieve historical reputation data for the specified nameserver. + Args: - domain (str): The domain to fetch information for. - + nameserver (str): The nameserver for which the reputation data is to be fetched. + explain (bool): Whether to include detailed calculation explanations. + limit (int): Maximum number of reputation entries to return. + Returns: - dict: A dictionary containing domain information fetched from the API. + dict: Reputation history for the given nameserver. """ - demisto.debug(f'Fetching domain information for domain: {domain}') - url_suffix = f'explore/domain/domaininfo/{domain}' - return self._http_request('GET', url_suffix) + url_suffix = f"{NAMESERVER_REPUTATION}/{nameserver}" -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. + params = filter_none_values({'explain': explain, 'limit': limit}) + + response = self._http_request(method="GET", url_suffix=url_suffix, params=params) + + # Return the reputation history, or an empty list if not found + return response.get('response', {}).get('ns_server_reputation', []) + + def get_subnet_reputation(self, subnet: str, explain: 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. + + Returns: + Dict[str, Any]: Subnet reputation history information. + """ + url_suffix = f"/{subnet}" + + params = { + "explain": str(explain).lower() if explain else None, + "limit": limit + } + + params = filter_none_values(params) + + return self._http_request(method="GET", url_suffix=url_suffix, params=params) + + +''' HELPER FUNCTIONS ''' +def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: + """Removes None values from a dictionary.""" + return {k: v for k, v in params.items() if v is not None} + + +''' COMMAND FUNCTIONS ''' + + +def test_module(client: Client, first_fetch_time: int) -> str: + """Tests API connectivity and authentication' + + Returning 'ok' indicates that the integration works like it is supposed to. + Connection to the service is successful. + Raises exceptions if something goes wrong. + + :type client: ``Client`` + :param Client: SilentPush client to use + + :type name: ``str`` + :param name: name to append to the 'Hello' string + + :return: 'ok' if test passed, anything else will fail the test. + :rtype: ``str`` """ - demisto.debug('Running test module...') + + # INTEGRATION DEVELOPER TIP + # Client class should raise the exceptions, but if the test fails + # the exception text is printed to the Cortex XSOAR UI. + # If you have some specific errors you want to capture (i.e., auth failure) + # you should catch the exception here and return a string with a more + # readable output (for example return 'Authentication Error, API Key + # invalid'). + # Cortex XSOAR will print everything you return that is different than 'ok' as + # an error. try: - client.list_domain_information('silentpush.com') - demisto.debug('Test module completed successfully') + resp = client.get_job_status("job_id", "max_wait", "result_type") + if resp.get("status_code") != 200: + return f"Connection failed :- {resp.get('errors')}" 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 - - -''' COMMAND FUNCTIONS ''' -def list_domain_information_command(client: Client, args: dict) -> CommandResults: +@metadata_collector.command( + command_name="silentpush-get-job-status", + inputs_list=JOB_STATUS_INPUTS, + outputs_prefix="SilentPush.JobStatus", + outputs_list=JOB_STATUS_OUTPUTS, + description="This command retrieve status of running job or results from completed job.", +) +def get_job_status_command(client: Client, args: dict) -> 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. - + Retrieves the status of a job based on the provided job ID and other optional parameters. + 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 that interacts with the service to fetch job status. + args (dict): A dictionary of arguments, which should include: + - 'job_id' (str): The unique identifier of the job for which status is being retrieved. + - 'max_wait' (Optional[int]): The maximum wait time in seconds (default is None). + - 'result_type' (Optional[str]): Type of result to retrieve. Valid options are 'Status', + 'Include Metadata', or 'Exclude Metadata' (default is None). + Returns: - CommandResults: The command results containing readable output and the raw response. + CommandResults: The command results containing: + - 'outputs_prefix' (str): The prefix for the output context. + - 'outputs_key_field' (str): The field used as the key in the outputs. + - 'outputs' (dict): A dictionary with job ID and job status information. + - 'readable_output' (str): A formatted string that represents the job status in a human-readable format. + - 'raw_response' (dict): The raw response received from the service. + + Raises: + DemistoException: If the 'job_id' parameter is missing or if no job status is found for the given job ID. """ - domain = args.get('domain', 'silentpush.com') - demisto.debug(f'Processing domain: {domain}') + job_id = args.get('job_id') + max_wait = arg_to_number(args.get('max_wait')) + result_type = args.get('result_type') - raw_response = client.list_domain_information(domain) - demisto.debug(f'Response from API: {raw_response}') + if not job_id: + raise DemistoException("job_id is a required parameter") - readable_output = tableToMarkdown('Domain Information', raw_response) + raw_response = client.get_job_status(job_id, max_wait, result_type) + job_status = raw_response.get('response', {}) + if not job_status: + raise DemistoException(f"No job status found for Job ID: {job_id}") + + readable_output = tableToMarkdown( + f"Job Status for Job ID: {job_id}", + [job_status], + headers=list(job_status.keys()), + removeNull=True + ) return CommandResults( - outputs_prefix='SilentPush.Domain', - outputs_key_field='domain', - outputs=raw_response, + outputs_prefix='SilentPush.JobStatus', + outputs_key_field='job_id', + outputs={'job_id': job_id, **job_status}, readable_output=readable_output, raw_response=raw_response ) -''' MAIN FUNCTION ''' +@metadata_collector.command( + command_name="silentpush-get-nameserver-reputation", + inputs_list=NAMESERVER_REPUTATION_INPUTS, + outputs_prefix="SilentPush.SubnetReputation", + outputs_list=NAMESERVER_REPUTATION_OUTPUTS, + description="This command retrieve historical reputation data for a specified nameserver, including reputation scores and optional detailed calculation information.", +) +def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResults: + """ + Command handler for retrieving nameserver reputation. + Args: + client (Client): The API client instance. + args (dict): Command arguments. -def main(): + Returns: + CommandResults: The command results containing nameserver reputation data. + """ + nameserver = args.get("nameserver") + explain = argToBoolean(args.get("explain", "false")) + limit = arg_to_number(args.get("limit")) + + if not nameserver: + raise ValueError("Nameserver is required.") + + # Fetch reputation data + reputation_data = client.get_nameserver_reputation(nameserver, explain, limit) + + # Prepare the readable output + if reputation_data: + readable_output = tableToMarkdown( + f"Nameserver Reputation for {nameserver}", + reputation_data, + headers=list(reputation_data[0].keys()), + removeNull=True + ) + else: + readable_output = f"No reputation history found for nameserver: {nameserver}" + + # Return command results + return CommandResults( + outputs_prefix="SilentPush.NameserverReputation", + outputs_key_field="ns_server", + outputs={"nameserver": nameserver, "reputation_data": reputation_data}, + readable_output=readable_output, + raw_response=reputation_data + ) + +@metadata_collector.command( + command_name="silentpush-get-subnet-reputation", + inputs_list=SUBNET_REPUTATION_INPUTS, + outputs_prefix="SilentPush.NameserverReputation", + outputs_list=SUBNET_REPUTATION_OUTPUTS, + description="This command retrieves the reputation history for a specific subnet." +) +def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: + """ + Retrieves the reputation history of a given subnet. + + Args: + client (Client): The API client instance. + args (dict): Command arguments containing: + - subnet (str): The subnet to query. + - explain (bool, optional): Whether to include an explanation. + - limit (int, optional): Limit the number of reputation records. + + Returns: + CommandResults: The command result containing the subnet reputation data. """ - 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. + subnet = args.get('subnet') + if not subnet: + raise DemistoException("Subnet is a required parameter.") + + explain = argToBoolean(args.get('explain', False)) + limit = arg_to_number(args.get('limit')) + + raw_response = client.get_subnet_reputation(subnet, explain, limit) + subnet_reputation = raw_response.get('response', {}).get('subnet_reputation_history', []) + + readable_output = ( + f"No reputation history found for subnet: {subnet}" + if not subnet_reputation + else tableToMarkdown(f"Subnet Reputation for {subnet}", subnet_reputation, removeNull=True) + ) + + return CommandResults( + outputs_prefix='SilentPush.SubnetReputation', + outputs_key_field='subnet', + outputs={'subnet': subnet, 'reputation_history': subnet_reputation}, + readable_output=readable_output, + raw_response=raw_response + ) + + + +''' MAIN FUNCTION ''' + + +def main() -> None: + """main function, parses params and runs command functions + + :return: + :rtype: """ + try: params = demisto.params() api_key = params.get('credentials', {}).get('password') @@ -200,9 +493,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, @@ -210,26 +500,25 @@ def main(): proxy=proxy ) - command = demisto.command() - demisto.debug(f'Command being called is {command}') - - if command == 'test-module': - result = test_module(client) + if demisto.command() == 'test-module': + result = test_module(client, demisto.args()) return_results(result) - - elif command == 'silentpush-list-domain-information': - return_results(list_domain_information_command(client, demisto.args())) - - else: - raise DemistoException(f'Unsupported command: {command}') + + elif demisto.command() == 'silentpush-get-job-status': + return_results(get_job_status_command(client, demisto.args())) + + elif demisto.command() == 'silentpush-get-nameserver-reputation': + return_results(get_nameserver_reputation_command(client, demisto.args())) + + elif demisto.command() == 'silentpush-get-subnet-reputation': + return_results(get_subnet_reputation_command(client, demisto.args())) 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)}') + demisto.error(traceback.format_exc()) # print the traceback + return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}') ''' ENTRY POINT ''' - if __name__ in ('__main__', '__builtin__', 'builtins'): main() diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 098cfeef1186..572f87b3cc80 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -1,109 +1,153 @@ +category: Data Enrichment & Threat Intelligence +description: The Silent Push Platform uses first-party data and a proprietary scanning engine to enrich global DNS data with risk and reputation scoring, giving security teams the ability to join the dots across the entire IPv4 and IPv6 range, and identify adversary infrastructure before an attack is launched. The content pack integrates with the Silent Push system to gain insights into domain/IP information, reputations, enrichment, and infratag-related details. It also provides functionality to live-scan URLs and take screenshots of them. Additionally, it allows fetching future attack feeds from the Silent Push system. commonfields: id: SilentPush version: -1 - name: SilentPush +name: SilentPush +display: SilentPush +configuration: +- display: Base URL + name: url + type: 0 + required: true + defaultvalue: https://api.silentpush.com +- display: API Key + name: credentials + type: 14 + required: false +- display: Trust any certificate (not secure) + name: insecure + type: 8 + required: false +- display: Use system proxy settings + name: proxy + type: 8 + required: false +script: + commands: + - deprecated: false + description: This command retrieve status of running job or results from completed job. + name: silentpush-get-job-status + arguments: + - name: job_id + isArray: false + description: ID of the job returned by Silent Push actions. + required: true + secret: false + default: false + - name: max_wait + isArray: false + description: Number of seconds to wait for results (0-25 seconds). + required: false + secret: false + default: false + - name: result_type + isArray: false + description: Type of result to include in the response. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.JobStatus.get + description: URL to retrieve the job status. + type: String + - contextPath: SilentPush.JobStatus.job_id + description: Unique identifier for the job. + type: String + - contextPath: SilentPush.JobStatus.status + description: Current status of the job. + type: String + - deprecated: false + description: This command retrieve historical reputation data for a specified nameserver, including reputation scores and optional detailed calculation information. + name: silentpush-get-nameserver-reputation + arguments: + - name: nameserver + isArray: false + description: Nameserver name for which information needs to be retrieved + required: true + secret: false + default: false + - name: explain + isArray: false + description: Show the information used to calculate the reputation score + required: false + secret: false + default: false + - name: limit + isArray: false + description: The maximum number of reputation history to retrieve + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.SubnetReputation.date + description: Date of the reputation history entry (in YYYYMMDD format). + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server + description: Name of the nameserver associated with the reputation history entry. + type: String + - contextPath: SilentPush.SubnetReputation.ns_server_reputation + description: Reputation score of the nameserver on the specified date. + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server_reputation_explain + description: Explanation of the reputation score, including domain density and listed domains. + type: Unknown + - contextPath: SilentPush.SubnetReputation.ns_server_domain_density + description: Number of domains associated with the nameserver. + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server_domains_listed + description: Number of domains listed in reputation databases. + type: Number + - deprecated: false + description: This command retrieves the reputation history for a specific subnet. + name: silentpush-get-subnet-reputation + arguments: + - name: subnet + isArray: false + description: IPv4 subnet for which reputation information needs to be retrieved. + required: true + secret: false + default: false + - name: explain + isArray: false + description: Show the detailed information used to calculate the reputation score. + required: false + secret: false + default: false + - name: limit + isArray: false + description: Maximum number of reputation history entries to retrieve. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.NameserverReputation.date + description: The date of the subnet reputation record. + type: Number + - contextPath: SilentPush.NameserverReputation.subnet + description: The subnet associated with the reputation record. + type: String + - contextPath: SilentPush.NameserverReputation.subnet_reputation + description: The reputation score of the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_in_subnet + description: Total number of IPs in the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_num_active + description: Number of active IPs in the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_num_listed + description: Number of listed IPs in the subnet. + type: Number + script: '-' 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. - 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, and risk scores. - -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 - -args: - - id: domain - isArray: false - description: | - The domain to fetch information for. - type: string - -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 - + subtype: python3 + dockerimage: demisto/python3:3.11.10.116949 + feed: false + isfetch: false + runonce: false + longRunning: false + longRunningPort: false +fromversion: 5.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' - -# Optional: Adding the configuration section for any configuration-related parameters -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 diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py index d957c5edf2a0..c42f4e3a8b39 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush_test.py @@ -10,11 +10,13 @@ you are implementing with your integration """ +from demisto_sdk.commands.common.handlers import JSON_Handler + import json def util_load_json(path): - with open(path, encoding='utf-8') as f: + with open(path, encoding="utf-8") as f: return json.loads(f.read()) @@ -29,13 +31,11 @@ def test_baseintegration_dummy(): """ from BaseIntegration import Client, baseintegration_dummy_command - client = Client(base_url='some_mock_url', verify=False) - args = { - 'dummy': 'this is a dummy response' - } + client = Client(base_url="some_mock_url", verify=False) + args = {"dummy": "this is a dummy response", "dummy2": "a dummy value"} response = baseintegration_dummy_command(client, args) - mock_response = util_load_json('test_data/baseintegration-dummy.json') + assert response.outputs == args + - assert response.outputs == mock_response # TODO: ADD HERE unit tests for every command diff --git a/Packs/SilentPush/Integrations/SilentPush/test_data/baseintegration-dummy.json b/Packs/SilentPush/Integrations/SilentPush/test_data/baseintegration-dummy.json deleted file mode 100644 index 37fa47b18cd0..000000000000 --- a/Packs/SilentPush/Integrations/SilentPush/test_data/baseintegration-dummy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dummy": "this is a dummy response" -} \ No newline at end of file diff --git a/Packs/SilentPush/pack_metadata.json b/Packs/SilentPush/pack_metadata.json index b5cd72cb3e81..123de5898f65 100644 --- a/Packs/SilentPush/pack_metadata.json +++ b/Packs/SilentPush/pack_metadata.json @@ -1,18 +1,15 @@ { "name": "Silent Push", - "description": "The Silent Push platform exposes Indicators of Future Attack (IOFA) by applying unique behavioral fingerprints to attacker activity. By searching our dataset, security teams can identify new impending attacks, rather than relying on known IOCs.", + "description": "SilentPush integration for domain and IP intelligence\u001b[C", "support": "partner", "currentVersion": "1.0.0", - "author": "Silent Push", + "author": "Yash", "url": "", "email": "", "categories": [ "Data Enrichment & Threat Intelligence" ], - "tags": [ - "IoC", - "IoFA" - ], + "tags": [], "useCases": [], "keywords": [], "marketplaces": [ From a566dce14fb27a8fe94d63a556905bd0f986bd50 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 19:12:43 +0530 Subject: [PATCH 2/7] silentpush-get-asns-for-domain. --- .../Integrations/SilentPush/SilentPush.py | 89 +++++++++- .../Integrations/SilentPush/SilentPush.yml | 153 ------------------ 2 files changed, 85 insertions(+), 157 deletions(-) delete mode 100644 Packs/SilentPush/Integrations/SilentPush/SilentPush.yml diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 6051c393096b..82fae6d7b0fb 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -20,6 +20,7 @@ JOB_STATUS = "explore/job" NAMESERVER_REPUTATION = "explore/nsreputation/nameserver" SUBNET_REPUTATION = "explore/ipreputation/history/subnet" +ASNS_DOMAIN = "explore/padns/lookup/domain/asns" ''' COMMANDS INPUTS ''' @@ -56,15 +57,21 @@ description='Maximum number of reputation history entries to retrieve.' ) ] +ASNS_DOMAIN_INPUTS = [ + InputArgument(name='domain', # option 1 + description='Domain name to search ASNs for. Retrieves ASNs associated with A records for the specified domain and its subdomains in the last 30 days.', + required=True) + ] + ''' COMMANDS OUTPUTS ''' JOB_STATUS_OUTPUTS = [ - OutputArgument(name='get', output_type=str, description='URL to retrieve the job status.'), - OutputArgument(name='job_id', output_type=str, description='Unique identifier for the job.'), - OutputArgument(name='status', output_type=str, description='Current status of the job.') - ] + OutputArgument(name='get', output_type=str, description='URL to retrieve the job status.'), + OutputArgument(name='job_id', output_type=str, description='Unique identifier for the job.'), + OutputArgument(name='status', output_type=str, description='Current status of the job.') + ] NAMESERVER_REPUTATION_OUTPUTS = [ OutputArgument(name='date', output_type=int, description='Date of the reputation history entry (in YYYYMMDD format).'), @@ -82,6 +89,11 @@ OutputArgument(name='ips_num_active', output_type=int, description='Number of active IPs in the subnet.'), OutputArgument(name='ips_num_listed', output_type=int, description='Number of listed IPs in the subnet.') ] +ASNS_DOMAIN_OUTPUTS = [ + OutputArgument(name='domain', output_type=str, description='The domain name for which ASNs are retrieved.'), + OutputArgument(name='domain_asns', output_type=dict, description='Dictionary of Autonomous System Numbers (ASNs) associated with the domain.') + ] + metadata_collector = YMLMetadataCollector( @@ -276,6 +288,21 @@ def get_subnet_reputation(self, subnet: str, explain: bool = False, limit: Optio return self._http_request(method="GET", url_suffix=url_suffix, params=params) + def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: + """ + Retrieve Autonomous System Numbers (ASNs) associated with the specified domain. + + Args: + domain (str): The domain to retrieve ASNs for. + + Returns: + Dict[str, Any]: A dictionary containing the ASN information for the domain. + """ + url_suffix = f"{ASNS_DOMAIN}/{domain}" + + # Send the request and return the response directly + return self._http_request(method="GET", url_suffix=url_suffix) + ''' HELPER FUNCTIONS ''' def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: @@ -475,6 +502,57 @@ def get_subnet_reputation_command(client: Client, args: dict) -> CommandResults: ) +@metadata_collector.command( + command_name="silentpush-get-asns-for-domain", + inputs_list=ASNS_DOMAIN_INPUTS, + outputs_prefix="SilentPush.DomainASNs", + outputs_list=ASNS_DOMAIN_OUTPUTS, + description="This command retrieves Autonomous System Numbers (ASNs) associated with a domain." +) +def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: + """ + Retrieves Autonomous System Numbers (ASNs) for the specified domain. + + Args: + client (Client): The client object used to interact with the service. + args (dict): Arguments passed to the command, including the domain. + + Returns: + CommandResults: The results containing ASNs for the domain or an error message. + """ + domain = args.get('domain') + + if not domain: + raise DemistoException("Domain is a required parameter.") + + 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} + 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 + }, + readable_output=readable_output, + raw_response=raw_response + ) + ''' MAIN FUNCTION ''' @@ -513,6 +591,9 @@ def main() -> None: elif demisto.command() == 'silentpush-get-subnet-reputation': return_results(get_subnet_reputation_command(client, demisto.args())) + elif demisto.command() == 'silentpush-get-asns-for-domain': + return_results(get_asns_for_domain_command(client, demisto.args())) + except Exception as e: demisto.error(traceback.format_exc()) # print the traceback return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}') diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml deleted file mode 100644 index 572f87b3cc80..000000000000 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ /dev/null @@ -1,153 +0,0 @@ -category: Data Enrichment & Threat Intelligence -description: The Silent Push Platform uses first-party data and a proprietary scanning engine to enrich global DNS data with risk and reputation scoring, giving security teams the ability to join the dots across the entire IPv4 and IPv6 range, and identify adversary infrastructure before an attack is launched. The content pack integrates with the Silent Push system to gain insights into domain/IP information, reputations, enrichment, and infratag-related details. It also provides functionality to live-scan URLs and take screenshots of them. Additionally, it allows fetching future attack feeds from the Silent Push system. -commonfields: - id: SilentPush - version: -1 -name: SilentPush -display: SilentPush -configuration: -- display: Base URL - name: url - type: 0 - required: true - defaultvalue: https://api.silentpush.com -- display: API Key - name: credentials - type: 14 - required: false -- display: Trust any certificate (not secure) - name: insecure - type: 8 - required: false -- display: Use system proxy settings - name: proxy - type: 8 - required: false -script: - commands: - - deprecated: false - description: This command retrieve status of running job or results from completed job. - name: silentpush-get-job-status - arguments: - - name: job_id - isArray: false - description: ID of the job returned by Silent Push actions. - required: true - secret: false - default: false - - name: max_wait - isArray: false - description: Number of seconds to wait for results (0-25 seconds). - required: false - secret: false - default: false - - name: result_type - isArray: false - description: Type of result to include in the response. - required: false - secret: false - default: false - outputs: - - contextPath: SilentPush.JobStatus.get - description: URL to retrieve the job status. - type: String - - contextPath: SilentPush.JobStatus.job_id - description: Unique identifier for the job. - type: String - - contextPath: SilentPush.JobStatus.status - description: Current status of the job. - type: String - - deprecated: false - description: This command retrieve historical reputation data for a specified nameserver, including reputation scores and optional detailed calculation information. - name: silentpush-get-nameserver-reputation - arguments: - - name: nameserver - isArray: false - description: Nameserver name for which information needs to be retrieved - required: true - secret: false - default: false - - name: explain - isArray: false - description: Show the information used to calculate the reputation score - required: false - secret: false - default: false - - name: limit - isArray: false - description: The maximum number of reputation history to retrieve - required: false - secret: false - default: false - outputs: - - contextPath: SilentPush.SubnetReputation.date - description: Date of the reputation history entry (in YYYYMMDD format). - type: Number - - contextPath: SilentPush.SubnetReputation.ns_server - description: Name of the nameserver associated with the reputation history entry. - type: String - - contextPath: SilentPush.SubnetReputation.ns_server_reputation - description: Reputation score of the nameserver on the specified date. - type: Number - - contextPath: SilentPush.SubnetReputation.ns_server_reputation_explain - description: Explanation of the reputation score, including domain density and listed domains. - type: Unknown - - contextPath: SilentPush.SubnetReputation.ns_server_domain_density - description: Number of domains associated with the nameserver. - type: Number - - contextPath: SilentPush.SubnetReputation.ns_server_domains_listed - description: Number of domains listed in reputation databases. - type: Number - - deprecated: false - description: This command retrieves the reputation history for a specific subnet. - name: silentpush-get-subnet-reputation - arguments: - - name: subnet - isArray: false - description: IPv4 subnet for which reputation information needs to be retrieved. - required: true - secret: false - default: false - - name: explain - isArray: false - description: Show the detailed information used to calculate the reputation score. - required: false - secret: false - default: false - - name: limit - isArray: false - description: Maximum number of reputation history entries to retrieve. - required: false - secret: false - default: false - outputs: - - contextPath: SilentPush.NameserverReputation.date - description: The date of the subnet reputation record. - type: Number - - contextPath: SilentPush.NameserverReputation.subnet - description: The subnet associated with the reputation record. - type: String - - contextPath: SilentPush.NameserverReputation.subnet_reputation - description: The reputation score of the subnet. - type: Number - - contextPath: SilentPush.NameserverReputation.ips_in_subnet - description: Total number of IPs in the subnet. - type: Number - - contextPath: SilentPush.NameserverReputation.ips_num_active - description: Number of active IPs in the subnet. - type: Number - - contextPath: SilentPush.NameserverReputation.ips_num_listed - description: Number of listed IPs in the subnet. - type: Number - script: '-' - type: python - subtype: python3 - dockerimage: demisto/python3:3.11.10.116949 - feed: false - isfetch: false - runonce: false - longRunning: false - longRunningPort: false -fromversion: 5.0.0 -tests: -- No tests From c1ac4826318a9280dc9eb6f28a768efef1ed2f25 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 19:27:18 +0530 Subject: [PATCH 3/7] silentpush-density-lookup. --- .../Integrations/SilentPush/SilentPush.py | 87 +++++++- .../Integrations/SilentPush/SilentPush.yml | 199 ++++++++++++++++++ 2 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 Packs/SilentPush/Integrations/SilentPush/SilentPush.yml diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 82fae6d7b0fb..ff6e82acf78d 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -21,6 +21,7 @@ NAMESERVER_REPUTATION = "explore/nsreputation/nameserver" SUBNET_REPUTATION = "explore/ipreputation/history/subnet" ASNS_DOMAIN = "explore/padns/lookup/domain/asns" +DENSITY_LOOKUP = "explore/padns/lookup/density" ''' COMMANDS INPUTS ''' @@ -62,7 +63,16 @@ description='Domain name to search ASNs for. Retrieves ASNs associated with A records for the specified domain and its subdomains in the last 30 days.', required=True) ] - +DENSITY_LOOKUP_INPUTS = [ + InputArgument(name='qtype', + description='Query type.', + required=True), + InputArgument(name='query', + description='Value to query.', + required=True), + InputArgument(name='scope', + description='Match level (optional).') + ] ''' COMMANDS OUTPUTS ''' @@ -93,6 +103,10 @@ OutputArgument(name='domain', output_type=str, description='The domain name for which ASNs are retrieved.'), OutputArgument(name='domain_asns', output_type=dict, description='Dictionary of Autonomous System Numbers (ASNs) associated with the domain.') ] +DENSITY_LOOKUP_OUTPUTS = [ + OutputArgument(name='density', output_type=int, description='The density value associated with the query result.'), + OutputArgument(name='nssrv', output_type=str, description='The name server (NS) for the query result.') +] @@ -303,6 +317,27 @@ def get_asns_for_domain(self, domain: str) -> Dict[str, Any]: # Send the request and return the response directly return self._http_request(method="GET", url_suffix=url_suffix) + def density_lookup(self, qtype: str, query: str, **kwargs) -> Dict[str, Any]: + """ + Perform a density lookup based on various query types and optional parameters. + + Args: + qtype (str): Query type to perform the lookup. Options include: nssrv, mxsrv, nshash, mxhash, ipv4, ipv6, asn, chv. + query (str): The value to look up. + **kwargs: Optional parameters (e.g., filters) for scoping the lookup. + + Returns: + Dict[str, Any]: The results of the density lookup, containing relevant information based on the query. + """ + url_suffix = f"{DENSITY_LOOKUP}/{qtype}/{query}" + + params = filter_none_values(kwargs) + + return self._http_request( + method="GET", + url_suffix=url_suffix, + params=params + ) ''' HELPER FUNCTIONS ''' def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: @@ -553,6 +588,53 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) +@metadata_collector.command( + command_name="silentpush-density-lookup", + inputs_list=DENSITY_LOOKUP_INPUTS, + outputs_prefix="SilentPush.DensityLookup", + outputs_list=DENSITY_LOOKUP_OUTPUTS, + description="This command query granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information." +) +def density_lookup_command(client: Client, args: dict) -> CommandResults: + """ + Command function to perform a density lookup on the SilentPush API. + + Args: + client (Client): SilentPush API client. + args (dict): Command arguments containing 'qtype' and 'query', and optionally 'scope'. + + Returns: + CommandResults: Formatted results of the density lookup, including either the density records or an error message. + """ + 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') + + raw_response = client.density_lookup(qtype=qtype, query=query, scope=scope) + + records = raw_response.get('response', {}).get('records', []) + + readable_output = ( + f"No density records found for {qtype} {query}" + if not records + else 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 + ) + + + + ''' MAIN FUNCTION ''' @@ -593,6 +675,9 @@ def main() -> None: elif demisto.command() == 'silentpush-get-asns-for-domain': return_results(get_asns_for_domain_command(client, demisto.args())) + + elif demisto.command() == 'silentpush-density-lookup': + return_results(density_lookup_command(client, demisto.args())) except Exception as e: demisto.error(traceback.format_exc()) # print the traceback diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml new file mode 100644 index 000000000000..3e839b15f91e --- /dev/null +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -0,0 +1,199 @@ +category: Data Enrichment & Threat Intelligence +description: The Silent Push Platform uses first-party data and a proprietary scanning engine to enrich global DNS data with risk and reputation scoring, giving security teams the ability to join the dots across the entire IPv4 and IPv6 range, and identify adversary infrastructure before an attack is launched. The content pack integrates with the Silent Push system to gain insights into domain/IP information, reputations, enrichment, and infratag-related details. It also provides functionality to live-scan URLs and take screenshots of them. Additionally, it allows fetching future attack feeds from the Silent Push system. +commonfields: + id: SilentPush + version: -1 +name: SilentPush +display: SilentPush +configuration: +- display: Base URL + name: url + type: 0 + required: true + defaultvalue: https://api.silentpush.com +- display: API Key + name: credentials + type: 14 + required: false +- display: Trust any certificate (not secure) + name: insecure + type: 8 + required: false +- display: Use system proxy settings + name: proxy + type: 8 + required: false +script: + commands: + - deprecated: false + description: This command query granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information. + name: silentpush-density-lookup + arguments: + - name: qtype + isArray: false + description: Query type. + required: true + secret: false + default: false + - name: query + isArray: false + description: Value to query. + required: true + secret: false + default: false + - name: scope + isArray: false + description: Match level (optional). + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.DensityLookup.density + description: The density value associated with the query result. + type: Number + - contextPath: SilentPush.DensityLookup.nssrv + description: The name server (NS) for the query result. + type: String + - deprecated: false + description: This command retrieves Autonomous System Numbers (ASNs) associated with a domain. + name: silentpush-get-asns-for-domain + arguments: + - name: domain + isArray: false + description: Domain name to search ASNs for. Retrieves ASNs associated with A records for the specified domain and its subdomains in the last 30 days. + required: true + secret: false + default: false + outputs: + - contextPath: SilentPush.DomainASNs.domain + description: The domain name for which ASNs are retrieved. + type: String + - contextPath: SilentPush.DomainASNs.domain_asns + description: Dictionary of Autonomous System Numbers (ASNs) associated with the domain. + type: Unknown + - deprecated: false + description: This command retrieve status of running job or results from completed job. + name: silentpush-get-job-status + arguments: + - name: job_id + isArray: false + description: ID of the job returned by Silent Push actions. + required: true + secret: false + default: false + - name: max_wait + isArray: false + description: Number of seconds to wait for results (0-25 seconds). + required: false + secret: false + default: false + - name: result_type + isArray: false + description: Type of result to include in the response. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.JobStatus.get + description: URL to retrieve the job status. + type: String + - contextPath: SilentPush.JobStatus.job_id + description: Unique identifier for the job. + type: String + - contextPath: SilentPush.JobStatus.status + description: Current status of the job. + type: String + - deprecated: false + description: This command retrieve historical reputation data for a specified nameserver, including reputation scores and optional detailed calculation information. + name: silentpush-get-nameserver-reputation + arguments: + - name: nameserver + isArray: false + description: Nameserver name for which information needs to be retrieved + required: true + secret: false + default: false + - name: explain + isArray: false + description: Show the information used to calculate the reputation score + required: false + secret: false + default: false + - name: limit + isArray: false + description: The maximum number of reputation history to retrieve + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.SubnetReputation.date + description: Date of the reputation history entry (in YYYYMMDD format). + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server + description: Name of the nameserver associated with the reputation history entry. + type: String + - contextPath: SilentPush.SubnetReputation.ns_server_reputation + description: Reputation score of the nameserver on the specified date. + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server_reputation_explain + description: Explanation of the reputation score, including domain density and listed domains. + type: Unknown + - contextPath: SilentPush.SubnetReputation.ns_server_domain_density + description: Number of domains associated with the nameserver. + type: Number + - contextPath: SilentPush.SubnetReputation.ns_server_domains_listed + description: Number of domains listed in reputation databases. + type: Number + - deprecated: false + description: This command retrieves the reputation history for a specific subnet. + name: silentpush-get-subnet-reputation + arguments: + - name: subnet + isArray: false + description: IPv4 subnet for which reputation information needs to be retrieved. + required: true + secret: false + default: false + - name: explain + isArray: false + description: Show the detailed information used to calculate the reputation score. + required: false + secret: false + default: false + - name: limit + isArray: false + description: Maximum number of reputation history entries to retrieve. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.NameserverReputation.date + description: The date of the subnet reputation record. + type: Number + - contextPath: SilentPush.NameserverReputation.subnet + description: The subnet associated with the reputation record. + type: String + - contextPath: SilentPush.NameserverReputation.subnet_reputation + description: The reputation score of the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_in_subnet + description: Total number of IPs in the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_num_active + description: Number of active IPs in the subnet. + type: Number + - contextPath: SilentPush.NameserverReputation.ips_num_listed + description: Number of listed IPs in the subnet. + type: Number + script: '-' + type: python + subtype: python3 + dockerimage: demisto/python3:3.11.10.116949 + feed: false + isfetch: false + runonce: false + longRunning: false + longRunningPort: false +fromversion: 5.0.0 +tests: +- No tests From f595627b7f96970fc3a594326faa290bc7b5b301 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 20:09:47 +0530 Subject: [PATCH 4/7] silentpush-search-domains. --- .../Integrations/SilentPush/SilentPush.py | 176 +++++++++++++++++- .../Integrations/SilentPush/SilentPush.yml | 91 ++++++++- 2 files changed, 261 insertions(+), 6 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index ff6e82acf78d..5316ca4f2f4e 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -22,6 +22,7 @@ SUBNET_REPUTATION = "explore/ipreputation/history/subnet" ASNS_DOMAIN = "explore/padns/lookup/domain/asns" DENSITY_LOOKUP = "explore/padns/lookup/density" +SEARCH_DOMAIN = "explore/domain/search" ''' COMMANDS INPUTS ''' @@ -73,6 +74,32 @@ InputArgument(name='scope', description='Match level (optional).') ] +SEARCH_DOMAIN_INPUTS = [ + InputArgument(name='domain', + description='Name or wildcard pattern of domain names to search for.'), + InputArgument(name='domain_regex', + description='A valid RE2 regex pattern to match domains. Overrides the domain argument.'), + InputArgument(name='name_server', + description='Name server name or wildcard pattern of the name server used by domains.'), + InputArgument(name='asnum', + description='Autonomous System (AS) number to filter domains.'), + InputArgument(name='asname', + description='Search for all AS numbers where the AS Name begins with the specified value.'), + InputArgument(name='min_ip_diversity', + description='Minimum IP diversity limit to filter domains.'), + InputArgument(name='registrar', + description='Name or partial name of the registrar used to register domains.'), + InputArgument(name='min_asn_diversity', + description='Minimum ASN diversity limit to filter domains.'), + InputArgument(name='certificate_issuer', + description='Filter domains that had SSL certificates issued by the specified certificate issuer. Wildcards supported.'), + InputArgument(name='whois_date_after', + description='Filter domains with a WHOIS creation date after this date (YYYY-MM-DD).'), + InputArgument(name='skip', + description='Number of results to skip in the search query.'), + InputArgument(name='limit', + description='Number of results to return. Defaults to the SilentPush API\'s behavior.') + ] ''' COMMANDS OUTPUTS ''' @@ -104,9 +131,15 @@ OutputArgument(name='domain_asns', output_type=dict, description='Dictionary of Autonomous System Numbers (ASNs) associated with the domain.') ] DENSITY_LOOKUP_OUTPUTS = [ - OutputArgument(name='density', output_type=int, description='The density value associated with the query result.'), - OutputArgument(name='nssrv', output_type=str, description='The name server (NS) for the query result.') -] + OutputArgument(name='density', output_type=int, description='The density value associated with the query result.'), + OutputArgument(name='nssrv', output_type=str, description='The name server (NS) for the query result.') + ] +SEARCH_DOMAIN_OUTPUTS = [ + OutputArgument(name='asn_diversity', output_type=int, description='The diversity of Autonomous System Numbers (ASNs) associated with the domain.'), + OutputArgument(name='host', output_type=str, description='The domain name (host) associated with the record.'), + OutputArgument(name='ip_diversity_all', output_type=int, description='The total number of unique IPs associated with the domain.'), + OutputArgument(name='ip_diversity_groups', output_type=int, description='The number of unique IP groups associated with the domain.') + ] @@ -141,7 +174,7 @@ name="credentials", display="API Key", required=False, - key_type=ParameterTypes.TEXT_AREA_ENCRYPTED, + key_type=ParameterTypes.AUTH, ), ConfKey( name="insecure", @@ -339,6 +372,62 @@ def density_lookup(self, qtype: str, query: str, **kwargs) -> Dict[str, Any]: params=params ) + 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 (YYYY-MM-DD). + end_date (str, optional): End date for domain search (YYYY-MM-DD). + 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 (ASN) filter. + asname (str, optional): ASN 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): Filter domains by certificate issuer. + whois_date_after (str, optional): Filter domains based on WHOIS date (YYYY-MM-DD). + skip (int, optional): Number of results to skip. + + Returns: + dict: Search results matching the specified criteria. + """ + url_suffix = SEARCH_DOMAIN + + # Prepare parameters and filter out None values using filter_none_values helper function + params = filter_none_values({ + '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, + }) + + # Make the request with the filtered parameters + return self._http_request('GET', url_suffix, params=params) + + ''' HELPER FUNCTIONS ''' def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: """Removes None values from a dictionary.""" @@ -375,7 +464,7 @@ def test_module(client: Client, first_fetch_time: int) -> str: # Cortex XSOAR will print everything you return that is different than 'ok' as # an error. try: - resp = client.get_job_status("job_id", "max_wait", "result_type") + resp = client.search_domains("job_id", "max_wait", "result_type") if resp.get("status_code") != 200: return f"Connection failed :- {resp.get('errors')}" return 'ok' @@ -632,8 +721,82 @@ def density_lookup_command(client: Client, args: dict) -> CommandResults: raw_response=raw_response ) +@metadata_collector.command( + command_name="silentpush-search-domains", + inputs_list=SEARCH_DOMAIN_INPUTS, + outputs_prefix="SilentPush.Domain", + outputs_list=SEARCH_DOMAIN_OUTPUTS, + description="This command search for domains with optional filters." +) +def search_domains_command(client: Client, args: dict) -> CommandResults: + """ + Command to search for domains based on various filter parameters. + Args: + client (Client): The client instance to interact with the external service. + args (dict): Arguments containing filter parameters for domain search. + Returns: + CommandResults: The results of the domain search, including readable output and raw response. + """ + # Extract arguments + 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)) + 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')) + + # Call the client method to search domains + 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, + 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 + ) + + records = raw_response.get('response', {}).get('records', []) + + if not records: + return CommandResults( + readable_output="No domains found.", + raw_response=raw_response, + outputs_prefix='SilentPush.Domain', + outputs_key_field='domain', + outputs=records + ) + + readable_output = tableToMarkdown('Domain Search Results', records) + + return CommandResults( + outputs_prefix='SilentPush.Domain', + outputs_key_field='domain', + outputs=records, + readable_output=readable_output, + raw_response=raw_response + ) ''' MAIN FUNCTION ''' @@ -679,6 +842,9 @@ def main() -> None: elif demisto.command() == 'silentpush-density-lookup': return_results(density_lookup_command(client, demisto.args())) + elif demisto.command() == 'silentpush-search-domains': + return_results(search_domains_command(client, demisto.args())) + except Exception as e: demisto.error(traceback.format_exc()) # print the traceback return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}') diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 3e839b15f91e..28e93ad21af3 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -13,7 +13,7 @@ configuration: defaultvalue: https://api.silentpush.com - display: API Key name: credentials - type: 14 + type: 9 required: false - display: Trust any certificate (not secure) name: insecure @@ -185,6 +185,95 @@ script: - contextPath: SilentPush.NameserverReputation.ips_num_listed description: Number of listed IPs in the subnet. type: Number + - deprecated: false + description: This command search for domains with optional filters. + name: silentpush-search-domains + arguments: + - name: domain + isArray: false + description: Name or wildcard pattern of domain names to search for. + required: false + secret: false + default: false + - name: domain_regex + isArray: false + description: A valid RE2 regex pattern to match domains. Overrides the domain argument. + required: false + secret: false + default: false + - name: name_server + isArray: false + description: Name server name or wildcard pattern of the name server used by domains. + required: false + secret: false + default: false + - name: asnum + isArray: false + description: Autonomous System (AS) number to filter domains. + required: false + secret: false + default: false + - name: asname + isArray: false + description: Search for all AS numbers where the AS Name begins with the specified value. + required: false + secret: false + default: false + - name: min_ip_diversity + isArray: false + description: Minimum IP diversity limit to filter domains. + required: false + secret: false + default: false + - name: registrar + isArray: false + description: Name or partial name of the registrar used to register domains. + required: false + secret: false + default: false + - name: min_asn_diversity + isArray: false + description: Minimum ASN diversity limit to filter domains. + required: false + secret: false + default: false + - name: certificate_issuer + isArray: false + description: Filter domains that had SSL certificates issued by the specified certificate issuer. Wildcards supported. + required: false + secret: false + default: false + - name: whois_date_after + isArray: false + description: Filter domains with a WHOIS creation date after this date (YYYY-MM-DD). + required: false + secret: false + default: false + - name: skip + isArray: false + description: Number of results to skip in the search query. + required: false + secret: false + default: false + - name: limit + isArray: false + description: Number of results to return. Defaults to the SilentPush API's behavior. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.Domain.asn_diversity + description: The diversity of Autonomous System Numbers (ASNs) associated with the domain. + type: Number + - contextPath: SilentPush.Domain.host + description: The domain name (host) associated with the record. + type: String + - contextPath: SilentPush.Domain.ip_diversity_all + description: The total number of unique IPs associated with the domain. + type: Number + - contextPath: SilentPush.Domain.ip_diversity_groups + description: The number of unique IP groups associated with the domain. + type: Number script: '-' type: python subtype: python3 From ae0c753af1f65c3c351ed5ed25badbba924e4db6 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 21:12:10 +0530 Subject: [PATCH 5/7] silentpush-list-domain-infratags. --- .../Integrations/SilentPush/SilentPush.py | 181 ++++++++++++++++++ .../Integrations/SilentPush/SilentPush.yml | 64 +++++++ 2 files changed, 245 insertions(+) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 5316ca4f2f4e..c2830bb165ab 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -23,6 +23,7 @@ ASNS_DOMAIN = "explore/padns/lookup/domain/asns" DENSITY_LOOKUP = "explore/padns/lookup/density" SEARCH_DOMAIN = "explore/domain/search" +DOMAIN_INFRATAGS = "explore/bulk/domain/infratags" ''' COMMANDS INPUTS ''' @@ -100,6 +101,20 @@ InputArgument(name='limit', description='Number of results to return. Defaults to the SilentPush API\'s behavior.') ] +DOMAIN_INFRATAGS_INPUTS = [ + InputArgument(name='domains', + description='Comma-separated list of domains.', + required=True), + InputArgument(name='cluster', + description='Whether to cluster the results.'), + InputArgument(name='mode', + description='Mode for lookup (live/padns). Defaults to "live".', + default='live'), + InputArgument(name='match', + description='Handling of self-hosted infrastructure. Defaults to "self".', + default='self') + ] + ''' COMMANDS OUTPUTS ''' @@ -140,6 +155,46 @@ OutputArgument(name='ip_diversity_all', output_type=int, description='The total number of unique IPs associated with the domain.'), OutputArgument(name='ip_diversity_groups', output_type=int, description='The number of unique IP groups associated with the domain.') ] +DOMAIN_INFRATAGS_OUTPUTS = [ + OutputArgument(name='infratags.domain', + output_type=str, + description='The domain associated with the infratag.'), + OutputArgument(name='infratags.mode', + output_type=str, + description='The mode associated with the domain infratag.'), + OutputArgument(name='infratags.tag', + output_type=str, + description='The tag associated with the domain infratag.'), + + OutputArgument(name='tag_clusters.25.domains', + output_type=list, + description='List of domains in the tag cluster with score 25.'), + OutputArgument(name='tag_clusters.25.match', + output_type=str, + description='The match string associated with the domains in the tag cluster with score 25.'), + + OutputArgument(name='tag_clusters.50.domains', + output_type=list, + description='List of domains in the tag cluster with score 50.'), + OutputArgument(name='tag_clusters.50.match', + output_type=str, + description='The match string associated with the domains in the tag cluster with score 50.'), + + OutputArgument(name='tag_clusters.75.domains', + output_type=list, + description='List of domains in the tag cluster with score 75.'), + OutputArgument(name='tag_clusters.75.match', + output_type=str, + description='The match string associated with the domains in the tag cluster with score 75.'), + + OutputArgument(name='tag_clusters.100.domains', + output_type=list, + description='List of domains in the tag cluster with score 100.'), + OutputArgument(name='tag_clusters.100.match', + output_type=str, + description='The match string associated with the domains in the tag cluster with score 100.') + ] + @@ -427,6 +482,65 @@ def search_domains(self, query: Optional[str] = None, start_date: Optional[str] # Make the request with the filtered parameters return self._http_request('GET', url_suffix, params=params) + def list_domain_infratags( + self, + domains: list, + cluster: bool = False, + mode: str = 'live', + match: str = 'self', + as_of: Optional[str] = None, + origin_uid: Optional[str] = None, + use_get: bool = False + ) -> dict: + """ + Retrieve infrastructure tags for specified domains, supporting both GET and POST methods. + + Args: + domains (list): List of domains to fetch infrastructure tags for. + cluster (bool): Whether to include cluster information (default: False). + mode (str): Tag retrieval mode (default: 'live'). + match (str): Matching criteria (default: 'self'). + as_of (Optional[str]): Specific timestamp for tag retrieval. + origin_uid (Optional[str]): Unique identifier for the API user. + use_get (bool): Use GET method instead of POST (default: False). + + Returns: + dict: API response containing infratags and optional tag clusters. + """ + url_suffix = DOMAIN_INFRATAGS + + # Construct the params dictionary + params = { + 'mode': mode, + 'match': match, + 'clusters': int(cluster), + 'as_of': as_of, + 'origin_uid': origin_uid + } + + # Remove any None values from params using filter_none_values helper function + params = filter_none_values(params) + + if use_get: + # Use GET method + response = self._http_request( + method='GET', + url_suffix=url_suffix, + params=params + ) + else: + # Use POST method + payload = {'domains': domains} + response = self._http_request( + method='POST', + url_suffix=url_suffix, + params=params, + data=payload + ) + + return response + + ''' HELPER FUNCTIONS ''' def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: @@ -799,6 +913,70 @@ def search_domains_command(client: Client, args: dict) -> CommandResults: ) +def format_tag_clusters(tag_clusters: list) -> str: + """ + Helper function to format the tag clusters output. + + Args: + tag_clusters (list): List of domain tag clusters. + + Returns: + str: Formatted table output for tag clusters. + """ + if not tag_clusters: + return "\n\n**No tag cluster data returned by the API.**" + + cluster_details = [{'Cluster Level': key, 'Details': value} for cluster in tag_clusters for key, value in cluster.items()] + return tableToMarkdown('Domain Tag Clusters', cluster_details) + +@metadata_collector.command( + command_name="silentpush-list-domain-infratags", + inputs_list=DOMAIN_INFRATAGS_INPUTS, + outputs_prefix="SilentPush.InfraTags", + outputs_list=DOMAIN_INFRATAGS_OUTPUTS, + description="This command get infratags for multiple domains with optional clustering." +) +def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: + """ + Command function to retrieve domain infratags with optional cluster details. + + Args: + client (Client): SilentPush API client. + args (dict): Command arguments. + + Returns: + CommandResults: Formatted results of the infratags lookup. + """ + 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) + origin_uid = args.get('origin_uid', None) + use_get = argToBoolean(args.get('use_get', False)) + + if not domains and not use_get: + raise ValueError('"domains" argument is required when using POST.') + + + raw_response = client.list_domain_infratags(domains, cluster, mode, match, as_of, origin_uid, use_get) + infratags = raw_response.get('response', {}).get('infratags', []) + tag_clusters = raw_response.get('response', {}).get('tag_clusters', []) + + readable_output = tableToMarkdown('Domain Infratags', infratags) + + if cluster: + readable_output += format_tag_clusters(tag_clusters) + + return CommandResults( + outputs_prefix='SilentPush.InfraTags', + outputs_key_field='domain', + outputs=raw_response, + readable_output=readable_output, + raw_response=raw_response + ) + + ''' MAIN FUNCTION ''' @@ -844,6 +1022,9 @@ def main() -> None: elif demisto.command() == 'silentpush-search-domains': return_results(search_domains_command(client, demisto.args())) + + elif demisto.command() == 'silentpush-list-domain-infratags': + return_results(list_domain_infratags_command(client, demisto.args())) except Exception as e: demisto.error(traceback.format_exc()) # print the traceback diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 28e93ad21af3..9ea318419c59 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -185,6 +185,70 @@ script: - contextPath: SilentPush.NameserverReputation.ips_num_listed description: Number of listed IPs in the subnet. type: Number + - deprecated: false + description: This command get infratags for multiple domains with optional clustering. + name: silentpush-list-domain-infratags + arguments: + - name: domains + isArray: false + description: Comma-separated list of domains. + required: true + secret: false + default: false + - name: cluster + isArray: false + description: Whether to cluster the results. + required: false + secret: false + default: false + - name: mode + isArray: false + description: Mode for lookup (live/padns). Defaults to "live". + required: false + secret: false + default: false + defaultValue: live + - name: match + isArray: false + description: Handling of self-hosted infrastructure. Defaults to "self". + required: false + secret: false + default: false + defaultValue: self + outputs: + - contextPath: SilentPush.InfraTags.infratags.domain + description: The domain associated with the infratag. + type: String + - contextPath: SilentPush.InfraTags.infratags.mode + description: The mode associated with the domain infratag. + type: String + - contextPath: SilentPush.InfraTags.infratags.tag + description: The tag associated with the domain infratag. + type: String + - contextPath: SilentPush.InfraTags.tag_clusters.25.domains + description: List of domains in the tag cluster with score 25. + type: Unknown + - contextPath: SilentPush.InfraTags.tag_clusters.25.match + description: The match string associated with the domains in the tag cluster with score 25. + type: String + - contextPath: SilentPush.InfraTags.tag_clusters.50.domains + description: List of domains in the tag cluster with score 50. + type: Unknown + - contextPath: SilentPush.InfraTags.tag_clusters.50.match + description: The match string associated with the domains in the tag cluster with score 50. + type: String + - contextPath: SilentPush.InfraTags.tag_clusters.75.domains + description: List of domains in the tag cluster with score 75. + type: Unknown + - contextPath: SilentPush.InfraTags.tag_clusters.75.match + description: The match string associated with the domains in the tag cluster with score 75. + type: String + - contextPath: SilentPush.InfraTags.tag_clusters.100.domains + description: List of domains in the tag cluster with score 100. + type: Unknown + - contextPath: SilentPush.InfraTags.tag_clusters.100.match + description: The match string associated with the domains in the tag cluster with score 100. + type: String - deprecated: false description: This command search for domains with optional filters. name: silentpush-search-domains From 6988da91509724118f0db1732444d1b1a85d2f3e Mon Sep 17 00:00:00 2001 From: yash-metron Date: Thu, 30 Jan 2025 21:15:54 +0530 Subject: [PATCH 6/7] silentpush-list-domain-infratags-hot-fix. --- Packs/SilentPush/Integrations/SilentPush/SilentPush.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index c2830bb165ab..89210f1f915d 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -379,7 +379,7 @@ def get_subnet_reputation(self, subnet: str, explain: bool = False, limit: Optio Returns: Dict[str, Any]: Subnet reputation history information. """ - url_suffix = f"/{subnet}" + url_suffix = f"{SUBNET_REPUTATION}/{subnet}" params = { "explain": str(explain).lower() if explain else None, @@ -665,7 +665,7 @@ def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResu CommandResults: The command results containing nameserver reputation data. """ nameserver = args.get("nameserver") - explain = argToBoolean(args.get("explain", "false")) + explain = argToBoolean(args.get("explain", False)) limit = arg_to_number(args.get("limit")) if not nameserver: From d6da047b3f406857e42f799b7ed6f6e3f4afb658 Mon Sep 17 00:00:00 2001 From: yash-metron Date: Fri, 31 Jan 2025 12:08:50 +0530 Subject: [PATCH 7/7] silentpush-list-domain-information. --- .../Integrations/SilentPush/SilentPush.py | 235 +++++++++++++++++- .../Integrations/SilentPush/SilentPush.yml | 75 +++++- Packs/SilentPush/pack_metadata.json | 2 +- 3 files changed, 299 insertions(+), 13 deletions(-) diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py index 89210f1f915d..bfbfaaca8f80 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.py +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.py @@ -7,7 +7,7 @@ import urllib3 import dateparser import traceback -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Tuple # Disable insecure warnings urllib3.disable_warnings() @@ -24,6 +24,9 @@ DENSITY_LOOKUP = "explore/padns/lookup/density" SEARCH_DOMAIN = "explore/domain/search" DOMAIN_INFRATAGS = "explore/bulk/domain/infratags" +DOMAIN_INFO = "explore/bulk/domaininfo" +RISK_SCORE = "explore/bulk/domain/riskscore" +WHOIS = "explore/domain/whois" ''' COMMANDS INPUTS ''' @@ -62,7 +65,7 @@ ] ASNS_DOMAIN_INPUTS = [ InputArgument(name='domain', # option 1 - description='Domain name to search ASNs for. Retrieves ASNs associated with A records for the specified domain and its subdomains in the last 30 days.', + description='Domain name to search ASNs for. Retrieves ASNs associated with a records for the specified domain and its subdomains in the last 30 days.', required=True) ] DENSITY_LOOKUP_INPUTS = [ @@ -114,6 +117,17 @@ description='Handling of self-hosted infrastructure. Defaults to "self".', default='self') ] +LIST_DOMAIN_INPUTS = [ + InputArgument(name='domains', + description='Comma-separated list of domains to query.', + required=True), + InputArgument(name='fetch_risk_score', + description='Whether to fetch risk scores for the domains.', + required=False), + InputArgument(name='fetch_whois_info', + description='Whether to fetch WHOIS information for the domains.', + required=False) + ] @@ -194,6 +208,21 @@ output_type=str, description='The match string associated with the domains in the tag cluster with score 100.') ] +LIST_DOMAIN_OUTPUTS = [ + OutputArgument(name='domain', output_type=str, description='The domain name queried.'), + OutputArgument(name='last_seen', output_type=int, description='The last seen date of the domain in YYYYMMDD format.'), + OutputArgument(name='query', output_type=str, description='The domain name used for the query.'), + OutputArgument(name='whois_age', output_type=int, description='The age of the domain in days based on WHOIS creation date.'), + OutputArgument(name='first_seen', output_type=int, description='The first seen date of the domain in YYYYMMDD format.'), + OutputArgument(name='is_new', output_type=bool, description='Indicates whether the domain is newly observed.'), + OutputArgument(name='zone', output_type=str, description='The top-level domain (TLD) or zone of the queried domain.'), + OutputArgument(name='registrar', output_type=str, description='The registrar responsible for the domain registration.'), + OutputArgument(name='age_score', output_type=int, description='A risk score based on the domain\'s age.'), + OutputArgument(name='whois_created_date', output_type=str, description='The WHOIS creation date of the domain in YYYY-MM-DD HH:MM:SS format.'), + OutputArgument(name='is_new_score', output_type=int, description='A risk score indicating how new the domain is.'), + OutputArgument(name='age', output_type=int, description='The age of the domain in days.') + ] + @@ -541,6 +570,103 @@ def list_domain_infratags( return response + def fetch_bulk_domain_info(self, domains: List[str]) -> Dict[str, Any]: + """Fetch basic domain information for a list of domains.""" + response = self._http_request( + method='POST', + url_suffix=DOMAIN_INFO, + data={'domains': domains} + ) + domain_info_list = response.get('response', {}).get('domaininfo', []) + return {item['domain']: item for item in domain_info_list} + + + def fetch_risk_scores(self, domains: List[str]) -> Dict[str, Any]: + """Fetch risk scores for a list of domains.""" + response = self._http_request( + method='POST', + url_suffix=RISK_SCORE, + data={'domains': domains} + ) + risk_score_list = response.get('response', []) + return {item['domain']: item for item in risk_score_list} + + + def fetch_whois_info(self, domain: str) -> Dict[str, Any]: + """Fetch WHOIS information for a single domain.""" + try: + response = self._http_request( + method='GET', + url_suffix=f'{WHOIS}/{domain}' + ) + whois_data = response.get('response', {}).get('whois', [{}])[0] + + return { + 'Registrant Name': whois_data.get('name', 'N/A'), + 'Registrant Organization': whois_data.get('org', 'N/A'), + 'Registrant Address': ', '.join(whois_data.get('address', [])) if isinstance(whois_data.get('address'), list) else whois_data.get('address', 'N/A'), + 'Registrant City': whois_data.get('city', 'N/A'), + 'Registrant State': whois_data.get('state', 'N/A'), + 'Registrant Country': whois_data.get('country', 'N/A'), + 'Registrant Zipcode': whois_data.get('zipcode', 'N/A'), + 'Creation Date': whois_data.get('created', 'N/A'), + 'Updated Date': whois_data.get('updated', 'N/A'), + 'Expiration Date': whois_data.get('expires', 'N/A'), + 'Registrar': whois_data.get('registrar', 'N/A'), + 'WHOIS Server': whois_data.get('whois_server', 'N/A'), + 'Nameservers': ', '.join(whois_data.get('nameservers', [])), + 'Emails': ', '.join(whois_data.get('emails', [])) + } + except Exception as e: + return {'error': str(e)} + + + def list_domain_information(self, domains: List[str], fetch_risk_score: Optional[bool] = False, fetch_whois_info: Optional[bool] = False) -> Dict[str, Any]: + """ + Retrieve domain information along with optional risk scores and WHOIS data. + + Args: + http_request (function): HTTP request function for making API calls. + domains (List[str]): List of domains to get information for. + fetch_risk_score (bool, optional): Whether to fetch risk scores. Defaults to False. + fetch_whois_info (bool, optional): Whether to fetch WHOIS information. Defaults to False. + + Returns: + Dict[str, Any]: Dictionary containing domain information with optional risk scores and WHOIS data. + + Raises: + ValueError: If more than 100 domains are provided. + """ + if len(domains) > 100: + raise ValueError("Maximum of 100 domains can be submitted in a single request.") + + domain_info_dict = self.fetch_bulk_domain_info(domains) + + risk_score_dict = self.fetch_risk_scores(domains) if fetch_risk_score else {} + + whois_info_dict = {domain: self.fetch_whois_info(domain) for domain in domains} if fetch_whois_info else {} + + results = [] + for domain in domains: + domain_info = { + 'domain': domain, + **domain_info_dict.get(domain, {}), + } + + if fetch_risk_score: + risk_data = risk_score_dict.get(domain, {}) + domain_info.update({ + 'risk_score': risk_data.get('sp_risk_score', 'N/A'), + 'risk_score_explanation': risk_data.get('sp_risk_score_explain', 'N/A') + }) + + if fetch_whois_info: + domain_info['whois_info'] = whois_info_dict.get(domain, {}) + + results.append(domain_info) + + return {'domains': results} + ''' HELPER FUNCTIONS ''' def filter_none_values(params: Dict[str, Any]) -> Dict[str, Any]: @@ -697,7 +823,7 @@ def get_nameserver_reputation_command(client: Client, args: dict) -> CommandResu @metadata_collector.command( command_name="silentpush-get-subnet-reputation", inputs_list=SUBNET_REPUTATION_INPUTS, - outputs_prefix="SilentPush.NameserverReputation", + outputs_prefix="SilentPush.SubnetReputation", outputs_list=SUBNET_REPUTATION_OUTPUTS, description="This command retrieves the reputation history for a specific subnet." ) @@ -796,7 +922,7 @@ def get_asns_for_domain_command(client: Client, args: dict) -> CommandResults: inputs_list=DENSITY_LOOKUP_INPUTS, outputs_prefix="SilentPush.DensityLookup", outputs_list=DENSITY_LOOKUP_OUTPUTS, - description="This command query granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information." + description="This command queries granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information." ) def density_lookup_command(client: Client, args: dict) -> CommandResults: """ @@ -977,6 +1103,104 @@ def list_domain_infratags_command(client: Client, args: dict) -> CommandResults: ) +@metadata_collector.command( + command_name="silentpush-list-domain-information", + inputs_list=LIST_DOMAIN_INPUTS, + outputs_prefix="SilentPush.Domain", + outputs_list=LIST_DOMAIN_OUTPUTS, + description="This command get infratags for multiple domains with optional clustering." +) +def list_domain_information_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + Handle the list-domain-information command execution. + + Args: + client (Client): The client object for making API calls + args (Dict[str, Any]): Command arguments + + Returns: + CommandResults: Results for XSOAR + """ + domains, fetch_risk_score, fetch_whois_info = parse_arguments(args) + response = client.list_domain_information(domains, fetch_risk_score, fetch_whois_info) + markdown = format_domain_information(response, fetch_risk_score, fetch_whois_info) + + return CommandResults( + outputs_prefix='SilentPush.Domain', + outputs_key_field='domain', + outputs=response.get('domains', []), + readable_output=markdown, + raw_response=response + ) + +def parse_arguments(args: Dict[str, Any]) -> Tuple[List[str], bool, bool]: + """ + Parse and validate command arguments. + + Args: + args (Dict[str, Any]): Command arguments + + Returns: + Tuple[List[str], bool, bool]: Parsed domains, risk score flag, and WHOIS flag + """ + domains_arg = args.get('domains', '') + if not domains_arg: + raise DemistoException('No domains provided') + + domains = [domain.strip() for domain in domains_arg.split(',') if domain.strip()] + fetch_risk_score = argToBoolean(args.get('fetch_risk_score', False)) + fetch_whois_info = argToBoolean(args.get('fetch_whois_info', False)) + + return domains, fetch_risk_score, fetch_whois_info + +def format_domain_information(response: Dict[str, Any], fetch_risk_score: bool, fetch_whois_info: bool) -> str: + """ + Format the response data into markdown format. + + Args: + response (Dict[str, Any]): API response data + fetch_risk_score (bool): Whether to include risk score data + fetch_whois_info (bool): Whether to include WHOIS data + + Returns: + str: Markdown-formatted response + """ + markdown = ['# Domain Information Results\n'] + + for domain_data in response.get('domains', []): + domain = domain_data.get('domain', 'N/A') + markdown.append(f'## Domain: {domain}') + + basic_info = { + 'Created Date': domain_data.get('whois_created_date', 'N/A'), + 'Updated Date': domain_data.get('whois_updated_date', 'N/A'), + 'Expiration Date': domain_data.get('whois_expiration_date', 'N/A'), + 'Registrar': domain_data.get('registrar', 'N/A'), + 'Status': domain_data.get('status', 'N/A'), + 'Name Servers': domain_data.get('nameservers', 'N/A') + } + markdown.append(tableToMarkdown('Domain Information', [basic_info])) + + if fetch_risk_score: + risk_info = { + 'Risk Score': domain_data.get('risk_score', 'N/A'), + 'Risk Score Explanation': domain_data.get('risk_score_explanation', 'N/A') + } + markdown.append(tableToMarkdown('Risk Assessment', [risk_info])) + + if fetch_whois_info: + whois_info = domain_data.get('whois_info', {}) + if whois_info and isinstance(whois_info, dict): + if 'error' in whois_info: + markdown.append(f'WHOIS Error: {whois_info["error"]}') + else: + markdown.append(tableToMarkdown('WHOIS Information', [whois_info])) + + markdown.append('\n---\n') + + return '\n'.join(markdown) + + ''' MAIN FUNCTION ''' @@ -1026,6 +1250,9 @@ def main() -> None: elif demisto.command() == 'silentpush-list-domain-infratags': return_results(list_domain_infratags_command(client, demisto.args())) + elif demisto.command() == 'silentpush-list-domain-information': + return_results(list_domain_information_command(client, demisto.args())) + except Exception as e: demisto.error(traceback.format_exc()) # print the traceback return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}') diff --git a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml index 9ea318419c59..ad3e880b933b 100644 --- a/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml +++ b/Packs/SilentPush/Integrations/SilentPush/SilentPush.yml @@ -26,7 +26,7 @@ configuration: script: commands: - deprecated: false - description: This command query granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information. + description: This command queries granular DNS/IP parameters (e.g., NS servers, MX servers, IPaddresses, ASNs) for density information. name: silentpush-density-lookup arguments: - name: qtype @@ -60,7 +60,7 @@ script: arguments: - name: domain isArray: false - description: Domain name to search ASNs for. Retrieves ASNs associated with A records for the specified domain and its subdomains in the last 30 days. + description: Domain name to search ASNs for. Retrieves ASNs associated with a records for the specified domain and its subdomains in the last 30 days. required: true secret: false default: false @@ -167,24 +167,83 @@ script: secret: false default: false outputs: - - contextPath: SilentPush.NameserverReputation.date + - contextPath: SilentPush.SubnetReputation.date description: The date of the subnet reputation record. type: Number - - contextPath: SilentPush.NameserverReputation.subnet + - contextPath: SilentPush.SubnetReputation.subnet description: The subnet associated with the reputation record. type: String - - contextPath: SilentPush.NameserverReputation.subnet_reputation + - contextPath: SilentPush.SubnetReputation.subnet_reputation description: The reputation score of the subnet. type: Number - - contextPath: SilentPush.NameserverReputation.ips_in_subnet + - contextPath: SilentPush.SubnetReputation.ips_in_subnet description: Total number of IPs in the subnet. type: Number - - contextPath: SilentPush.NameserverReputation.ips_num_active + - contextPath: SilentPush.SubnetReputation.ips_num_active description: Number of active IPs in the subnet. type: Number - - contextPath: SilentPush.NameserverReputation.ips_num_listed + - contextPath: SilentPush.SubnetReputation.ips_num_listed description: Number of listed IPs in the subnet. type: Number + - deprecated: false + description: This command get infratags for multiple domains with optional clustering. + name: silentpush-list-domain-information + arguments: + - name: domains + isArray: false + description: Comma-separated list of domains to query. + required: true + secret: false + default: false + - name: fetch_risk_score + isArray: false + description: Whether to fetch risk scores for the domains. + required: false + secret: false + default: false + - name: fetch_whois_info + isArray: false + description: Whether to fetch WHOIS information for the domains. + required: false + secret: false + default: false + outputs: + - contextPath: SilentPush.Domain.domain + description: The domain name queried. + type: String + - contextPath: SilentPush.Domain.last_seen + description: The last seen date of the domain in YYYYMMDD format. + type: Number + - contextPath: SilentPush.Domain.query + description: The domain name used for the query. + type: String + - contextPath: SilentPush.Domain.whois_age + description: The age of the domain in days based on WHOIS creation date. + type: Number + - contextPath: SilentPush.Domain.first_seen + description: The first seen date of the domain in YYYYMMDD format. + type: Number + - contextPath: SilentPush.Domain.is_new + description: Indicates whether the domain is newly observed. + type: Boolean + - contextPath: SilentPush.Domain.zone + description: The top-level domain (TLD) or zone of the queried domain. + type: String + - contextPath: SilentPush.Domain.registrar + description: The registrar responsible for the domain registration. + type: String + - contextPath: SilentPush.Domain.age_score + description: A risk score based on the domain's age. + type: Number + - contextPath: SilentPush.Domain.whois_created_date + description: The WHOIS creation date of the domain in YYYY-MM-DD HH:MM:SS format. + type: String + - contextPath: SilentPush.Domain.is_new_score + description: A risk score indicating how new the domain is. + type: Number + - contextPath: SilentPush.Domain.age + description: The age of the domain in days. + type: Number - deprecated: false description: This command get infratags for multiple domains with optional clustering. name: silentpush-list-domain-infratags diff --git a/Packs/SilentPush/pack_metadata.json b/Packs/SilentPush/pack_metadata.json index 123de5898f65..8cfd8027eea4 100644 --- a/Packs/SilentPush/pack_metadata.json +++ b/Packs/SilentPush/pack_metadata.json @@ -3,7 +3,7 @@ "description": "SilentPush integration for domain and IP intelligence\u001b[C", "support": "partner", "currentVersion": "1.0.0", - "author": "Yash", + "author": "Silent Push", "url": "", "email": "", "categories": [