diff --git a/Packs/SpurContextAPI/CONTRIBUTORS.json b/Packs/SpurContextAPI/CONTRIBUTORS.json index ce9d08a30890..b8b68c41573c 100644 --- a/Packs/SpurContextAPI/CONTRIBUTORS.json +++ b/Packs/SpurContextAPI/CONTRIBUTORS.json @@ -1,3 +1,4 @@ [ - "Fabio Dias" -] + "Fabio Dias", + "Sokrates Hillestad" +] \ No newline at end of file diff --git a/Packs/SpurContextAPI/Integrations/SpurContextAPI/README.md b/Packs/SpurContextAPI/Integrations/SpurContextAPI/README.md index 6810ec08b055..75a74b65ccc6 100644 --- a/Packs/SpurContextAPI/Integrations/SpurContextAPI/README.md +++ b/Packs/SpurContextAPI/Integrations/SpurContextAPI/README.md @@ -7,12 +7,16 @@ This integration was integrated and tested with version 2 of SpurContextAPI. 2. Search for SpurContextAPI. 3. Click **Add instance** to create and configure a new integration instance. - | **Parameter** | **Required** | - | --- | --- | - | API Token | True | + | **Parameter** | **Description** | **Required** | + | --- | --- | --- | + | Server URL (e.g. https://api.spur.us/) | | False | + | API Token | | True | + | Source Reliability | Reliability of the source providing the intelligence data. | False | + | Use system proxy settings | | False | 4. Click **Test** to validate the URLs, token, and connection. + ## Commands You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. @@ -52,3 +56,51 @@ Enrich indicators using the Spur Context API. | SpurContextAPI.Context.client_count | number | The average number of clients we observe on this IP address. | | SpurContextAPI.Context.client_behaviors | array | An array of behavior tags for an IP Address. | | SpurContextAPI.Context.client_types | array | The different type of client devices that we have observed on this IP address. | +### ip + +*** +IP reputation command using the Spur Context API. + +#### Base Command + +`ip` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| ip | IP address to enrich. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| DBotScore.Score | string | The actual score. | +| DBotScore.Indicator | string | The indicator that was tested. | +| DBotScore.Type | string | The indicator type. | +| DBotScore.Vendor | string | The vendor used to calculate the score. | +| DBotScore.Reliability | String | Reliability of the source providing the intelligence data. | +| IP.Address | string | IP address. | +| IP.ASN | string | The autonomous system name for the IP address, for example: "AS8948". | +| IP.ASOwner | String | The autonomous system owner of the IP. | +| IP.ClientTypes | array | The organization name. | +| IP.Geo.Country | string | The country in which the IP address is located. | +| IP.Organization.Name | string | The organization name. | +| IP.Risks | array | Risks that we have determined based on our collection of data. | +| IP.Tunnels | array | The different types of proxy or VPN services that are running on this IP address. | +| SpurContextAPI.Context.ip | string | IP that was enriched. | +| SpurContextAPI.Context.as | object | Autonomous System details for an IP Address. | +| SpurContextAPI.Context.organization | string | The organization using this IP address. | +| SpurContextAPI.Context.infrastructure | string | The primary infracstructure type that this IP address supports. Common tags are MOBILE and DATACENTER. | +| SpurContextAPI.Context.location | object | Data-center or IP Hosting location based on MaxMind GeoLite. | +| SpurContextAPI.Context.services | array | The different types of proxy or VPN services that are running on this IP address. | +| SpurContextAPI.Context.tunnels | array | Different VPN or proxy tunnels that are currently in-use on this IP address. | +| SpurContextAPI.Context.risks | array | Risks that we have determined based on our collection of data. | +| SpurContextAPI.Context.client_concentration | object | The strongest location concentration for clients using this IP address. | +| SpurContextAPI.Context.client_countries | number | The number of countries that we have observed clients located in for this IP address. | +| SpurContextAPI.Context.client_spread | number | The total geographic area in kilometers where we have observed users. | +| SpurContextAPI.Context.client_proxies | array | The different types of callback proxies we have observed on clients using this IP address. | +| SpurContextAPI.Context.client_count | number | The average number of clients we observe on this IP address. | +| SpurContextAPI.Context.client_behaviors | array | An array of behavior tags for an IP Address. | +| SpurContextAPI.Context.client_types | array | The different type of client devices that we have observed on this IP address. | + diff --git a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py index d5cd65030910..d252f1717a25 100644 --- a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py +++ b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py @@ -11,29 +11,29 @@ urllib3.disable_warnings() -''' CONSTANTS ''' +""" CONSTANTS """ -DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' # ISO8601 format with UTC, default in XSOAR +DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR -''' CLIENT CLASS ''' +""" CLIENT CLASS """ class Client(BaseClient): - def ip(self, ip: str) -> CommandResults: + def ip(self, ip: str) -> dict: # Validate that the input is a valid IP address try: ipaddress.ip_address(ip) except ValueError: - raise ValueError(f'Invalid IP address: {ip}') + raise ValueError(f"Invalid IP address: {ip}") encoded_ip = urllib.parse.quote(ip) full_url = urljoin(self._base_url, "/v2/context") full_url = urljoin(full_url, encoded_ip) - demisto.debug(f'SpurContextAPI full_url: {full_url}') + demisto.debug(f"SpurContextAPI full_url: {full_url}") # Make the request response = self._http_request( - method='GET', + method="GET", full_url=full_url, headers=self._headers, ) @@ -41,7 +41,37 @@ def ip(self, ip: str) -> CommandResults: return response -''' HELPER FUNCTIONS ''' +""" SPUR IP INDICATOR CLASS """ + + +class SpurIP(Common.IP): + + def __init__(self, client_types=None, risks=None, tunnels=None, **kwargs) -> None: + + super().__init__(**kwargs) + + self.client_types = client_types if client_types else [] + self.risks = risks if risks else [] + self.tunnels = tunnels if tunnels else {} + + def to_context(self) -> dict: + context = super().to_context() + + context_path = context[super().CONTEXT_PATH] + + if self.risks: + context_path["Risks"] = self.risks + + if self.client_types: + context_path["ClientTypes"] = self.client_types + + if self.tunnels: + context_path["Tunnels"] = self.tunnels + + return context + + +""" HELPER FUNCTIONS """ def fix_nested_client(data): @@ -60,105 +90,136 @@ def fix_nested_client(data): return new_dict -''' COMMAND FUNCTIONS ''' +""" COMMAND FUNCTIONS """ def test_module(client: Client) -> 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: client to use - - :return: 'ok' if test passed, anything else will fail the test. - :rtype: ``str`` - """ - message: str = '' + message: str = "" try: - full_url = urljoin(client._base_url, 'status') - demisto.debug(f'SpurContextAPI full_url: {full_url}') + full_url = urljoin(client._base_url, "status") + demisto.debug(f"SpurContextAPI full_url: {full_url}") client._http_request( - method='GET', + method="GET", full_url=full_url, headers=client._headers, raise_on_status=True, ) - message = 'ok' + message = "ok" except DemistoException as e: - if 'Forbidden' in str(e) or 'Authorization' in str(e): # TODO: make sure you capture authentication errors - message = 'Authorization Error: make sure API Key is correctly set' + if "Forbidden" in str(e) or "Authorization" in str(): # TODO: make sure you capture authentication errors + message = "Authorization Error: make sure API Key is correctly set" else: raise e return message def enrich_command(client: Client, args: dict[str, Any]) -> CommandResults: - ip = args.get('ip', None) + ip = args.get("ip", None) if not ip: - raise ValueError('IP not specified') + raise ValueError("IP not specified") response = client.ip(ip) - # Make sure the response is a dictionary - if isinstance(response, dict): + if not isinstance(response, dict): + raise ValueError(f"Invalid response from API: {response}") + + response = fix_nested_client(response) + return CommandResults( + outputs_prefix="SpurContextAPI.Context", + outputs_key_field="", + outputs=response, + raw_response=response, + ) + + +def _build_dbot_score(ip: str) -> Common.DBotScore: + reliability = demisto.params().get("reliability") + return Common.DBotScore( + indicator=ip, + indicator_type=DBotScoreType.IP, + integration_name="SpurContextAPI", + score=Common.DBotScore.NONE, + reliability=reliability, + ) + + +def _build_spur_indicator(ip: str, response: dict) -> SpurIP: + response_as = response.get("as", {}) + response_location = response.get("location", {}) + return SpurIP( + ip=ip, + asn=response_as.get("number"), + as_owner=response_as.get("organization"), + dbot_score=_build_dbot_score(ip), + organization_name=response.get("organization"), + geo_country=response_location.get("country"), + risks=response.get("risks"), + client_types=response.get("client_types"), + tunnels=response.get("tunnels"), + ) + + +def ip_command(client: Client, args: dict[str, Any]) -> list[CommandResults]: + ips = argToList(args["ip"]) + + results: List[CommandResults] = [] + + for ip in ips: + response = client.ip(ip) + + if not isinstance(response, dict): + raise ValueError(f"Invalid response from API: {response}") + response = fix_nested_client(response) - return CommandResults( - outputs_prefix='SpurContextAPI.Context', - outputs_key_field='', + + results.append(CommandResults( + outputs_prefix="SpurContextAPI.Context", + outputs_key_field="", outputs=response, raw_response=response, - ) - else: - raise ValueError(f'Invalid response from API: {response}') + indicator=_build_spur_indicator(ip, response), + )) + + return results -''' MAIN FUNCTION ''' +""" MAIN FUNCTION """ def main() -> None: - """main function, parses params and runs command functions + api_key = demisto.params().get("credentials", {}).get("password") + base_url = demisto.params().get("base_url") + verify_certificate = not demisto.params().get("insecure", False) + proxy = demisto.params().get("proxy", False) + demisto.debug(f"Command being called is {demisto.command()}") - :return: - :rtype: - """ + command = demisto.command() + demisto.debug(f"Command being called is {command}") - api_key = demisto.params().get('credentials', {}).get('password') - base_url = demisto.params().get('base_url') - verify_certificate = not demisto.params().get('insecure', False) - proxy = demisto.params().get('proxy', False) - demisto.debug(f'Command being called is {demisto.command()}') try: - - headers: dict = { - "TOKEN": api_key - } + headers: dict = {"TOKEN": api_key} client = Client( - base_url=base_url, - verify=verify_certificate, - headers=headers, - proxy=proxy) + base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy + ) - if demisto.command() == 'test-module': - # This is the call made when pressing the integration Test button. - result = test_module(client) - return_results(result) + command = demisto.command() - elif demisto.command() == 'spur-context-api-enrich': + if command == "test-module": + return_results(test_module(client)) + elif command == "ip": + return_results(ip_command(client, demisto.args())) + elif command == "spur-context-api-enrich": return_results(enrich_command(client, demisto.args())) - # Log exceptions and return errors except Exception: - return_error(f'Error: {traceback.format_exc()}') + return_error(f"Error: {traceback.format_exc()}") -''' ENTRY POINT ''' +""" ENTRY POINT """ -if __name__ in ('__main__', '__builtin__', 'builtins'): +if __name__ in ("__main__", "__builtin__", "builtins"): main() diff --git a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.yml b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.yml index a709bbbc5e00..dd4f6364d46a 100644 --- a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.yml +++ b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.yml @@ -13,6 +13,20 @@ configuration: name: credentials required: true type: 9 +- additionalinfo: Reliability of the source providing the intelligence data. + defaultvalue: B - Usually reliable + display: Source Reliability + name: reliability + options: + - A+ - 3rd party enrichment + - A - Completely reliable + - B - Usually reliable + - C - Fairly reliable + - D - Not usually reliable + - E - Unreliable + - F - Reliability cannot be judged + required: false + type: 15 - defaultvalue: 'false' display: Use system proxy settings name: proxy @@ -75,6 +89,98 @@ script: - contextPath: SpurContextAPI.Context.client_types type: array description: The different type of client devices that we have observed on this IP address. + - name: ip + description: 'IP reputation command using the Spur Context API.' + arguments: + - name: ip + required: true + description: IP address to enrich. + isArray: true + outputs: + - contextPath: DBotScore.Score + description: The actual score. + type: string + - contextPath: DBotScore.Indicator + description: The indicator that was tested. + type: string + - contextPath: DBotScore.Type + description: The indicator type. + type: string + - contextPath: DBotScore.Vendor + description: The vendor used to calculate the score. + type: string + - contextPath: DBotScore.Reliability + description: Reliability of the source providing the intelligence data. + type: String + - contextPath: IP.Address + description: IP address. + type: string + - contextPath: IP.ASN + description: 'The autonomous system name for the IP address, for example: "AS8948".' + type: string + - contextPath: IP.ASOwner + description: The autonomous system owner of the IP. + type: String + - contextPath: IP.ClientTypes + description: The organization name. + type: array + - contextPath: IP.Geo.Country + description: The country in which the IP address is located. + type: string + - contextPath: IP.Organization.Name + description: The organization name. + type: string + - contextPath: IP.Risks + description: Risks that we have determined based on our collection of data. + type: array + - contextPath: IP.Tunnels + description: The different types of proxy or VPN services that are running on this IP address. + type: array + - contextPath: SpurContextAPI.Context.ip + type: string + description: IP that was enriched. + - contextPath: SpurContextAPI.Context.as + type: object + description: Autonomous System details for an IP Address. + - contextPath: SpurContextAPI.Context.organization + type: string + description: The organization using this IP address. + - contextPath: SpurContextAPI.Context.infrastructure + type: string + description: The primary infracstructure type that this IP address supports. Common tags are MOBILE and DATACENTER. + - contextPath: SpurContextAPI.Context.location + type: object + description: Data-center or IP Hosting location based on MaxMind GeoLite. + - contextPath: SpurContextAPI.Context.services + type: array + description: The different types of proxy or VPN services that are running on this IP address. + - contextPath: SpurContextAPI.Context.tunnels + type: array + description: Different VPN or proxy tunnels that are currently in-use on this IP address. + - contextPath: SpurContextAPI.Context.risks + type: array + description: Risks that we have determined based on our collection of data. + - contextPath: SpurContextAPI.Context.client_concentration + type: object + description: The strongest location concentration for clients using this IP address. + - contextPath: SpurContextAPI.Context.client_countries + type: number + description: The number of countries that we have observed clients located in for this IP address. + - contextPath: SpurContextAPI.Context.client_spread + type: number + description: The total geographic area in kilometers where we have observed users. + - contextPath: SpurContextAPI.Context.client_proxies + type: array + description: The different types of callback proxies we have observed on clients using this IP address. + - contextPath: SpurContextAPI.Context.client_count + type: number + description: The average number of clients we observe on this IP address. + - contextPath: SpurContextAPI.Context.client_behaviors + type: array + description: An array of behavior tags for an IP Address. + - contextPath: SpurContextAPI.Context.client_types + type: array + description: The different type of client devices that we have observed on this IP address. runonce: false script: '' type: python @@ -82,4 +188,4 @@ script: dockerimage: demisto/python3:3.10.13.89873 fromversion: 6.10.0 tests: -- No tests (auto formatted) +- No tests (auto formatted) \ No newline at end of file diff --git a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI_test.py b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI_test.py index b0ba201be626..945da9060636 100644 --- a/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI_test.py +++ b/Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI_test.py @@ -53,7 +53,7 @@ """ import pytest -from SpurContextAPI import Client, enrich_command, test_module, main +from SpurContextAPI import Client, SpurIP, enrich_command, ip_command, test_module, main, Common, DBotScoreType # Sample API response for testing MOCK_HTTP_RESPONSE = { @@ -111,11 +111,57 @@ def test_enrich_command(client): assert result.outputs['ip'] == MOCK_HTTP_RESPONSE['ip'] +def test_ip_command(client): + args = {'ip': '1.1.1.1'} + results = ip_command(client, args)[0] + assert isinstance(results.indicator, SpurIP) + assert results.indicator.risks == MOCK_HTTP_RESPONSE['risks'] + + def test_test_module(client): result = test_module(client) assert result == 'ok' +def test_spur_ip_to_context(): + ip = '1.1.1.1' + asn = 'AS12345' + as_owner = 'Test AS' + client_types = ['MOBILE', 'DESKTOP'] + risks = ['WEB_SCRAPING', 'TUNNEL'] + tunnels = { + 'type': 'VPN', + 'operator': 'NORD_VPN', + 'anonymous': True, + 'entries': ['1.1.1.1'], + 'exits': ['1.1.1.1'] + } + ip_indicator = SpurIP( + ip=ip, + asn=asn, + as_owner=as_owner, + client_types=client_types, + dbot_score=Common.DBotScore( + indicator=ip, + indicator_type=DBotScoreType.IP, + integration_name="SpurContextAPI", + score=Common.DBotScore.NONE, + ), + risks=risks, + tunnels=tunnels + ) + + context = ip_indicator.to_context() + context_path = context[Common.IP.CONTEXT_PATH] + + assert context_path['Address'] == ip + assert context_path['ASN'] == asn + assert context_path['ASOwner'] == as_owner + assert context_path['Risks'] == risks + assert context_path['ClientTypes'] == client_types + assert context_path['Tunnels'] == tunnels + + def test_main_enrich_command(mocker): mocker.patch('SpurContextAPI.demisto.command', return_value='spur-context-api-enrich') mocker.patch('SpurContextAPI.demisto.args', return_value={'ip': '1.1.1.1'}) diff --git a/Packs/SpurContextAPI/ReleaseNotes/1_0_2.md b/Packs/SpurContextAPI/ReleaseNotes/1_0_2.md new file mode 100644 index 000000000000..9bb075e14b5f --- /dev/null +++ b/Packs/SpurContextAPI/ReleaseNotes/1_0_2.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### SpurContextAPI + +Added the ***ip*** reputation command. \ No newline at end of file diff --git a/Packs/SpurContextAPI/pack_metadata.json b/Packs/SpurContextAPI/pack_metadata.json index b90e0143d13c..10bf19a0ca3e 100644 --- a/Packs/SpurContextAPI/pack_metadata.json +++ b/Packs/SpurContextAPI/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Spur Context API", "description": "Enrich IP addresses with data from the Spur Context API", "support": "partner", - "currentVersion": "1.0.1", + "currentVersion": "1.0.2", "author": "Spur Intelligence Corporation", "url": "https://spur.us/contact/", "email": "support@spur.us", @@ -24,4 +24,4 @@ "githubUser": [ "jjunqueira" ] -} +} \ No newline at end of file