Skip to content

Commit

Permalink
IP Command for the Spur Context API Integration (demisto#36466) (demi…
Browse files Browse the repository at this point in the history
…sto#36633)

* Added the IP reputation command

* Added test for the IP repuration command

* Cleanup

* Updated version and README

* Fixed output descriptions

* Updated README with updated output descriptions

* Updated release notes

* Added myself to CONTRIBUTORS.json

* Updated release notes

* Some cleanup

* Minor fixes

* Added test for spur ip to_context

* Make IP command work on an array of IPs

* Try to use if elif instead of match to now confuse the pre-commit workflow

* Try to make pre-commit happy

* No newline at end of file

* Apply suggestions from code review



* Some cleanup for consistency

* Added newline to end of file

* Update Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py



* Update Packs/SpurContextAPI/ReleaseNotes/1_0_2.md



* Add back missing try statement

* pre-commit fix

---------

Co-authored-by: defendable-sokrates <[email protected]>
Co-authored-by: Yaakov Praisler <[email protected]>
Co-authored-by: yaakovpraisler <[email protected]>
  • Loading branch information
4 people authored Oct 7, 2024
1 parent f09d68a commit 4271942
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 74 deletions.
5 changes: 3 additions & 2 deletions Packs/SpurContextAPI/CONTRIBUTORS.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[
"Fabio Dias"
]
"Fabio Dias",
"Sokrates Hillestad"
]
58 changes: 55 additions & 3 deletions Packs/SpurContextAPI/Integrations/SpurContextAPI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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. |

191 changes: 126 additions & 65 deletions Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,67 @@
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,
)

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):
Expand All @@ -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()
Loading

0 comments on commit 4271942

Please sign in to comment.