Skyhigh Security

Skyhigh Security is a cloud-based, multi-tenant service that enables Cloud Discovery and Risk Monitoring, Cloud Usage Analytics, Cloud Access and Control.

Network Security · Skyhigh Security SSE

Details

IDSkyhigh Security
ProviderSTG
CategoryNetwork Security
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM

README

Additional information

Skyhigh Security is a cloud-based, multi-tenant service that enables Cloud Discovery and Risk Monitoring, Cloud Usage Analytics, Cloud Access and Control.
This integration was integrated and tested with version 1 of Skyhigh Security.

API limitations

Do to API limitations, keep in mind that over time the integration can start to work more slowly.
The solution is to restart the last-run.

Configure Skyhigh Security in Cortex

Parameter Description Required
Base URL (e.g., https://www.myshn.net)   True
Password The username and password to use for the connection True
Maximum number of incidents to fetch every time. Default is 50. Maximum is 500. False  
First fetch in timestamp format (<number> <time unit>, e.g., 12 hours, 7 days). Default is 3 days.   False
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.

skyhigh-security-incident-query


Retrieves a list of incidents in ascending time modified order.

Base Command

skyhigh-security-incident-query

Input

Argument Name Description Required
limit Maximum number of items that will be returned within a single response. Maximum is 500. If the limit value exceeds the 500 maximum, it will not be flagged as an error but will also not increase results. Default is 50. Optional
page Pagination support for use with a large “limit” value. Optional
page_size Pagination support for use with a large “limit” value. The maximum is 500. Optional
start_time For time arguments use the ISO-8601 standard - ‘%Y-%m-%dT%H:%M:%SZ’ or relative time (last X days). Default is 3 days. Optional
end_time For time arguments use the ISO-8601 standard - ‘%Y-%m-%dT%H:%M:%SZ’ or relative time (last X days). Optional
actor_ids The actor IDs of the incidents to retrieve. Optional
service_names The service names of the incidents to retrieve. Optional
incident_types The type of the incidents to retrieve. Possible values are: Alert, Threat. Optional
categories The categories of the incidents to retrieve. When defining the categories argument the incident_types argument is ignored. Possible values are: Access, Admin, Audit, CompromisedAccount, Data, InsiderThreat, Policy, PrivilegeAccess, Vulnerability. Optional

Context Output

Path Type Description
SkyhighSecurity.Incident Unknown The incident’s metadata.

Command example

!skyhigh-security-incident-query limit="3" start_time="3 days"

Context Example

{
  "SkyhighSecurity": {
    "Incident": [
      {
        "activityNames": [],
        "actorId": "NOT AVAILABLE",
        "actorIdType": "USER",
        "incidentGroup": "Alert.Policy.CloudAccess",
        "incidentGroupId": null,
        "incidentId": "CAP-111111",
        "incidentRiskScore": 10,
        "incidentRiskSeverity": "high",
        "incidentRiskSeverityId": 2,
        "information": {
          "contentItemCreatedOn": "2022-07-01T19:13:53.075Z",
          "contentItemId": "UNKNOWN",
          "contentItemName": "/css/webfonts/office365icons.eot?",
          "contentItemType": "REQUEST",
          "device": {
            "application": {
              "type": "application",
              "user_agent": "Chrome"
            },
            "device_type": "Desktop",
            "ip": "1.1.1.1",
            "os": {
              "platform": "Windows",
              "type": "os"
            }
          },
          "eventId": "222222",
          "lastExecutedResponseLabel": "Block Access",
          "policyId": 333333,
          "policyName": "Block downloads to unmanaged devices",
          "userAttributes": {}
        },
        "instanceId": 444444,
        "instanceName": "zengel",
        "responses": ["Block Access"],
        "serviceNames": ["Microsoft Office 365 and OneDrive"],
        "significantlyUpdatedAt": "2022-07-01T19:13:57.053Z",
        "status": "new",
        "timeCreated": "2022-07-01T19:13:53.075Z",
        "timeModified": "2022-07-01T19:13:57.053Z"
      },
      {
        "activityNames": [],
        "actorId": "NOT AVAILABLE",
        "actorIdType": "USER",
        "incidentGroup": "Alert.Policy.CloudAccess",
        "incidentGroupId": null,
        "incidentId": "CAP-555555",
        "incidentRiskScore": 3,
        "incidentRiskSeverity": "low",
        "incidentRiskSeverityId": 0,
        "information": {
          "contentItemCreatedOn": "2022-07-02T02:38:16.706Z",
          "contentItemId": "UNKNOWN",
          "contentItemName": "/",
          "contentItemType": "REQUEST",
          "device": {
            "application": {
              "type": "application",
              "user_agent": "Unknown"
            },
            "device_type": "Unknown",
            "ip": "2.2.2.2",
            "os": {
              "platform": "Unknown",
              "type": "os"
            }
          },
          "eventId": "666666",
          "lastExecutedResponseLabel": "Allow Access",
          "policyId": 777777,
          "policyName": "allow successfactors",
          "userAttributes": {}
        },
        "instanceId": 888888,
        "instanceName": "ZengelBiz",
        "responses": ["Allow Access"],
        "serviceNames": ["SAP - SuccessFactors HXM Suite"],
        "significantlyUpdatedAt": "2022-07-02T02:38:18.682Z",
        "status": "new",
        "timeCreated": "2022-07-02T02:38:16.706Z",
        "timeModified": "2022-07-02T02:38:18.682Z"
      },
      {
        "activityNames": [],
        "actorId": "NOT AVAILABLE",
        "actorIdType": "USER",
        "incidentGroup": "Alert.Policy.CloudAccess",
        "incidentGroupId": null,
        "incidentId": "CAP-999999",
        "incidentRiskScore": 3,
        "incidentRiskSeverity": "low",
        "incidentRiskSeverityId": 0,
        "information": {
          "contentItemCreatedOn": "2022-07-02T02:38:16.888Z",
          "contentItemId": "UNKNOWN",
          "contentItemName": "/",
          "contentItemType": "REQUEST",
          "device": {
            "application": {
              "type": "application",
              "user_agent": "Unknown"
            },
            "device_type": "Unknown",
            "ip": "2.2.2.2",
            "os": {
              "platform": "Unknown",
              "type": "os"
            }
          },
          "eventId": "144304",
          "lastExecutedResponseLabel": "Allow Access",
          "policyId": 777777,
          "policyName": "allow successfactors",
          "userAttributes": {}
        },
        "instanceId": 888888,
        "instanceName": "ZengelBiz",
        "responses": ["Allow Access"],
        "serviceNames": ["SAP - SuccessFactors HXM Suite"],
        "significantlyUpdatedAt": "2022-07-02T02:38:19.202Z",
        "status": "new",
        "timeCreated": "2022-07-02T02:38:16.888Z",
        "timeModified": "2022-07-02T02:38:19.202Z"
      }
    ]
  }
}

Human Readable Output

Skyhigh Security Incidents

Alert Severity Incident ID Service Name Status Time (UTC) User Name
high CAP-111111 Microsoft Office 365 and OneDrive new 2022-07-01T19:13:53.075Z NOT AVAILABLE
low CAP-555555 SAP - SuccessFactors HXM Suite new 2022-07-02T02:38:16.706Z NOT AVAILABLE
low CAP-999999 SAP - SuccessFactors HXM Suite new 2022-07-02T02:38:16.888Z NOT AVAILABLE

skyhigh-security-incident-status-update


Update status of single/multiple incidents.

Note!
For multiple IDs, a single status will be applied for all IDs
e.g., 123, 456, 789 » change status to » closed.

Base Command

skyhigh-security-incident-status-update

Input

Argument Name Description Required
incident_ids The incidents IDs that should be updated. Required
status The new status of the incidents. Possible values are: new, opened, false positive, resolved, suppressed, archived. Required

Context Output

There is no context output for this command.

Command example

!skyhigh-security-incident-status-update incident_ids=CAP-114044 status=archived

Human Readable Output

Status updated for user

skyhigh-security-anomaly-activity-list


Fetches activities for a given anomaly ID.

Base Command

skyhigh-security-anomaly-activity-list

Input

Argument Name Description Required
anomaly_id The anomaly ID from where to retrieve the activities. Only for incidents of type anomaly (ANO-123). Required

Context Output

Path Type Description  
SkyhighSecurity.AnomalyActivity.timeStamp String The timestamp of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.actionName String The action name.  
SkyhighSecurity.AnomalyActivity.asnName String The ASN name of an activity.  
SkyhighSecurity.AnomalyActivity.city String The city where the anomaly activity occurred.  
SkyhighSecurity.AnomalyActivity.collabGroup String The collaboration group for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.count Number The number of anomalies detected.  
SkyhighSecurity.AnomalyActivity.country String The country of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.deviceManaged Boolean Whether the anomaly activity is managed by the device or not.  
SkyhighSecurity.AnomalyActivity.directory String The directory of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.downloadBytes Number The number of bytes downloaded by the anomaly activity.  
SkyhighSecurity.AnomalyActivity.eventCount Number The number of anomalies detected.  
SkyhighSecurity.AnomalyActivity.fileFolderPath String The file folder path for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.fileName String The file name of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.fileSharingEnabled Boolean Whether the CASB file sharing is enabled or not.  
SkyhighSecurity.AnomalyActivity.fileSize Number The file size of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.fileType String The file type of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.geoOrgNameV1 String The geo organization name.  
SkyhighSecurity.AnomalyActivity.httpMethod String The HTTP method used by the anomaly activity.  
SkyhighSecurity.AnomalyActivity.instanceId String The instance ID for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.isSourceTrusted Boolean Whether the anomaly activity source is trusted or not.  
SkyhighSecurity.AnomalyActivity.networkType String The network type for the anomaly.  
SkyhighSecurity.AnomalyActivity.objectType String The object type for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.operation String The operation type.  
SkyhighSecurity.AnomalyActivity.proxyDescription String The proxy description for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.proxyType String The proxy type for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.region String The region where the anomaly activity occurred.  
SkyhighSecurity.AnomalyActivity.serviceName String The name of the service.  
SkyhighSecurity.AnomalyActivity.siteUrl String The URL of the CASB’s site.  
SkyhighSecurity.AnomalyActivity.sourceIP IP The IP address of the source IP.  
SkyhighSecurity.AnomalyActivity.sourceIdentifier String The source identifier for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.targetId String The target ID for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.targetType String The anomaly activity type.  
SkyhighSecurity.AnomalyActivity.tenantId Number The tenant ID for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.threatCategory String The threat category for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.trustEntity String The trust entity for the anomaly activity.  
SkyhighSecurity.AnomalyActivity.trustReason String The trust reason of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.uploadBytes Number The number of bytes uploaded.  
SkyhighSecurity.AnomalyActivity.url String The URL of the anomaly activity.  
SkyhighSecurity.AnomalyActivity.user String The user who triggered the anomaly.  

skyhigh-security-policy-dictionary-list


List existing policy dictionaries.

Base Command

skyhigh-security-policy-dictionary-list

Input

Argument Name Description Required
limit Maximum number of policies that will be returned within a single response. Default is 50. Optional
page Pagination support for use with a large “limit” value. Optional
page_size Pagination support for use with a large “limit” value. Optional
name The name of the policies to retrieve. Optional

Context Output

Path Type Description
SkyhighSecurity.Dictionaries.ID Number The ID for the dictionary.
SkyhighSecurity.Dictionaries.LastModified String The date the dictionary was last modified.
SkyhighSecurity.Dictionaries.Name String The name of the dictionary.

Command example

!skyhigh-security-policy-dictionary-list limit="3"

Context Example

{
  "SkyhighSecurity": {
    "dictionaries": [
      {
        "ID": 121212,
        "LastModified": "2022-07-04T14:02:03.000+0000",
        "Name": "(Default) Internal Domains"
      },
      {
        "ID": 131313,
        "LastModified": "2020-04-15T13:08:09.000+0000",
        "Name": "Access Whitelist Users"
      },
      {
        "ID": 141414,
        "LastModified": "2021-07-14T12:22:37.000+0000",
        "Name": "Allowed Geo"
      }
    ]
  }
}

Human Readable Output

List of Skyhigh Security Policies

ID Last Modified Name
121212 2022-07-04T14:02:03.000+0000 (Default) Internal Domains
131313 2020-04-15T13:08:09.000+0000 Access Whitelist Users
141414 2021-07-14T12:22:37.000+0000 Allowed Geo

skyhigh-security-policy-dictionary-update


Adds new content to an existing policy dictionary.

Base Command

skyhigh-security-policy-dictionary-update

Input

Argument Name Description Required
dictionary_id The dictionary where to set the policy. Required
name A name for the new key-value which will be added in the dictionary. Required
content The value to be set in the dictionary for the given key-name. Multiple values can be separated by commas. Required

Context Output

There is no context output for this command.

Command example

!skyhigh-security-policy-dictionary-update dictionary_id="121212" name="(Default) Internal Domains" content="gmail.com, outlook.com"

Human Readable Output

Dictionary id: 121212 was updated.

Configuration parameters

  • url — Base URL (e.g., https://www.myshn.net) (required)
  • credentials — Credentials (required)
  • max_fetch — Maximum number of incidents to fetch every time. Default is 50. Maximum is 500.
  • first_fetch — First fetch in timestamp format (<number> <time unit>, e.g., 12 hours, 7 days). Default is 3 days.
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (5)

  • skyhigh-security-anomaly-activity-list

    Fetches activities for a given anomaly ID.

  • skyhigh-security-incident-query

    Retrieves a list of incidents in ascending time modified order.

  • skyhigh-security-incident-status-update

    Update status of single/multiple incidents. Note! For multiple IDs, a single status will be applied for all IDs e.g., 123, 456, 789 >> change status to >> closed.

  • skyhigh-security-policy-dictionary-list

    List existing policy dictionaries.

  • skyhigh-security-policy-dictionary-update

    Adds new content to an existing policy dictionary.

import csv
import io
import traceback
from typing import Any

# Disable insecure warnings
import urllib3
from CommonServerPython import *  # noqa # pylint: disable=unused-wildcard-import

from CommonServerUserPython import *  # noqa

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"  # ISO8601 format with UTC, default in XSOAR
CategoryToIncidentType = {
    "Access": "Alert",
    "Admin": "Alert",
    "Audit": "Alert",
    "Data": "Alert",
    "Policy": "Alert",
    "Vulnerability": "Alert",
    "CompromisedAccount": "Threat",
    "InsiderThreat": "Threat",
    "PrivilegeAccess": "Threat",
}

""" CLIENT CLASS """


def csv2json(csv_data: str):
    """Converts data from csv to json
    Args:
        csv_data: data in csv format
    Returns:
        the same data in json formal
    """
    reader = csv.DictReader(io.StringIO(csv_data))
    json_data = list(reader)
    return json_data


class Client(BaseClient):
    def test(self):
        self.incident_query(1, (arg_to_datetime("3 days") or datetime.now() - timedelta(days=3)).strftime(DATE_FORMAT))

    def incident_query(
        self,
        limit: Optional[int],
        start_time: str = "",
        end_time: str = "",
        actor_ids: list[str] = None,
        service_names: list[str] = None,
        categories: list[str] = None,
    ) -> dict[str, Any]:
        url_suffix = "/external/api/v1/queryIncidents"
        params = {"limit": limit or 50}
        data = assign_params(
            startTime=start_time,
            endTime=end_time,
            actorIds=actor_ids,
            serviceNames=service_names,
            incidentCriteria=assign_params(categories=categories),
        )
        return self._http_request("POST", url_suffix, params=params, json_data=data, raise_on_status=True)

    def status_update(self, incident_ids: List, status: str) -> dict[str, str]:
        url_suffix = "/external/api/v1/modifyIncidents"
        data = [{"incidentId": incident_id, "changeRequests": {"WORKFLOW_STATUS": status}} for incident_id in incident_ids]
        return self._http_request("POST", url_suffix, json_data=data, raise_on_status=True)

    def anomaly_activity_list(self, incident_id: Optional[int]) -> Optional[bytes]:
        url_suffix = "/external/api/v1/queryActivities"
        data = {"incident_id": incident_id}
        results = self._http_request("POST", url_suffix, json_data=data, resp_type="response")
        demisto.debug(f"This is the results from the activity list: {results}")

        activities = results.content
        demisto.debug(f"This is the content from the activity list: {activities}")
        return activities

    def policy_dictionary_list(self) -> List[dict]:
        url_suffix = "/dlp/dictionary"
        return self._http_request("GET", url_suffix, raise_on_status=True)

    def policy_dictionary_update(self, dict_id: Optional[int], name: str, content: str) -> dict[str, str]:
        url_suffix = "/dlp/dictionary"
        data = {"id": dict_id, "name": name, "content": content}
        return self._http_request("PUT", url_suffix, data=json.dumps(data), raise_on_status=True)


""" HELPER FUNCTIONS """


def calculate_offset_and_limit(**kwargs) -> tuple[int, int]:
    if limit := arg_to_number(kwargs.get("limit")):  # 'limit' is stronger than pagination ('page', and 'page_size').
        return 0, limit
    if (page := arg_to_number(kwargs.get("page"))) and (page_size := arg_to_number(kwargs.get("page_size"))):
        page -= 1  # First page means list in index zero.
        return page * page_size, page * page_size + page_size
    return 0, 50


def convert_to_xsoar_severity(severity: str) -> float:
    """Maps a Skyhigh Security incident severity to a Cortex XSOAR severity.

    Known documented values ('low', 'medium', 'high'), as well as 'info', map to their
    XSOAR equivalents. The lookup is case-insensitive. Any unrecognized or missing value
    is mapped to IncidentSeverity.UNKNOWN.

    :type severity: ``str``
    :param severity: severity as returned from the Skyhigh Security API.

    :return: Cortex XSOAR Severity.
    :rtype: ``float``
    """
    severity_map = {
        "info": IncidentSeverity.INFO,
        "low": IncidentSeverity.LOW,
        "medium": IncidentSeverity.MEDIUM,
        "high": IncidentSeverity.HIGH,
    }
    normalized = (severity or "").lower()
    if normalized not in severity_map:
        demisto.debug(f"Unmapped Skyhigh incidentRiskSeverity '{severity}' received; defaulting to UNKNOWN severity.")
        return IncidentSeverity.UNKNOWN
    return severity_map[normalized]


""" 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``
    """

    try:
        client.test()
        message = "ok"
    except DemistoException as e:
        if "Forbidden" in str(e) or "Authorization" in str(e):
            message = "Authorization Error: make sure API Key is correctly set"
        else:
            raise
    return message


def fetch_incidents(client: Client, params: dict) -> tuple[dict, list]:
    last_run = demisto.getLastRun()
    xsoar_incidents = []

    limit = arg_to_number(params.get("max_fetch", 50))
    if not (start_time := last_run.get("start_time")):  # in the first interval.
        start_time = (arg_to_datetime(params.get("first_fetch", "3 days"))).strftime(DATE_FORMAT)  # type: ignore[union-attr]

    result = client.incident_query(limit, start_time)

    if incidents := result.get("body", {}).get("incidents", []):
        ids = set(last_run.get("ids", set()))

        for incident in incidents:
            # Since the API returns the incidents in ascending time modified order.
            # As mentioned here:
            # https://success.myshn.net/Skyhigh_CASB/Skyhigh_CASB_APIs/Incidents_API/02_Incidents_API_Paths#_responses_3
            # We need to verify no duplicates are pushed to xsoar.
            if (incident_id := incident.get("incidentId")) not in ids:
                xsoar_incidents.append(
                    {
                        "name": f"Skyhigh Security Incident {incident_id}",
                        "occurred": incident.get("timeModified"),
                        "rawJSON": json.dumps(incident),
                        "dbotMirrorId": incident_id,
                        "severity": convert_to_xsoar_severity(incident.get("incidentRiskSeverity", "low")),
                    }
                )

                ids.add(incident_id)

        last_run = {"start_time": result.get("body", {}).get("responseInfo", {}).get("nextStartTime", ""), "ids": list(ids)}

    return last_run, xsoar_incidents


def incident_query_command(client: Client, args: dict) -> CommandResults:
    limit = arg_to_number(args.get("limit", 50))
    start_time = (arg_to_datetime(args.get("start_time")) or datetime.now() - timedelta(days=3)).strftime(DATE_FORMAT)
    end_time = (arg_to_datetime(args.get("end_time")) or datetime.now()).strftime(DATE_FORMAT)
    actor_ids = argToList(args.get("actor_ids"))
    service_names = argToList(args.get("service_names"))
    if categories := argToList(args.get("categories")):
        categories = [{"incidentType": CategoryToIncidentType.get(category), "category": category} for category in categories]
    elif incident_types := argToList(args.get("incident_types")):
        categories = [{"incidentType": incident_type} for incident_type in incident_types]

    if not (page_number := arg_to_number(args.get("page_number"))) or not (page_size := arg_to_number(args.get("page_size"))):
        result = client.incident_query(limit, start_time, end_time, actor_ids, service_names, categories)
    else:
        result = {}
        for _ in range(page_number):
            result = client.incident_query(page_size, start_time, end_time, actor_ids, service_names, categories)
            start_time = result.get("body", {}).get("responseInfo", {}).get("nextStartTime", {})

    if incidents := result.get("body", {}).get("incidents"):
        readable_dict = []

        for incident in incidents:
            readable_dict.append(
                {
                    "IncidentID": incident.get("incidentId"),
                    "Time(UTC)": incident.get("timeCreated"),
                    "Status": incident.get("status"),
                    "Alert Action": incident.get("remediationResponse"),
                    "Service Name": incident.get("serviceNames"),
                    "Alert Severity": incident.get("incidentRiskSeverity"),
                    "User Name": incident.get("actorId"),
                    "Policy Name": incident.get("policyName"),
                }
            )

        readable_output = tableToMarkdown(
            "Skyhigh Security Incidents", readable_dict, headerTransform=pascalToSpace, removeNull=True
        )

        return CommandResults(
            outputs=incidents,
            outputs_prefix="SkyhighSecurity.Incident",
            outputs_key_field="incidentId",
            readable_output=readable_output,
            raw_response=incidents,
        )
    else:
        return CommandResults(
            readable_output="No Incidents were found with the requested filters.",
        )


def status_update_command(client: Client, args: dict) -> CommandResults:
    incident_ids = argToList(args.get("incident_ids"))
    status = str(args.get("status"))

    result = client.status_update(incident_ids, status)
    readable_output = "Status updated for user"

    return CommandResults(
        readable_output=readable_output,
        raw_response=result,
    )


def anomaly_activity_list_command(client: Client, args: dict) -> CommandResults:
    anomaly_id = arg_to_number(args.get("anomaly_id"))

    result = client.anomaly_activity_list(anomaly_id)
    if not result:
        return CommandResults(readable_output="No activities found for anomaly ID " + str(anomaly_id))

    anomaly_results = csv2json(result.decode("utf-8"))

    return CommandResults(
        outputs=result,
        outputs_prefix="SkyhighSecurity.Dictionaries",
        outputs_key_field="ID",
        readable_output=tableToMarkdown("Anomaly Activity List", anomaly_results),
        raw_response=result,
    )


def policy_dictionary_list_command(client: Client, args: dict) -> CommandResults:
    offset, limit = calculate_offset_and_limit(**args)
    names = argToList(args.get("name"))

    result = client.policy_dictionary_list()
    policies = result[offset:limit]

    filtered_policies = []
    for policy in policies:
        if (names and policy.get("name") in names) or not names:
            filtered_policies.append(
                {
                    "ID": policy.get("id"),
                    "Name": policy.get("name"),
                    "LastModified": policy.get("last_modified_time"),
                }
            )

    readable_output = tableToMarkdown(
        "List of Skyhigh Security Policies", filtered_policies, headerTransform=pascalToSpace, removeNull=True
    )

    return CommandResults(
        outputs=filtered_policies,
        outputs_prefix="SkyhighSecurity.Dictionaries",
        outputs_key_field="ID",
        readable_output=readable_output,
        raw_response=filtered_policies,
    )


def policy_dictionary_update_command(client: Client, args: dict) -> CommandResults:
    dict_id = arg_to_number(args.get("dictionary_id"))
    name = str(args.get("name"))
    content = str(args.get("content"))

    result = client.policy_dictionary_update(dict_id, name, content)

    return CommandResults(readable_output=f"Dictionary id: {dict_id} was updated.", raw_response=result)


""" MAIN FUNCTION """


def main() -> None:
    """main function, parses params and runs command functions

    :return:
    :rtype:
    """

    params = demisto.params()
    base_url = urljoin(params["url"].removesuffix("/"), "/shnapi/rest")
    verify_certificate = not params.get("insecure", False)
    credentials = params.get("credentials", {})
    handle_proxy()
    command = demisto.command()

    demisto.debug(f"Command being called is {command}")

    try:
        commands: dict = {
            "skyhigh-security-incident-query": incident_query_command,
            "skyhigh-security-incident-status-update": status_update_command,
            "skyhigh-security-anomaly-activity-list": anomaly_activity_list_command,
            "skyhigh-security-policy-dictionary-list": policy_dictionary_list_command,
            "skyhigh-security-policy-dictionary-update": policy_dictionary_update_command,
        }

        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            auth=(credentials.get("identifier"), credentials.get("password")),
            proxy=params.get("proxy"),
        )

        if command == "test-module":
            result = test_module(client)
            return_results(result)

        if command == "fetch-incidents":
            last_run, incidents = fetch_incidents(client, params)
            demisto.setLastRun(last_run)
            demisto.incidents(incidents)

        elif command in commands:
            return_results(commands[command](client, demisto.args()))

    # Log exceptions and return errors
    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()