Details
| ID | Aha |
|---|---|
| Provider | Aha Labs Inc. |
| Category | Utilities |
| From Version | 6.5.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Use the Aha! integration to list and manage Cortex XSOAR features from Aha.
This integration was integrated and tested with API version December 02, 2022 release of Aha.
Configure Aha in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | True | |
| Project Name | Check the Aha! project name in the URL. Replace the <PROJECT_NAME> placeholder in the following : example.com.aha.io/products/<PROJECT_NAME>/features. | True |
| Api Key | API Key to access the service REST API. | True |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
aha-get-features
Lists all features from service, unless a specific feature is specified.
Base Command
aha-get-features
Input
| Argument Name | Description | Required |
|---|---|---|
| from_date | Show features created after this date. Default is 2020-01-01. | Optional |
| feature_name | The name of a specific feature to retrieve. | Optional |
| fields | A comma-separated list of fields to include in the Aha! service response. Default is name,reference_num,id,created_at. | Optional |
| page | The specific results page to retrieve. Default is 1. | Optional |
| per_page | The maximum number of results per page. Default is 30. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| AHA.Feature.id | UUID | The feature ID. |
| AHA.Feature.name | String | The feature name. |
| AHA.Feature.reference_num | String | The feature reference number. |
| AHA.Feature.workflow_status | String | The feature status description. |
| AHA.Feature.description | String | The feature description. |
| AHA.Feature.created_at | Date | The feature creation date. |
Command example
!aha-get-features
!aha-get-features feature_name=DEMO-10 fields=workflow_status
!aha-get-features fields=workflow_status page=2 per_page=30
aha-edit-feature
You can edit the following fields in a feature: Name and Description.
Base Command
aha-edit-feature
Input
| Argument Name | Description | Required |
|---|---|---|
| feature_name | The name of the feature to edit. | Required |
| fields | Fields in JSON format to edit in a feature. Possible fields are name and status. Status should match Aha values under workflow_status. Example:” {“name”: “name”, “status” : “Closed”}. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| AHA.Feature.id | UUID | The feature ID. |
| AHA.Feature.name | String | The feature name. |
| AHA.Feature.reference_num | String | The feature reference number. |
| AHA.Feature.workflow_status | String | The feature status description. |
| AHA.Feature.description | String | The feature description. |
| AHA.Feature.created_at | Date | The feature creation date. |
Command example
!aha-edit-feature feature_name=DEMO-10 fields=`{"name":"the_new_name", "status":"Closed"}
aha-get-ideas
Lists all ideas from service, unless a specific idea is specified.
Base Command
aha-get-ideas
Input
| Argument Name | Description | Required |
|---|---|---|
| from_date | Show ideas created after this date. Default is 2020-01-01. | Optional |
| idea_name | The name of a specific idea to retrieve. | Optional |
| fields | A comma-separated list of fields to include in the Aha! service response. Default is name,reference_num,id,created_at. | Optional |
| page | The specific results page to retrieve. Default is 1. | Optional |
| per_page | The maximum number of results per page. Default is 30. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| AHA.Idea.id | UUID | The idea ID. |
| AHA.Idea.name | String | The idea name. |
| AHA.Idea.reference_num | String | The idea reference number. |
| AHA.Idea.workflow_status | String | The idea status description. |
| AHA.Idea.description | String | The idea description. |
| AHA.Idea.created_at | Date | The idea creation date. |
Command example
!aha-get-ideas
!aha-get-ideas idea_name=DEMO-I-2895
!aha-get-ideas idea_name=DEMO-I-2895 fields=workflow_status
aha-edit-idea
Edit an idea status to Shipped.
Base Command
aha-edit-idea
Input
| Argument Name | Description | Required |
|---|---|---|
| idea_name | The name of the idea to edit. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| AHA.Idea.id | UUID | The idea ID. |
| AHA.Idea.name | String | The idea name. |
| AHA.Idea.reference_num | String | The idea reference number. |
| AHA.Idea.workflow_status | String | The idea status description. |
| AHA.Idea.description | String | The idea description. |
| AHA.Idea.created_at | Date | The idea creation date. |
Command example
!aha-edit-idea idea_name=DEMO-I-2895
Configuration parameters
url— Server URL (required)project_name— Project Name (required)api_key— (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (4)
-
aha-edit-featureYou can edit the following fields in a feature: Name and Status.
-
aha-edit-ideaEdit an idea status to Shipped.
-
aha-get-featuresLists all features from service, unless a specific feature is specified.
-
aha-get-ideasLists all ideas from service, unless a specific idea is specified.
from CommonServerPython import * # noqa # pylint: disable=unused-wildcard-import from CommonServerUserPython import * # noqa import requests from enum import Enum # Disable insecure warnings requests.packages.urllib3.disable_warnings() # type: ignore[attr-defined] # pylint: disable=no-member """ CONSTANTS """ REPLACE = "replace" DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR URL_SUFFIX_PATTERN = f"/products/{REPLACE}/" EDIT_FIELDS = ["id", "reference_num", "name", "description", "workflow_status", "created_at"] DEFAULT_FIELDS = ["reference_num", "name", "id", "created_at"] FEATURE_FIELDS = ["ideas"] """ AHA ENUM""" class AHA_TYPE(Enum): IDEAS = 1 FEATURES = 2 def get_url_suffix(self) -> str: if self == AHA_TYPE.IDEAS: return "ideas/" else: return "features/" def get_type_plural(self) -> str: if self == AHA_TYPE.IDEAS: return "ideas" else: return "features" def get_type_singular(self) -> str: if self == AHA_TYPE.IDEAS: return "idea" else: return "feature" def get_type_for_outputs(self) -> str: if self == AHA_TYPE.IDEAS: return "Idea" else: return "Feature" """ CLIENT CLASS """ class Client(BaseClient): url = "" def __init__(self, headers: dict, base_url: str, proxy: bool, verify: bool, url: str): super().__init__(base_url=base_url, proxy=proxy, verify=verify, headers=headers) self.url = url self._headers["Content-Type"] = "application/json" def get(self, aha_type: AHA_TYPE, name: str, fields: str, from_date: str, page: str, per_page: str) -> dict: """ Retrieves a list of features/ideas from AHA Args: aha_type: determine what to get ideas or features using AHA_TYPE Enum. name: str if given it will fetch the feature/idea specified. if not, it will fetch all features/ideas. fields: str optional feature/idea fields to retrieve from the service. from_date: str format: YYYY-MM-DD get features/ideas created after from_date. page: str pagination specify the number of the page. per_page: str pagination specify the maximum number of features/ideas per page. """ headers = self._headers params = { "updated_since": from_date, "fields": fields, "page": page, "per_page": per_page, } return self._http_request( method="GET", url_suffix=f"{self.url}{aha_type.get_url_suffix()}{name}", headers=headers, params=params, resp_type="json", ) def edit(self, aha_object_name: str, aha_type: AHA_TYPE, fields: dict) -> dict: """ Updates fields in a feature/idea from AHA Args: aha_object_name: str idea to update aha_type: determine what to edit ideas or features using AHA_TYPE Enum. fields: Dict fields to update """ payload = build_edit_idea_req_payload() if aha_type == AHA_TYPE.IDEAS else build_edit_feature_req_payload(fields=fields) demisto.debug(f"Edit {aha_type.get_type_singular()} payload: {payload}") fields = ",".join(EDIT_FIELDS) url_suffix = f"{self.url}{aha_type.get_url_suffix()}{aha_object_name}?fields={fields}" return self._http_request(method="PUT", url_suffix=url_suffix, resp_type="json", json_data=payload) """ HELPER FUNCTIONS""" def build_edit_feature_req_payload(fields: dict): payload: dict = {"feature": {}} for field in fields: feature = payload.get("feature", {}) if field == "status": workflow_status = {"name": fields[field]} feature["workflow_status"] = workflow_status else: feature[field] = fields[field] return payload def build_edit_idea_req_payload(): payload: dict = {"idea": {}} idea = payload.get("idea", {}) idea["workflow_status"] = "Shipped" return payload def extract_ideas_from_feature(ideas: List) -> List: ret_list: list[str] = [] for idea in ideas: ret_list.append(idea.get("reference_num")) return ret_list def parse_multiple_objects(aha_objects: dict, fields: List) -> List: res_list = [] for res in aha_objects: curr = parse_single_object(res, fields=fields) res_list.extend(curr) demisto.debug(f"Parsed response fields: {res_list}") return res_list def parse_single_object(aha_object: dict, fields: List = DEFAULT_FIELDS) -> List: ret_dict = {} for curr in fields: if curr == "description": ret_dict[curr] = aha_object.get(curr, {}).get("body") elif curr == "workflow_status": ret_dict[curr] = aha_object.get(curr, {}).get("name") elif curr == "ideas": ret_dict[curr] = extract_ideas_from_feature(aha_object.get(curr, {})) else: ret_dict[curr] = aha_object.get(curr, "") return [ret_dict] """ COMMAND FUNCTIONS """ def test_module(client: Client) -> str: """Tests API connectivity and authentication'""" message: str = "" try: result = client.get(AHA_TYPE.FEATURES, "", "", "2020-01-01", page="1", per_page="1") if result: message = "ok" except DemistoException as e: if "Forbidden" in str(e) or "Authorization" in str(e): message = "Authorization Error: make sure that the API Key is setup correctly." else: raise e return message def get_command( client: Client, aha_type: AHA_TYPE, from_date: str, aha_object_name: str = "", fields: str = "", page: str = "1", per_page: str = "30", ) -> CommandResults: message: List = [] fields_list: List = DEFAULT_FIELDS + argToList(fields) if aha_type == AHA_TYPE.FEATURES: fields_list.extend(FEATURE_FIELDS) req_fields = ",".join(fields_list) response = client.get( aha_type=aha_type, name=aha_object_name, fields=req_fields, from_date=from_date, page=page, per_page=per_page ) if response: if aha_type.get_type_plural() in response: message = parse_multiple_objects(response[aha_type.get_type_plural()], fields_list) else: message = parse_single_object(response[aha_type.get_type_singular()], fields_list) human_readable = tableToMarkdown(f"Aha! get {aha_type.get_type_plural()}", message, removeNull=True) else: human_readable = "" demisto.debug(f"{response=} -> {human_readable=}") return CommandResults( outputs_prefix=f"AHA.{aha_type.get_type_for_outputs()}", outputs_key_field="id", outputs=message, raw_response=response, readable_output=human_readable, ) def edit_command(client: Client, aha_type: AHA_TYPE, aha_object_name: str, fields: str = "{}") -> CommandResults: message: List = [] fieldsDict = json.loads(fields) response = client.edit(aha_object_name=aha_object_name, aha_type=aha_type, fields=fieldsDict) if response: message = parse_single_object(response[aha_type.get_type_singular()], fields=EDIT_FIELDS) human_readable = tableToMarkdown(f"Aha! edit {aha_type.get_type_singular()}", message, removeNull=True) else: human_readable = "" demisto.debug(f"{response=} -> {human_readable=}") return CommandResults( outputs_prefix=f"AHA.{aha_type.get_type_for_outputs()}", outputs_key_field="id", outputs=message, readable_output=human_readable, raw_response=response, ) """ MAIN FUNCTION """ def main() -> None: params = demisto.params() base_url = urljoin(params["url"], "/api/v1") project_name = params.get("project_name", {}) url = URL_SUFFIX_PATTERN.replace(REPLACE, project_name) api_key = params.get("api_key", {}).get("password", {}) proxy = params.get("proxy", False) verify = not params.get("insecure", False) demisto.debug(f"Command being called is {demisto.command()}") try: headers: dict = {"Authorization": f"Bearer {api_key}"} client = Client(headers=headers, base_url=base_url, proxy=proxy, verify=verify, url=url) command = demisto.command() args = demisto.args() if command == "test-module": result = test_module(client) return_results(result) elif command == "aha-get-features": command_result = get_command(client, aha_type=AHA_TYPE.FEATURES, aha_object_name=args.pop("feature_name", ""), **args) return_results(command_result) elif command == "aha-edit-feature": command_result = edit_command( client, aha_type=AHA_TYPE.FEATURES, aha_object_name=args.pop("feature_name", ""), **args ) return_results(command_result) elif command == "aha-get-ideas": command_result = get_command( client=client, aha_type=AHA_TYPE.IDEAS, aha_object_name=args.pop("idea_name", ""), **args ) return_results(command_result) elif command == "aha-edit-idea": command_result = edit_command(client, aha_type=AHA_TYPE.IDEAS, aha_object_name=args.pop("idea_name", ""), **args) return_results(command_result) else: raise NotImplementedError(f"{command} command is not implemented.") except Exception as e: return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()