Aha

Use the Aha! integration to list and manage Cortex XSOAR features from Aha.

Utilities · AHA

Details

IDAha
ProviderAha Labs Inc.
CategoryUtilities
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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-feature

    You can edit the following fields in a feature: Name and Status.

  • aha-edit-idea

    Edit an idea status to Shipped.

  • aha-get-features

    Lists all features from service, unless a specific feature is specified.

  • aha-get-ideas

    Lists 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()