Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IP Command for the Spur Context API Integration #36466

Open
wants to merge 27 commits into
base: contrib/defendable-sokrates_spur_context_api_ip_command
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7b087c9
Added the IP reputation command
defendable-sokrates Sep 24, 2024
e19e84f
Added test for the IP repuration command
defendable-sokrates Sep 24, 2024
4aca6ae
Cleanup
defendable-sokrates Sep 24, 2024
24f3b3e
Updated version and README
defendable-sokrates Sep 24, 2024
ce83d80
Fixed output descriptions
defendable-sokrates Sep 24, 2024
62a3cfb
Updated README with updated output descriptions
defendable-sokrates Sep 24, 2024
ec359aa
Merge branch 'demisto:master' into spur_context_api_ip_command
defendable-sokrates Sep 24, 2024
5de84ed
Updated release notes
defendable-sokrates Sep 24, 2024
b06e7ee
Added myself to CONTRIBUTORS.json
defendable-sokrates Sep 24, 2024
332b325
Updated release notes
defendable-sokrates Sep 24, 2024
b50a893
Merge branch 'contrib/defendable-sokrates_spur_context_api_ip_command…
defendable-sokrates Sep 24, 2024
10aadb7
Some cleanup
defendable-sokrates Sep 24, 2024
aa96ceb
Minor fixes
defendable-sokrates Sep 25, 2024
93cf86e
Added test for spur ip to_context
defendable-sokrates Sep 25, 2024
1d582bf
Merge branch 'contrib/defendable-sokrates_spur_context_api_ip_command…
defendable-sokrates Sep 26, 2024
657efb3
Merge branch 'contrib/defendable-sokrates_spur_context_api_ip_command…
defendable-sokrates Sep 30, 2024
b14ef15
Make IP command work on an array of IPs
defendable-sokrates Sep 30, 2024
faf4a02
Try to use if elif instead of match to now confuse the pre-commit wor…
defendable-sokrates Sep 30, 2024
eecd033
Merge branch 'contrib/defendable-sokrates_spur_context_api_ip_command…
defendable-sokrates Sep 30, 2024
9b47715
Try to make pre-commit happy
defendable-sokrates Sep 30, 2024
eecb667
No newline at end of file
defendable-sokrates Sep 30, 2024
9ca8224
Apply suggestions from code review
defendable-sokrates Oct 1, 2024
aa757f6
Merge branch 'contrib/defendable-sokrates_spur_context_api_ip_command…
defendable-sokrates Oct 1, 2024
41af9a2
Some cleanup for consistency
defendable-sokrates Oct 1, 2024
9e66f5f
Added newline to end of file
defendable-sokrates Oct 1, 2024
c53de52
Update Packs/SpurContextAPI/Integrations/SpurContextAPI/SpurContextAP…
defendable-sokrates Oct 1, 2024
d5f03c2
Update Packs/SpurContextAPI/ReleaseNotes/1_0_2.md
defendable-sokrates Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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. |

192 changes: 126 additions & 66 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,135 @@ 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

:return:
:rtype:
"""
def main() -> None:
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()}")

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:
command = demisto.command()
demisto.debug(f"Command being called is {command}")

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