Axonius¶
Table of Contents¶
Release Notes¶
Version |
Date |
Notes |
---|---|---|
1.0.0 |
3/2024 |
Initial Release |
Overview¶
Axonius Cybersecurity Asset Management
This app provide Axonius device data enrichment to SOAR cases.
Provide Axonius device data enrichment to SOAR cases.
Key Features¶
Retrieve information on Axonius on device assets from artifacts in SOAR and add as a row in the Axonius Devices data table in a SOAR case. The following SOAR artifact types can be used to query devices in Axonius:
DNS Name
Email Recipient
Email Sender
IP Address
MAC Address
Service
Query Axonius devices using an AQL (Axonius Query Language) string or a saved query name defined in Axonius and to add to the Axonius Devices data table or to a note or attachment in SOAR.
Get the device count of an AQL device query.
Access Axonius device data via live links from Axonius Devices data table in SOAR case.
Write Axonius device JSON data to an attachment or a SOAR note from the Axonius Devices data table in SOAR.
Refetch Axonius device information from the Axonius Devices data table to compare device query results over time.
Create the following types of artifacts from a row in the Axonius Devices data table
IP Address
Service
System Name
Run Axonius Enforcements by Name from a SOAR case.
Requirements¶
This app supports the IBM Security QRadar SOAR Platform and the IBM Security QRadar SOAR for IBM Cloud Pak for Security.
SOAR platform¶
The SOAR platform supports two app deployment mechanisms, Edge Gateway (also known as App Host) and integration server.
If deploying to a SOAR platform with an App Host, the requirements are:
SOAR platform >=
50.0.0
.The app is in a container-based format (available from the AppExchange as a
zip
file).
If deploying to a SOAR platform with an integration server, the requirements are:
SOAR platform >=
50.0.0
.The app is in the older integration format (available from the AppExchange as a
zip
file which contains atar.gz
file).Integration server is running
resilient-circuits>=51.0.0.2.0
.If using an API key account, make sure the account provides the following minimum permissions:
Name
Permissions
Org Data
Read
Function
Read
Edit Incidents
Fields
Layouts
Read
Layouts
Edit
The following SOAR platform guides provide additional information:
Edge Gateway Deployment Guide or App Host Deployment Guide: provides installation, configuration, and troubleshooting information, including proxy server settings.
Integration Server Guide: provides installation, configuration, and troubleshooting information, including proxy server settings.
System Administrator Guide: provides the procedure to install, configure and deploy apps.
The above guides are available on the IBM Documentation website at ibm.biz/soar-docs. On this web page, select your SOAR platform version. On the follow-on page, you can find the Edge Gateway Deployment Guide, App Host Deployment Guide, or Integration Server Guide by expanding Apps in the Table of Contents pane. The System Administrator Guide is available by expanding System Administrator.
Cloud Pak for Security¶
If you are deploying to IBM Cloud Pak for Security, the requirements are:
IBM Cloud Pak for Security >=
1.10.15
.Cloud Pak is configured with an Edge Gateway.
The app is in a container-based format (available from the AppExchange as a
zip
file).
The following Cloud Pak guides provide additional information:
Edge Gateway Deployment Guide or App Host Deployment Guide: provides installation, configuration, and troubleshooting information, including proxy server settings. From the Table of Contents, select Case Management and Orchestration & Automation > Orchestration and Automation Apps.
System Administrator Guide: provides information to install, configure, and deploy apps. From the IBM Cloud Pak for Security IBM Documentation table of contents, select Case Management and Orchestration & Automation > System administrator.
These guides are available on the IBM Documentation website at ibm.biz/cp4s-docs. From this web page, select your IBM Cloud Pak for Security version. From the version-specific IBM Documentation page, select Case Management and Orchestration & Automation.
Proxy Server¶
The app does support a proxy server.
Python Environment¶
Python 3.6, Python 3.9 and Python 3.11 are supported. Additional package dependencies may exist for each of these packages:
resilient-circuits>=51.0.0.2.0
Axonius Development Version¶
This app has been implemented using:
Product Name |
Product Version |
API URL |
API Version |
---|---|---|---|
Axonius |
6_1_6_4 |
NA |
v2 |
Prerequisites¶
Axonius user account with an API key and secret.
Configuration¶
Get the API Key and Secret in Axonius¶
Click on the
User Settings
in the lower left corner of the Axonius consoleClick on the
API Key
tab to copy the API key and secret.
Installation¶
Install¶
To install or uninstall an App or Integration on the SOAR platform, see the documentation at ibm.biz/soar-docs.
To install or uninstall an App on IBM Cloud Pak for Security, see the documentation at ibm.biz/cp4s-docs and follow the instructions above to navigate to Orchestration and Automation.
App Configuration¶
The following table provides the settings you need to configure the app. These settings are made in the app.config file. See the documentation discussed in the Requirements section for the procedure.
Config |
Required |
Example |
Description |
---|---|---|---|
api_key |
Yes |
|
Axonius API Key. |
api_secret |
Yes |
|
Axonius API Secret. |
api_version |
Yes |
|
REST API version. |
endpoint_url |
Yes |
|
Axonius Platform URL. |
Function - Axonius: Get Device By ID¶
Get information on the Axonius device given an internal Axon ID. Optionally write the results to an incident or task attachment.
NOTE: The the JSON object returned from this function is very large, so the example playbook that uses this function writes the data to an attachment rather than a note.
Inputs:
Name |
Type |
Required |
Example |
Tooltip |
---|---|---|---|---|
|
|
No |
|
Name of the attachment to create in SOAR |
|
|
Yes |
|
- |
|
|
Yes |
|
Axonius Internal Axon ID |
|
|
No |
|
Task to which attachment is attached. |
|
|
Yes |
|
Indicate whether the device JSON returned is written to an incident attachment. |
Outputs:
NOTE: This example might be in JSON format, but
results
is a Python Dictionary on the SOAR platform.
results = {
"content": {
"attachment_name": "Axonius-Device-Shirley-Quilliams-iMac.json"
},
"inputs": {
"axonius_attachment_name": "Axonius-Device-Shirley-Quilliams-iMac.json",
"axonius_incident_id": 2144,
"axonius_internal_axon_id": "0de1ff00569723b2d11ec84665c4bd06",
"axonius_task_id": null,
"axonius_write_attachment": true
},
"metrics": {
"execution_time_ms": 92192,
"host": "mylaptop",
"package": "fn-axonius",
"package_version": "1.0.0",
"timestamp": "2024-03-12 13:24:06",
"version": "1.0"
},
"raw": null,
"reason": null,
"success": true,
"version": 2.0
}
Example Function Input Script:
inputs.axonius_internal_axon_id = row.axonius_id
inputs.axonius_write_attachment = True
inputs.axonius_attachment_name = "Axonius-Device-{0}.json".format(row.axonius_name)
inputs.axonius_incident_id = incident.id
inputs.axonius_task_id = None
Example Function Post Process Script:
results = playbook.functions.results.get_device_results
inputs = results.get("inputs", None)
axonius_internal_axon_id = inputs.get("axonius_internal_axon_id", None)
if results.get("success", False):
content = results.get("content", None)
if content:
attachment_name = content.get("attachment_name", None)
note_text = f"<b>Axonius: Write Device JSON to Attachment:</b> wrote attachment: <b>{attachment_name}</b>."
else:
note_text = f"<b>Axonius: Write Device JSON to Attachment:</b> attachment was not written (no content) for {axonius_internal_axon_id}."
else:
reason = results.get("reason", None)
note_text = f"<b>Axonius: Write Device JSON to Attachment:</b> attachment was not written {reason}<br> for ID {axonius_internal_axon_id}."
incident.addNote(note_text)
Function - Axonius: Get Device by Query¶
Get the devices in Axonius that match the specified Axonius (AQL) query string. Optionally specify the list of Axonius field names to return for each device. If no fields names are specified, a default list of field names is returned . Optionally specify the maximum number of devices to return. If no limit it specified, the Axonius limit of 1000 is used in the query. Optional parameters allow for writing the results to an incident or task attachment.
Activation form for the manual playbook Axonius: Query Devices
:
Inputs:
Name |
Type |
Required |
Example |
Tooltip |
---|---|---|---|---|
|
|
No |
|
Name of the attachment to create in SOAR |
|
|
No |
|
Maximum number of devices to return from the query. |
|
|
No |
|
- |
|
|
Yes |
|
- |
|
|
No |
|
Axonius Query string |
|
|
No |
|
An Axonius Saved Query Name that identifies a query of devices in Axonius. |
|
|
No |
|
Task to which attachment is attached. |
|
|
Yes |
|
Indicate whether the device JSON returned is written to an incident attachment. |
Outputs:
NOTE: This example might be in JSON format, but
results
is a Python Dictionary on the SOAR platform.
results = {
"content": {
"assets": [
{
"adapter_list_length": 12,
"adapters": [
"cisco_meraki_adapter",
"counter_act_adapter",
"crowd_strike_adapter",
"deep_security_adapter",
"epo_adapter",
"google_mdm_adapter",
"paloalto_panorama_adapter",
"sccm_adapter",
"service_now_adapter",
"tanium_adapter",
"tanium_asset_adapter",
"zoom_adapter"
],
"internal_axon_id": "ca1df3031a00e387c8a7da086272f2b6",
"labels": [
"Shirley"
],
"specific_data.data.hostname_preferred": "DESKTOP-shirleyc",
"specific_data.data.last_used_users": [
"shirley.cook@demo.local",
"van.greer@demo.local",
"shirley.cook@demo.local",
"van.greer@demo.local",
"shirley.cook@demo.local",
"van.greer@demo.local",
"shirley.cook@demo.local",
"van.greer@demo.local"
],
"specific_data.data.last_used_users_departments_association": [
"Legal",
"Product"
],
"specific_data.data.last_used_users_mail_association": [
"shirley.cook@demo.local",
"van.greer@demo.local"
],
"specific_data.data.name": [
"DESKTOP-shirleyc",
"DESKTOP-shirleyc",
"DESKTOP-shirleyc",
"DESKTOP-shirleyc",
"DESKTOP-shirleyc",
"DESKTOP-shirleyc"
],
"specific_data.data.network_interfaces.ips_preferred": [
"10.0.50.111"
],
"specific_data.data.os.type_distribution": [
"Windows XP",
"Windows XP",
"Windows XP",
"Windows XP",
"Windows XP",
"Windows XP",
"Windows XP"
]
}
],
"device_count": 1,
"endpoint_url": "https://myaxonius.on.axonius.com"
},
"inputs": {
"axonius_device_limit": 1,
"axonius_field_name_list": "[\"specific_data.data.network_interfaces.ips_preferred\", \"specific_data.data.name\", \"specific_data.data.hostname_preferred\", \"specific_data.data.owner\", \"specific_data.data.last_used_users_mail_association\", \"specific_data.data.os.type_distribution\", \"specific_data.data.last_used_users\", \"specific_data.data.last_used_users_departments_association\", \"specific_data.data.hard_drives.encryption_status\", \"specific_data.data.device_disabled\", \"specific_data.data.network_interfaces.security_level_preferred\", \"specific_data.data.network_interfaces.region_preferred\", \"specific_data.data.network_interfaces.country_preferred\", \"labels\"]",
"axonius_incident_id": 2144,
"axonius_query_string": "(\"specific_data.data.hostname\" == \"DESKTOP-shirleyc\")",
"axonius_saved_query_name": null,
"axonius_task_id": null,
"axonius_write_attachment": false
},
"metrics": {
"execution_time_ms": 24906,
"host": "mylaptop",
"package": "fn-axonius",
"package_version": "1.0.0",
"timestamp": "2024-03-21 16:41:51",
"version": "1.0"
},
"raw": null,
"reason": null,
"success": true,
"version": 2.0
}
Example Function Input Script:
from json import (dumps)
artifact_map = {
"Email Sender": f"(\"specific_data.data.last_used_users\" == \"{artifact.value}\")",
"Email Recipient": f"(\"specific_data.data.last_used_users\" == \"{artifact.value}\")",
"IP Address": f"(\"specific_data.data.network_interfaces.ips\" == \"{artifact.value}\")",
"MAC Address": f"(\"specific_data.data.network_interfaces.mac\" == \"{artifact.value}\")",
"DNS Name": f"(\"specific_data.data.hostname\" == \"{artifact.value}\")",
"System Name": f"(\"specific_data.data.hostname\" == \"{artifact.value}\")",
"Service": f"(\"specific_data.data.os.type_distribution\" == \"{artifact.value}\") or (\"specific_data.data.os.type\" == \"{artifact.value}\")"
}
inputs.axonius_query_string = artifact_map.get(artifact.type, None)
inputs.axonius_saved_query_name = None
if not inputs.axonius_query_string:
helper.fail("Artifact Type is not supported for this playbook")
field_names = ["specific_data.data.network_interfaces.ips_preferred",
"specific_data.data.name",
"specific_data.data.hostname_preferred",
"specific_data.data.owner",
"specific_data.data.last_used_users_mail_association",
"specific_data.data.os.type_distribution",
"specific_data.data.last_used_users",
"specific_data.data.last_used_users_departments_association",
"specific_data.data.hard_drives.encryption_status",
"specific_data.data.device_disabled",
"specific_data.data.network_interfaces.security_level_preferred",
"specific_data.data.network_interfaces.region_preferred",
"specific_data.data.network_interfaces.country_preferred",
"labels"]
inputs.axonius_field_name_list = dumps(field_names)
inputs.axonius_device_limit = getattr(playbook.inputs, "axonius_limit", None) if getattr(playbook.inputs, "axonius_limit", None) else 1
inputs.axonius_incident_id = incident.id
inputs.axonius_write_attachment = False
inputs.axonius_task_id = None
Example Function Post Process Script:
from datetime import datetime
DEVICE_URL = "{endpoint_url}/assets/devices/{axonius_id}/asset-profile/all"
def get_non_null_item_from_list(item_list):
# Return the first non-null item from the list, if any.
if isinstance(item_list, list):
non_null_item = next((item for item in item_list if item is not None), None)
return non_null_item
elif isinstance(item_list, str):
return item_list
else:
return None
def unique_list_items_to_csv_string(item_list):
# Return a comma separated list of unique strings from a list of strings.
if isinstance(item_list, list):
unique_items = list(set(item_list))
return ", ".join(unique_items)
elif isinstance(item_list, str):
return item_list
else:
return None
results = playbook.functions.results.get_device_results
inputs = results.get("inputs", None)
query_string = inputs.get("axonius_query_string", None) if inputs else None
saved_query_name = inputs.get("axonius_saved_query_name", None) if inputs else None
axonius_device_limit = inputs.get("axonius_device_limit") if inputs else None
inputs_string = ""
if query_string:
inputs_string = f"{inputs_string}<br><b>Query String:</b> {query_string}"
if saved_query_name:
inputs_string = f"{inputs_string}<br><b>Saved Query Name:</b> {saved_query_name}"
inputs_string = f"{inputs_string}<br><b>Limit:</b> {axonius_device_limit}"
if results.get("success", False):
content = results.get("content", {})
if content:
assets_list = content.get("assets", [])
if assets_list:
for asset in assets_list:
asset_row = incident.addRow("axonius_devices_dt")
asset_row.axonius_query_date = datetime.now()
asset_row.axonius_id = asset.get("internal_axon_id", None)
if content.get("endpoint_url"):
device_url = DEVICE_URL.format(endpoint_url=content.get("endpoint_url"), axonius_id=asset.get("internal_axon_id", None))
asset_row.axonius_link = "<a href='{0}'>Link</a>".format(device_url)
asset_row.axonius_ip = unique_list_items_to_csv_string(asset.get("specific_data.data.network_interfaces.ips_preferred", None))
asset_row.axonius_name = get_non_null_item_from_list(asset.get("specific_data.data.name", None))
asset_row.axonius_hostname = get_non_null_item_from_list(asset.get("specific_data.data.hostname_preferred", None))
asset_row.axonius_owner = unique_list_items_to_csv_string(asset.get("specific_data.data.owner", None))
asset_row.axonius_email = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users_mail_association", None))
asset_row.axonius_os_type_distribution = unique_list_items_to_csv_string(asset.get("specific_data.data.os.type_distribution", None))
asset_row.axonius_last_used_users = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users", None))
asset_row.axonius_last_used_users_dept = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users_departments_association", None))
asset_row.axonius_hard_drives_encryption_status = get_non_null_item_from_list(asset.get("specific_data.data.hard_drives.encryption_status", None))
asset_row.axonius_device_disabled = get_non_null_item_from_list(asset.get("specific_data.data.device_disabled", None))
asset_row.axonius_security_level = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.security_level_preferred", None))
asset_row.axonius_region = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.region_preferred", None))
asset_row.axonius_country = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.country_preferred", None))
asset_row.axonius_tags = unique_list_items_to_csv_string(asset.get("labels", None))
num_assets = len(assets_list)
note_text = f"<b>Axonius: Populate Devices Data Table:</b> Added <b>{num_assets}</b> asset to the Axonius Devices data table {inputs_string}"
else:
note_text = f"<b>Axonius: Populate Devices Data Table:</b> No device asset found matching inputs: {inputs_string}"
else:
note_text = f"Axonius: Populate Devices Data Table: No asset found (no content): {inputs_string}"
else:
reason = results.get("reason", None)
note_text = f"Axonius: Populate Devices Data Table: Failed function to get asset: {inputs_string}<br> Reason = {reason}"
incident.addNote(note_text)
Function - Axonius: Get Device Count¶
Get the number of devices that match the Axonius query.
Activation form for the manual playbook Axonius: Get Device Count
:
Inputs:
Name |
Type |
Required |
Example |
Tooltip |
---|---|---|---|---|
|
|
No |
|
Axonius Query string |
|
|
No |
|
An Axonius Saved Query Name that identifies a query of devices in Axonius. |
Outputs:
NOTE: This example might be in JSON format, but
results
is a Python Dictionary on the SOAR platform.
results = {
"content": {
"value": 5
},
"inputs": {
"axonius_query_string": "(\"specific_data.data.os.type\" == \"Windows\") and (\"specific_data.data.name\" == regex(\"Shirley\", \"i\"))",
"axonius_saved_query_name": null
},
"metrics": {
"execution_time_ms": 7309,
"host": "mylaptop",
"package": "fn-axonius",
"package_version": "1.0.0",
"timestamp": "2024-03-14 14:26:01",
"version": "1.0"
},
"raw": null,
"reason": null,
"success": true,
"version": 2.0
}
Example Function Input Script:
# Fill a default query string or saved query name in case user does not provide inputs.
DEFAULT_QUERY_STRING = None
DEFAULT_QUERY_NAME = None
if getattr(playbook.inputs, "axonius_query_string", None):
inputs.axonius_query_string = playbook.inputs.axonius_query_string.get("content")
else:
inputs.axonius_query_string = DEFAULT_QUERY_STRING if DEFAULT_QUERY_STRING else None
if getattr(playbook.inputs, "axonius_saved_query_name", None):
inputs.axonius_saved_query_name = getattr(playbook.inputs, "axonius_saved_query_name", None)
else:
inputs.axonius_saved_query_name = DEFAULT_QUERY_NAME if DEFAULT_QUERY_NAME else None
if not inputs.axonius_query_string and not inputs.axonius_saved_query_name:
helper.fail("A query string or a saved query_name must be specified")
Example Function Post Process Script:
results = playbook.functions.results.get_device_count_results
inputs = results.get("inputs", None)
query_string = inputs.get("axonius_query_string", None) if inputs else None
saved_query_name = inputs.get("axonius_saved_query_name", None) if inputs else None
inputs_string = ""
if query_string:
inputs_string = f"{inputs_string}<br><b>Query String:</b> {query_string}"
if saved_query_name:
inputs_string = f"{inputs_string}<br><b>Saved Query Name:</b> {saved_query_name}"
if results.get("success", False):
content = results.get("content", {})
if content:
value = content.get("value")
note_text = f"<b>Axonius: Get Device Count:</b> Found <b>{value}</b> devices matching:{inputs_string}"
else:
note_text = f"<b>Axonius: Get Device Count:</b> Found no devices: no content:{inputs_string}"
else:
reason = results.get("reason", None)
note_test = f"<b>Axonius: Get Device Count:</b> Found <b>{value}</b> devices matching:{inputs_string}<br>Reason: {reason}"
incident.addNote(note_text)
Function - Axonius: Run Enforcement Set¶
Run the Axonius enforcement set given an enforcement set name.
Activation form for the manual playbook Axonius: Run Enforcement by Name - Example
:
Inputs:
Name |
Type |
Required |
Example |
Tooltip |
---|---|---|---|---|
|
|
Yes |
|
Axonius Enforcement Set Name to be run. |
Outputs:
NOTE: This example might be in JSON format, but
results
is a Python Dictionary on the SOAR platform.
results = {
"content": {
"value": [
{
"id": "65ef47d95cd7f554699537f0",
"name": "addCustomData_002"
}
]
},
"inputs": {
"axonius_enforcement_set_name": "addCustomData_002"
},
"metrics": {
"execution_time_ms": 28131,
"host": "mylaptopl",
"package": "fn-axonius",
"package_version": "1.0.0",
"timestamp": "2024-03-11 16:57:29",
"version": "1.0"
},
"raw": null,
"reason": null,
"success": true,
"version": 2.0
}
Example Function Input Script:
inputs.axonius_enforcement_set_name = getattr(playbook.inputs, "axonius_enforcement_set_name", None)
if not inputs.axonius_enforcement_set_name:
helper.fail("Axonius Enforcement Set Name must be specified.")
Example Function Post Process Script:
results = playbook.functions.results.run_enforcement_set_results
if results.get("success", False):
content = results.get("content", {})
if content:
values = content.get("value")
note_text = ""
for value in values:
enforcement_name = value.get("name", None)
enforcement_id = value.get("id", None)
new_text = f"<b>Axonius: Run Enforcement by Name:</b> ran Enforcement Name: <b>{enforcement_name}</b><br> ID: {enforcement_id}<br>"
note_text = note_text + new_text
else:
note_text = f"<b>Axonius: Run Enforcement by Name:</b> did not run: no content"
else:
note_text = "<b>Axonius: Run Enforcement by Name:</b> failed:<br> {0}".format(results.get("reason", None))
incident.addNote(note_text)
Script - Axonius: Populate Devices Data Table¶
Add the results returned from querying devices to the Axonius Devices data table.
Object: incident
Script Text:
from datetime import datetime
DEVICE_URL = "{endpoint_url}/assets/devices/{axonius_id}/asset-profile/all"
def get_non_null_item_from_list(item_list):
# Return the first non-null item from the list, if any.
if isinstance(item_list, list):
non_null_item = next((item for item in item_list if item is not None), None)
return non_null_item
elif isinstance(item_list, str):
return item_list
else:
return None
def unique_list_items_to_csv_string(item_list):
# Return a comma separated list of unique strings from a list of strings.
if isinstance(item_list, list):
unique_items = list(set(item_list))
return ", ".join(unique_items)
elif isinstance(item_list, str):
return item_list
else:
return None
results = playbook.functions.results.get_device_results
inputs = results.get("inputs", None)
query_string = inputs.get("axonius_query_string", None) if inputs else None
saved_query_name = inputs.get("axonius_saved_query_name", None) if inputs else None
axonius_device_limit = inputs.get("axonius_device_limit") if inputs else None
inputs_string = ""
if query_string:
inputs_string = f"{inputs_string}<br><b>Query String:</b> {query_string}"
if saved_query_name:
inputs_string = f"{inputs_string}<br><b>Saved Query Name:</b> {saved_query_name}"
inputs_string = f"{inputs_string}<br><b>Limit:</b> {axonius_device_limit}"
if results.get("success", False):
content = results.get("content", {})
if content:
assets_list = content.get("assets", [])
if assets_list:
for asset in assets_list:
asset_row = incident.addRow("axonius_devices_dt")
asset_row.axonius_query_date = datetime.now()
asset_row.axonius_id = asset.get("internal_axon_id", None)
if content.get("endpoint_url"):
device_url = DEVICE_URL.format(endpoint_url=content.get("endpoint_url"), axonius_id=asset.get("internal_axon_id", None))
asset_row.axonius_link = "<a href='{0}'>Link</a>".format(device_url)
asset_row.axonius_ip = unique_list_items_to_csv_string(asset.get("specific_data.data.network_interfaces.ips_preferred", None))
asset_row.axonius_name = get_non_null_item_from_list(asset.get("specific_data.data.name", None))
asset_row.axonius_hostname = get_non_null_item_from_list(asset.get("specific_data.data.hostname_preferred", None))
asset_row.axonius_owner = unique_list_items_to_csv_string(asset.get("specific_data.data.owner", None))
asset_row.axonius_email = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users_mail_association", None))
asset_row.axonius_os_type_distribution = unique_list_items_to_csv_string(asset.get("specific_data.data.os.type_distribution", None))
asset_row.axonius_last_used_users = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users", None))
asset_row.axonius_last_used_users_dept = unique_list_items_to_csv_string(asset.get("specific_data.data.last_used_users_departments_association", None))
asset_row.axonius_hard_drives_encryption_status = get_non_null_item_from_list(asset.get("specific_data.data.hard_drives.encryption_status", None))
asset_row.axonius_device_disabled = get_non_null_item_from_list(asset.get("specific_data.data.device_disabled", None))
asset_row.axonius_security_level = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.security_level_preferred", None))
asset_row.axonius_region = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.region_preferred", None))
asset_row.axonius_country = get_non_null_item_from_list(asset.get("specific_data.data.network_interfaces.country_preferred", None))
asset_row.axonius_tags = unique_list_items_to_csv_string(asset.get("labels", None))
num_assets = len(assets_list)
note_text = f"<b>Axonius: Populate Devices Data Table:</b> Added <b>{num_assets}</b> asset to the Axonius Devices data table {inputs_string}"
else:
note_text = f"<b>Axonius: Populate Devices Data Table:</b> No device asset found matching inputs: {inputs_string}"
else:
note_text = f"Axonius: Populate Devices Data Table: No asset found (no content): {inputs_string}"
else:
reason = results.get("reason", None)
note_text = f"Axonius: Populate Devices Data Table: Failed function to get asset: {inputs_string}<br> Reason = {reason}"
incident.addNote(note_text)
Script - Convert JSON to rich text v1.3¶
This script converts a json object into a hierarchical display of rich text and adds the rich text to an incident’s rich text (custom) field or an incident note. A workflow property is used to share the json to convert and identify parameters used on how to perform the conversion. Typically, a function will create workflow property and this script will run after that function to perform the conversion.
Features:
Display the hierarchical nature of json, presenting the json keys (sorted if specified) as bold labels
Provide links to found URLs
Create either an incident note or add results to an incident (custom) rich text field.
Object: incident
Script Text:
# (c) Copyright IBM Corp. 2010, 2023. All Rights Reserved.
VERSION = 1.3
"""
This script converts a json object into a hierarchical display of rich text and adds the rich text to an incident's rich text (custom) field or an incident note.
A workflow property is used to define the json to convert and identify parameters used on how to perform the conversion.
Typically, a function will create workflow property and this script will run after that function to perform the conversion.
Features:
* Display the hierarchical nature of json, presenting the json keys as bold labels
* Provide links to found URLs
* Create either an incident note or add results to an incident (custom) rich text field.
In order to use this script, define a workflow property called: convert_json_to_rich_text, to define the json and parameters to use for the conversion.
Workflow properties can be added using a command similar to this:
workflow.addProperty('convert_json_to_rich_text', {
"version": 1.3,
"header": "Artifact scan results for: {}".format(artifact.value),
"padding": 10,
"separator": u"<br />",
"sort": True,
"json": results.content,
"json_omit_list": ["omit"],
"incident_field": None
})
Format of workflow.property.convert_json_to_rich_text:
{
"version": 1.3, [this is for future compatibility]
"header": str, [header line to add to converted json produced or None. Ex: Results from scanning artifact: xxx. The header may contain rich text tags]
"padding": 10, [padding for nested json elements, or defaults to 10]
"separator": u"<br />"|list such as ['<span>','</span>'], [html separator between json keys and lists or defaults to html break: '<br />'.
If a list, then the data is brackets by the pair specified]
"sort": True|False, [sort the json keys at each level when displayed]
"json": json, [required json to convert]
"json_omit_list": [list of json keys to exclude or None]
"incident_field": "<incident_field>" [indicates a builtin rich text incident field, such as 'description'
or a custom rich text field in the format: 'properties.<field>'. default: create an incident note]
}
For playbooks, use playbook.addProperty() with the same format as workflow.addProperty()
Playbooks can also use playbook.functions.results.convert_json_to_rich_text using the standard function output which contains the 'content' json element.
When using playbook.functions.results.convert_json_to_rich_text with standard function results, all the defaults for padding, separator, etc. are used.
"""
import re
# needed for python 3
try:
unicode("abc") # fails in py3
py2 = True
except:
unicode = str
py2 = False
rc = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+#\?]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
class ConvertJson:
"""Class to hold the conversion parameters and perform the conversion"""
def __init__(self, omit_keys=[], padding=10, separator=u"<br />", sort_keys=False):
self.omit_keys = omit_keys
self.padding = padding
self.separator = separator
self.sort_keys = sort_keys
def format_link(self, item):
"""[summary]
Find embedded urls (http(s)) and add html anchor tags to display as links
Args:
item ([string])
Returns:
[str]: None|original text if no links|text with html links
"""
formatted_item = item
if py2:
num_type = bool(item and isinstance(item, (int, long, bool, float)))
else:
num_type = bool(item and isinstance(item, (int, bool, float)))
if item and not num_type:
list = rc.findall(item)
if list:
for link in list:
formatted_item = formatted_item.replace(link, u"<a target='blank' href='{0}'>{0}</a>".format(link))
return formatted_item
def expand_list(self, list_value, is_list=False):
"""[summary]
convert items to html, adding indents to nested dictionaries.
Args:
list_value ([dict|list]): json element
Returns:
[str]: html converted code
"""
if not isinstance(list_value, list):
return self.format_link(list_value)
elif not list_value:
return u"None<br>"
try:
items_list = [] # this will ensure list starts on second line of key label
for item in list_value:
if isinstance(item, dict):
result = self.convert_json_to_rich_text(item)
if is_list:
items_list.append(u"<li>{}</li>".format(result))
else:
items_list.append(result)
elif isinstance(item, list):
items_list.append(self.expand_list(item, is_list=True))
elif is_list:
items_list.append(u"<li>{}</li>".format(self.format_link(unicode(item))))
else:
items_list.append(self.format_link(unicode(item)))
expand_list_result = self.add_separator(self.separator if not is_list else u"",
items_list,
is_list=is_list)
if is_list:
return u"<ul>{}</ul>".format(expand_list_result)
else:
return u"<div style='padding:5px'>{}</div>".format(expand_list_result)
except Exception as err:
return str(err)
def convert_json_to_rich_text(self, sub_dict):
"""[summary]
Walk dictionary tree and convert to html for better display
Args:
sub_dict ([type]): [description]
Returns:
[type]: [description]
"""
notes = []
if sub_dict and isinstance(sub_dict, (list, dict)):
if isinstance(sub_dict, list):
expanded_list = self.expand_list(sub_dict, is_list=True)
notes.append(self.add_separator(self.separator, expanded_list))
else:
keys = sorted (sub_dict.keys()) if self.sort_keys else sub_dict.keys()
for key in keys:
if key not in self.omit_keys:
value = sub_dict[key]
is_list = isinstance(value, list)
item_list = [u"<strong>{0}</strong>: ".format(key)]
if isinstance(value, dict):
convert_result = self.convert_json_to_rich_text(value)
if convert_result:
item_list.append(u"<div style='padding:{}px'>{}</div>".format(self.padding, convert_result))
else:
item_list.append(u"None<br>")
else:
item_list.append(self.expand_list(value, is_list=is_list))
notes.append(self.add_separator(self.separator, u"".join(make_unicode(v) for v in item_list), is_list=is_list))
result_notes = u"".join(notes)
if isinstance(self.separator, list):
return result_notes
else:
return result_notes.replace(
u"</div>{0}".format(self.separator), u"</div>").replace(
u"{0}</div>".format(self.separator), u"</div>"
) # tighten up result
def add_separator(self, separator, items, is_list=False):
"""
apply the separator to the data
:param separator: None, str or list such as ['<span>', '</span>']
:param items: str or list to add separator
:return: text with separator applied
"""
_items = items
if not _items:
return "<br>"
if not isinstance(_items, list):
_items = [_items]
if isinstance(separator, list):
return u"".join([u"{}{}{}".format(separator[0], item, separator[1]) for item in _items])
return u"{}{}".format(separator.join(_items), separator if not is_list else u"")
def make_unicode(value):
if value is None:
return 'None'
return unicode(value)
def get_results(property_name):
if playbook and playbook.functions.results[property_name] is not None:
return playbook.functions.results[property_name]
elif playbook and playbook.properties[property_name] is not None:
return playbook.properties[property_name]
elif workflow and workflow.properties[property_name] is not None:
return workflow.properties[property_name]
return None
def get_properties(property_name):
"""
Logic to collect the json and parameters from a workflow property.
Args:
property_name: workflow property to reference
Returns:
padding, separator, header, json_omit_list, incident_field, json, sort_keys
"""
result_properties = get_results(property_name)
if not result_properties:
helper.fail("Playbook/workflow property not found: {}".format(property_name))
padding = int(result_properties.get("padding", 10))
separator = result_properties.get("separator", u"<br />")
if isinstance(separator, list) and len(separator) != 2:
helper.fail("list of separators should be specified as a pair such as ['<div>', '</div>']: {}".format(separator))
header = result_properties.get("header")
sort_keys = bool(result_properties.get("sort", False))
json_omit_list = result_properties.get("json_omit_list")
if not json_omit_list:
json_omit_list = []
incident_field = result_properties.get("incident_field")
# workflow formatted content is 'json'. Standard functions is 'content'
json = result_properties.get("json") if result_properties.get("json") else result_properties.get("content")
json_err = None
# is there an issue we need handle now?
if not json and \
result_properties.get("success") == False and result_properties.get("reason"):
json_err = result_properties.get("reason")
return padding, separator, header, json_omit_list, incident_field, json, json_err, sort_keys
## S T A R T
padding, separator, header, json_omit_list, incident_field, json, json_err, sort_keys = get_properties('convert_json_to_rich_text')
if json_err:
result = "Result error: {}".format(json_err)
else:
if header:
if isinstance(separator, list):
hdr = u"{0}{1}{2}".format(separator[0], header, separator[1])
else:
hdr = u"{0}{1}".format(header, separator)
else:
hdr = u""
convert = ConvertJson(omit_keys=json_omit_list, padding=padding, separator=separator, sort_keys=sort_keys)
converted_json = convert.convert_json_to_rich_text(json)
result = u"{}{}".format(hdr, converted_json if converted_json else "\nNone")
rich_text_note = helper.createRichText(result)
if incident_field:
incident[incident_field] = rich_text_note
else:
incident.addNote(rich_text_note)
Playbooks¶
Playbook Name |
Description |
Activation Type |
Object |
Status |
Condition |
---|---|---|---|---|---|
Axonius: Add Artifacts to SOAR |
Add DNS Name, IP Address, Service, and System Name artifacts to the SOAR case from data in the Axonius Devices data table row. |
Manual |
axonius_devices_dt |
|
|
Axonius: Add to Devices Data Table |
Add the specified artifact to the Axonius Devices data table, |
Manual |
artifact |
|
|
Axonius: Get Device Count |
Get the device count of the devices in Axonius that match the query by AQL query string or saved query name in Axonius. |
Manual |
incident |
|
|
Axonius: Query Devices |
Query Axonius using the user specified AQL query string or a saved query name in Axonius. Optionally populate the devices returned in the Axonius Devices data table, write to a note or to an attachment. |
Manual |
incident |
|
|
Axonius: Refetch Device in Row |
Get the device data on the device in this row and add to a new row in the data table. Use to compare device query results over time. |
Manual |
axonius_devices_dt |
|
|
Axonius: Run Enforcement by Name - Example |
Manual playbook to run an Axonius enforcement by specifying the enforcement name in Axonius. |
Manual |
incident |
|
|
Axonius: Write Device JSON to an Attachment |
Write the JSON returned from Axonius device query to an attachment. |
Manual |
axonius_devices_dt |
|
|
Custom Layouts¶
The app automatically creates a custom Axonius
tab on first run of either the axonius_get_device_by_query
or axonius_get_device_count
functions:
Data Table - Axonius Devices¶
API Name:¶
axonius_devices_dt
Columns:¶
Column Name |
API Access Name |
Type |
Tooltip |
---|---|---|---|
Asset Name |
|
|
- |
Country |
|
|
- |
Device Disabled |
|
|
- |
|
|
- |
|
Hard Drives: Encryption Status |
|
|
- |
Hostname |
|
|
- |
ID |
|
|
- |
IP |
|
|
- |
Last Used Dept |
|
|
- |
Last Used Users |
|
|
- |
Link |
|
|
- |
OS: Type & Distribution |
|
|
- |
Owner |
|
|
- |
Query Date |
|
|
- |
Region |
|
|
- |
Security Level |
|
|
- |
Tags |
|
|
- |
Axonius Devices Data Table Limit¶
A large number of results may be returned from Axonius when using the playbook Axonius: Query Devices
. In order to avoid hitting maximum number of line limits processed in the python scripting engine and to avoid large unwieldy data tables, there is a variable DEFAULT_DATA_TABLE_LIMIT
that is defined in the input script of the Axonius: Query Devices
playbook to implement a hard limit of results (rows) added to the data table in one query. If need you need to limit results further or if you need to increase the results in the data table, edit this constant variable in the Axonius: Query Devices
input script.
# Limit number of devices populated in the Axonius Devices data table.
# Change this default value if you want more results in the data table.
DEFAULT_DATA_TABLE_LIMIT = 100
Axonius Devices Data Table Field Names¶
When using the Axonius App, if you require Axonius device fields that are not included in the Axonius Devices data table, you can select Note
or Attachment
for the Results Output
when running the playbook Axonius: Query Devices
. Modify the playbook input script to include the Axonius field_names
in the list retrieved from Axonius in the axonius_get_device_by_query
function. These are the default fields in the input function script:
field_names = ["specific_data.data.network_interfaces.ips_preferred",
"specific_data.data.name",
"specific_data.data.hostname_preferred",
"specific_data.data.owner",
"specific_data.data.last_used_users_mail_association",
"specific_data.data.os.type_distribution",
"specific_data.data.last_used_users",
"specific_data.data.last_used_users_departments_association",
"specific_data.data.hard_drives.encryption_status",
"specific_data.data.device_disabled",
"specific_data.data.network_interfaces.security_level_preferred",
"specific_data.data.network_interfaces.region_preferred",
"specific_data.data.network_interfaces.country_preferred",
"labels"]
Use the Axonius Query Wizard
to look-up the field names in Axonius:
Troubleshooting & Support¶
Refer to the documentation listed in the Requirements section for troubleshooting information.
For Support¶
This is an IBM supported app. Please search ibm.com/mysupport for assistance.