KOI
KOI is an endpoint security platform that provides visibility and control over browser extensions, SaaS applications, and web-based threats.
Endpoint · KOI
Details
| ID | KOI |
|---|---|
| Provider | KOI |
| Category | Endpoint |
| From Version | 6.10.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
README
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.
koi-get-events
Gets events from KOI. This command is used for developing/debugging. Use with caution, as it can create events, leading to event duplication and exceeding API request limitations.
Base Command
koi-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| event_type | The type of events to retrieve. If not specified, uses the value configured in the integration parameters. Possible values are: Alerts, Audit. Default is Alerts,Audit. | Optional |
| limit | The maximum number of events to return per type. Default is 50. | Optional |
| start_time | Filter events created at or after this time. Supports ISO 8601 format or relative time expressions (e.g., “3 days ago”, “2024-01-01T00:00:00Z”). | Optional |
| end_time | Filter events created at or before this time. Supports ISO 8601 format or relative time expressions (e.g., “now”, “2024-01-01T00:00:00Z”). | Optional |
| should_push_events | The flag that indicates whether to push events to Cortex XSIAM. Pushing events is supported on Cortex XSIAM only. When set to false, or on non-XSIAM platforms, events are displayed without being pushed. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| KOI.Event.id | String | The unique identifier of the event. |
| KOI.Event.source_log_type | String | The source log type of the event (Alerts or Audit). |
| KOI.Event._time | Date | The timestamp of the event in ISO 8601 format. |
| KOI.Event.created_at | Date | The creation time of the event (audit logs). |
Human Readable Output
KOI Events
id source_log_type _time severity status alert-001 Alerts 2024-01-01T00:00:00Z high open audit-001 Audit 2024-01-01T00:00:00Z
koi-blocklist-get
Retrieves all items in the blocklist.
Base Command
koi-blocklist-get
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Blocklist.item_id | String | The unique identifier of the blocklist item. |
| Koi.Blocklist.item_name | String | The name of the blocklist item. |
| Koi.Blocklist.item_display_name | String | The display name of the blocklist item. |
| Koi.Blocklist.marketplace | String | The marketplace of the blocklist item (e.g., vscode). |
| Koi.Blocklist.publisher_name | String | The publisher name of the blocklist item. |
| Koi.Blocklist.package_name | String | The package name of the blocklist item. |
| Koi.Blocklist.notes | String | Notes associated with the blocklist item. |
| Koi.Blocklist.created_by | String | The user who created the blocklist item. |
| Koi.Blocklist.created_at | Date | The creation time of the blocklist item in ISO 8601 format. |
Command example
!koi-blocklist-get
Human Readable Output
KOI Blocklist
Item Id Item Name Item Display Name Marketplace Publisher Name Package Name Notes Created By Created At mal-001 Bad Extension Malicious Extension chrome_web_store Suspicious Publisher bad-package Known malware distribution security@example.com 2025-05-01T09:15:00.000Z mal-002 Risky Plugin Risky Plugin vscode Unknown Publisher risky-plugin Data exfiltration risk admin@example.com 2025-05-02T14:30:00.000Z
koi-allowlist-get
Retrieves all items in the allowlist.
Base Command
koi-allowlist-get
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Allowlist.item_id | String | The unique identifier of the allowlist item. |
| Koi.Allowlist.item_name | String | The name of the allowlist item. |
| Koi.Allowlist.item_display_name | String | The display name of the allowlist item. |
| Koi.Allowlist.marketplace | String | The marketplace of the allowlist item (e.g., vscode). |
| Koi.Allowlist.publisher_name | String | The publisher name of the allowlist item. |
| Koi.Allowlist.package_name | String | The package name of the allowlist item. |
| Koi.Allowlist.notes | String | Notes associated with the allowlist item. |
| Koi.Allowlist.created_by | String | The user who created the allowlist item. |
| Koi.Allowlist.created_at | Date | The creation time of the allowlist item in ISO 8601 format. |
Command example
!koi-allowlist-get
Human Readable Output
KOI Allowlist
Item Id Item Name Item Display Name Marketplace Publisher Name Package Name Notes Created By Created At ext-123 My Extension My Extension Display Name vscode My Publisher my-package Approved for development purposes admin@example.com 2025-04-23T17:22:24.023Z ext-456 Another Ext Another Extension chrome Another Publisher another-package Approved by security team user@example.com 2025-04-24T10:00:00.000Z
koi-inventory-search
Searches inventory items using advanced query builder filters. Provide a filter via the ‘filter_json’ argument (inline JSON string) or the ‘filter_raw_json_entry_id’ argument (War Room file entry ID). At least one filter source must be provided.
Base Command
koi-inventory-search
Input
| Argument Name | Description | Required |
|---|---|---|
| filter_json | Advanced filter using query builder syntax as a JSON string. Either the ‘filter_json’ or the ‘filter_raw_json_entry_id’ argument must be provided. | Optional |
| filter_raw_json_entry_id | War Room entry ID of a JSON file containing the filter object. Takes priority over the ‘filter_json’ argument when both are provided. | Optional |
| page | Page number for pagination (1-based). When provided, fetches a single page and ignores the ‘limit’ argument. | Optional |
| page_size | Number of results per page (default: 50, max: 500). Used in single-page mode with the ‘page’ argument. | Optional |
| limit | Maximum total number of inventory items to return (default: 50, max: 1000). When provided without the ‘page’ argument, auto-paginates to collect up to this many items. Default is 50. | Optional |
| sort_by | Column to sort by. Possible values are: first_seen, last_seen, item_display_name, item_id, version, marketplace, endpoint_count, risk, risk_level, status, installs_count, released_at, publisher_name. Default is first_seen. | Optional |
| sort_direction | Sort direction. Possible values are: asc, desc. Default is desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Inventory.item_id | String | The unique identifier of the inventory item. |
| Koi.Inventory.item_display_name | String | The display name of the inventory item. |
| Koi.Inventory.marketplace | String | The marketplace source of the item. |
| Koi.Inventory.platforms | Unknown | List of platforms where the item is installed. |
| Koi.Inventory.publisher_name | String | The publisher name of the item. |
| Koi.Inventory.risk | Number | The numeric risk score of the item. |
| Koi.Inventory.risk_level | String | The risk level classification of the item. |
| Koi.Inventory.version | String | The version of the item. |
| Koi.Inventory.status | String | The governance status of the item. |
| Koi.Inventory.endpoint_count | Number | The number of endpoints where the item is installed. |
| Koi.Inventory.installs_count | Number | The total number of installs for the item. |
| Koi.Inventory.first_seen | Date | The date the item was first seen in ISO 8601 format. |
| Koi.Inventory.last_seen | Date | The date the item was last seen in ISO 8601 format. |
| Koi.Inventory.last_used | Date | The date the item was last used in ISO 8601 format. |
| Koi.Inventory.installation_method | String | The method used to install the item. |
| Koi.Inventory.short_description | String | A short description of the item. |
| Koi.Inventory.is_first_party | Boolean | Whether the item is a first-party item. |
| Koi.Inventory.is_signed | Boolean | Whether the item is signed. |
| Koi.Inventory.categories | Unknown | List of categories the item belongs to. |
| Koi.Inventory.findings | Unknown | List of findings associated with the item. |
| Koi.Inventory.governed_details | Unknown | Governance policy details for the item. |
| Koi.Inventory.released_at | Date | The release date of the item. Format: YYYY-MM-DD (e.g., 2023-01-15). |
| Koi.Inventory.brew_category_koi | String | The Homebrew package category (Koi classification). |
| Koi.Inventory.browser_category_koi | String | The browser extension category (Koi classification). |
| Koi.Inventory.chocolatey_category_koi | String | The Chocolatey package category (Koi classification). |
| Koi.Inventory.ide_category_koi | String | The IDE extension category (Koi classification). |
| Koi.Inventory.software_category_koi | String | The software category (Koi classification). |
Command example
!koi-inventory-search filter_json="{\"field\":\"risk_level\",\"operator\":\"eq\",\"value\":\"high\"}" limit=50
Human Readable Output
KOI Inventory Search
Item Id Item Display Name Marketplace Platforms Publisher Name Risk Risk Level Version Status Endpoint Count Installs Count Installation Method Is First Party Is Signed First Seen Last Seen Last Used Released At Short Description Categories Findings abc123 React Developer Tools chrome_web_store chrome, edge Meta 5 high 1.0.0 APPROVED 42 1000000 marketplace false true 2024-01-01T10:00:00Z 2024-10-15T10:00:00Z 2025-06-15T10:00:00Z 2023-01-15 React debugging tools Developer Tools malware, permissions
koi-policy-list
Retrieves a list of all policies. Use the ‘page’ and ‘page_size’ arguments to fetch a specific page, or use the ‘limit’ argument to auto-paginate and collect up to the specified number of policies. If the ‘page’ argument is provided, the ‘limit’ argument is ignored.
Base Command
koi-policy-list
Input
| Argument Name | Description | Required |
|---|---|---|
| page | Page number for pagination (1-based). When provided, fetches a single page and ignores the ‘limit’ argument. | Optional |
| page_size | Number of results per page (default: 50, max: 500). Used only in single-page mode together with the ‘page’ argument. | Optional |
| limit | Maximum total number of policies to return (default: 50, max: 1000). When provided without the ‘page’ argument, auto-paginates to collect up to this many policies. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Policy.id | Number | The unique identifier of the policy. |
| Koi.Policy.name | String | The name of the policy. |
| Koi.Policy.description | String | The description of the policy. |
| Koi.Policy.action | String | The action taken by the policy (e.g., block). |
| Koi.Policy.enabled | Boolean | Whether the policy is enabled. |
| Koi.Policy.group_ids | Unknown | List of group IDs associated with the policy. |
| Koi.Policy.creator_fullname | String | The full name of the policy creator. |
| Koi.Policy.created_at | Date | The creation time of the policy in ISO 8601 format. |
| Koi.Policy.updated_at | Date | The last update time of the policy in ISO 8601 format. |
Command example
!koi-policy-list limit=50
Human Readable Output
KOI Policies
Id Name Description Action Enabled Group Ids Creator Fullname Created At Updated At 1 My Policy This policy blocks high-risk extensions block true 1, 2, 3 John Doe 2025-04-23T17:22:24.023Z 2025-04-23T17:22:24.023Z 2 Allow Policy This policy allows approved extensions allow false 4 Jane Smith 2025-04-24T10:00:00.000Z 2025-04-24T12:30:00.000Z
koi-inventory-item-get
Retrieves comprehensive details for a specific software item, extension, or package using its unique identifier, marketplace, and version.
Base Command
koi-inventory-item-get
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | Unique identifier for the item. | Required |
| marketplace | The marketplace where the item is hosted. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Required |
| version | The specific version of the item to retrieve. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Inventory.item_id | String | The unique identifier of the inventory item. |
| Koi.Inventory.item_display_name | String | The display name of the inventory item. |
| Koi.Inventory.marketplace | String | The marketplace source of the item. |
| Koi.Inventory.platforms | Unknown | List of platforms where the item is installed. |
| Koi.Inventory.publisher_name | String | The publisher name of the item. |
| Koi.Inventory.risk | Number | The numeric risk score of the item. |
| Koi.Inventory.risk_level | String | The risk level classification of the item. |
| Koi.Inventory.version | String | The version of the item. |
| Koi.Inventory.status | String | The governance status of the item. |
| Koi.Inventory.endpoint_count | Number | The number of endpoints where the item is installed. |
| Koi.Inventory.installs_count | Number | The total number of installs for the item. |
| Koi.Inventory.installation_method | String | The method used to install the item. |
| Koi.Inventory.is_first_party | Boolean | Whether the item is a first-party item. |
| Koi.Inventory.is_signed | Boolean | Whether the item is signed. |
| Koi.Inventory.first_seen | Date | The date the item was first seen in ISO 8601 format. |
| Koi.Inventory.last_seen | Date | The date the item was last seen in ISO 8601 format. |
| Koi.Inventory.last_used | Date | The date the item was last used in ISO 8601 format. |
| Koi.Inventory.released_at | Date | The release date of the item. Format: YYYY-MM-DD (e.g., 2023-01-15). |
| Koi.Inventory.short_description | String | A short description of the item. |
| Koi.Inventory.categories | Unknown | List of categories the item belongs to. |
| Koi.Inventory.findings | Unknown | List of findings associated with the item including severity and evidence. |
| Koi.Inventory.governed_details | Unknown | Governance policy details for the item. |
| Koi.Inventory.brew_category_koi | String | The Homebrew package category (Koi classification). |
| Koi.Inventory.browser_category_koi | String | The browser extension category (Koi classification). |
| Koi.Inventory.chocolatey_category_koi | String | The Chocolatey package category (Koi classification). |
| Koi.Inventory.ide_category_koi | String | The IDE extension category (Koi classification). |
| Koi.Inventory.software_category_koi | String | The software category (Koi classification). |
Command example
!koi-inventory-item-get item_id=example-extension marketplace=vscode
Human Readable Output
KOI Inventory Item
Item Id Item Display Name Marketplace Platforms Publisher Name Risk Risk Level Version Status Endpoint Count Installs Count Installation Method Is First Party Is Signed First Seen Last Seen Last Used Released At Short Description Categories Findings abc123 React Developer Tools chrome_web_store chrome, edge Meta 5 high 1.0.0 Allowed 42 1000000 marketplace false true 2024-01-01T10:00:00Z 2024-10-15T10:00:00Z 2025-06-15T10:00:00Z 2023-01-15 React debugging tools Developer Tools {‘description’: ‘This item contains malware’, ‘evidence’: {}, ‘finding_id’: ‘malware_detected’, ‘finding_name’: ‘Malware Detected’, ‘severity’: ‘critical’}
koi-blocklist-items-add
Adds one or more items to the global blocklist. Provide either the ‘item_id’ and ‘marketplace’ arguments for a single item, or the ‘items_list_raw_json_entry_id’ argument for bulk addition from a JSON file.
Base Command
koi-blocklist-items-add
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | The ID of the item to add to the blocklist. Required when not using the ‘items_list_raw_json_entry_id’ argument. | Optional |
| marketplace | The source marketplace of the item. Required when not using the ‘items_list_raw_json_entry_id’ argument. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Optional |
| created_by | Email of the user who created this entry. | Optional |
| notes | Additional notes or justification for blocking the item. | Optional |
| items_list_raw_json_entry_id | War Room entry ID of a JSON file containing a list of items to add. Each item must have “item_id” and “marketplace” fields. Optional fields: “created_by”, “notes”. When provided, the ‘item_id’ and ‘marketplace’ arguments are ignored. | Optional |
Context Output
There is no context output for this command.
Command example
!koi-blocklist-items-add item_id=malicious-ext marketplace=chrome_web_store notes="Blocked due to security risk"
Human Readable Output
Blocklist item ‘malicious-ext’ (marketplace: chrome_web_store) was added successfully.
koi-policy-status-update
Enables or disables a policy by ID.
Base Command
koi-policy-status-update
Input
| Argument Name | Description | Required |
|---|---|---|
| policy_id | The ID of the policy to update. | Required |
| enabled | Whether to enable (true) or disable (false) the policy. Possible values are: true, false. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Policy.id | Number | The unique identifier of the policy. |
| Koi.Policy.name | String | The name of the policy. |
| Koi.Policy.description | String | The description of the policy. |
| Koi.Policy.action | String | The action taken by the policy (e.g., block). |
| Koi.Policy.enabled | Boolean | Whether the policy is enabled. |
| Koi.Policy.group_ids | Unknown | List of group IDs associated with the policy. |
| Koi.Policy.creator_fullname | String | The full name of the policy creator. |
| Koi.Policy.created_at | Date | The creation time of the policy in ISO 8601 format. |
| Koi.Policy.updated_at | Date | The last update time of the policy in ISO 8601 format. |
Command example
!koi-policy-status-update policy_id=1 enabled=true
Human Readable Output
KOI Policy Updated
Id Name Description Action Enabled Group Ids Creator Fullname Created At Updated At 1 My Policy This policy blocks high-risk extensions block true 1, 2, 3 John Doe 2025-04-23T17:22:24.023Z 2025-04-23T17:22:24.023Z
koi-inventory-list
Retrieves a paginated list of items installed across your organization’s endpoints. Supports extensive filtering by marketplace, platform, risk level, publisher, and specific categories.
Base Command
koi-inventory-list
Input
| Argument Name | Description | Required |
|---|---|---|
| page | Page number for pagination (1-based). When provided, fetches a single page and ignores the ‘limit’ argument. | Optional |
| page_size | Number of results per page (default: 50, max: 500). Used in single-page mode with the ‘page’ argument. | Optional |
| limit | Maximum total number of inventory items to return (default: 50, max: 1000). When provided without the ‘page’ argument, auto-paginates to collect up to this many items. Default is 50. | Optional |
| brew_category_koi | Filter by Homebrew package category (Koi classification). | Optional |
| browser_category_koi | Filter by browser extension category (Koi classification). | Optional |
| chocolatey_category_koi | Filter by Chocolatey package category (Koi classification). | Optional |
| device_id | Filter devices by device ID. | Optional |
| finding_id | Filter devices by finding ID. | Optional |
| first_seen | Filter by first seen date (items first seen on or after this date). ISO 8601 format (e.g., “2024-01-01T00:00:00Z”). | Optional |
| ide_category_koi | Filter by IDE extension category (Koi classification). | Optional |
| installation_method | Filter by installation method. Possible values are: marketplace, manual, built_in, side_loaded. | Optional |
| item_display_name | Filter by item display name. Performs case-insensitive partial match. | Optional |
| item_id | Filter by item ID. | Optional |
| marketplace | Filter by marketplace. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Optional |
| platform | Filter by platform. Possible values are: antigravity, aqua, arc, brave, brew, chatgpt_atlas, chocolatey, chrome, chromium, claude, clion, codex, comet, cursor, datagrip, dataspell, dia, edge, excel, firefox, fleet, goland, hugging_face, intellij_community, intellij, kiro, mac, npm, notepad++, opera, outlook, phpstorm, powerpoint, prisma_access_browser, pycharm, pypi, rider, rubymine, rustrover, vscode, webstorm, windsurf, word, windows, writerside. | Optional |
| publisher_name | Filter by publisher name. Performs case-insensitive partial match. | Optional |
| risk_level | Filter by risk level. Possible values are: low, medium, high, critical, pending. | Optional |
| software_category_koi | Filter by software category (Koi classification). | Optional |
| sort_by | Column to sort by. Possible values are: first_seen, last_seen, item_display_name, item_id, version, marketplace, endpoint_count, risk, risk_level, status, installs_count, released_at, publisher_name. Default is first_seen. | Optional |
| sort_direction | Sort direction. Possible values are: asc, desc. | Optional |
| view | Filter by predefined view (marketplace group). Possible values are: agentic_ai, ai_models, code_packages, extensions, os_packages, software. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Inventory.item_id | String | The unique identifier of the inventory item. |
| Koi.Inventory.item_display_name | String | The display name of the inventory item. |
| Koi.Inventory.marketplace | String | The marketplace source of the item. |
| Koi.Inventory.platforms | Unknown | List of platforms where the item is installed. |
| Koi.Inventory.publisher_name | String | The publisher name of the item. |
| Koi.Inventory.risk | Number | The numeric risk score of the item. |
| Koi.Inventory.risk_level | String | The risk level classification of the item. |
| Koi.Inventory.version | String | The version of the item. |
| Koi.Inventory.status | String | The governance status of the item. |
| Koi.Inventory.endpoint_count | Number | The number of endpoints where the item is installed. |
| Koi.Inventory.installs_count | Number | The total number of installs for the item. |
| Koi.Inventory.first_seen | Date | The date the item was first seen in ISO 8601 format. |
| Koi.Inventory.last_seen | Date | The date the item was last seen in ISO 8601 format. |
| Koi.Inventory.last_used | Date | The date the item was last used in ISO 8601 format. |
| Koi.Inventory.installation_method | String | The method used to install the item. |
| Koi.Inventory.short_description | String | A short description of the item. |
| Koi.Inventory.is_first_party | Boolean | Whether the item is a first-party item. |
| Koi.Inventory.is_signed | Boolean | Whether the item is signed. |
| Koi.Inventory.categories | Unknown | List of categories the item belongs to. |
| Koi.Inventory.findings | Unknown | List of findings associated with the item. |
| Koi.Inventory.governed_details | Unknown | Governance policy details for the item. |
| Koi.Inventory.released_at | Date | The release date of the item. Format: YYYY-MM-DD (e.g., 2023-01-15). |
| Koi.Inventory.brew_category_koi | String | The Homebrew package category (Koi classification). |
| Koi.Inventory.browser_category_koi | String | The browser extension category (Koi classification). |
| Koi.Inventory.chocolatey_category_koi | String | The Chocolatey package category (Koi classification). |
| Koi.Inventory.ide_category_koi | String | The IDE extension category (Koi classification). |
| Koi.Inventory.software_category_koi | String | The software category (Koi classification). |
Command example
!koi-inventory-list limit=50 marketplace=vscode sort_by=first_seen sort_direction=desc
Human Readable Output
KOI Inventory
Item Id Item Display Name Marketplace Platforms Publisher Name Risk Risk Level Version Status Endpoint Count Installs Count Installation Method Is First Party Is Signed First Seen Last Seen Last Used Released At Short Description Categories Findings abc123 React Developer Tools chrome_web_store chrome, edge Meta 5 high 1.0.0 APPROVED 42 1000000 marketplace false true 2024-01-01T10:00:00Z 2024-10-15T10:00:00Z 2025-06-15T10:00:00Z 2023-01-15 React debugging tools Developer Tools malware, permissions def456 Prettier - Code formatter vscode vscode Prettier 2 low 10.1.0 APPROVED 15 500000 manual true true 2024-03-10T08:30:00Z 2024-11-01T14:00:00Z 2022-06-01 Code formatter using prettier Productivity
koi-inventory-item-endpoints-list
Retrieves a paginated list of endpoints that have a specific item installed.
Base Command
koi-inventory-item-endpoints-list
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | Unique identifier for the item. | Required |
| marketplace | The marketplace where the item is hosted. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Required |
| version | The specific version of the item. | Required |
| page | Page number for pagination (1-based). When provided, fetches a single page and ignores the ‘limit’ argument. | Optional |
| page_size | Number of results per page (default: 50, max: 500). Used in single-page mode with the ‘page’ argument. | Optional |
| limit | Maximum total number of endpoints to return (default: 50, max: 1000). When provided without the ‘page’ argument, auto-paginates to collect up to this many endpoints. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Koi.Inventory.Endpoint.id | String | The unique identifier of the endpoint device. |
| Koi.Inventory.Endpoint.hostname | String | The hostname of the endpoint. |
| Koi.Inventory.Endpoint.os | String | The operating system of the endpoint. |
| Koi.Inventory.Endpoint.platform | String | The platform where the item is installed on this endpoint. |
| Koi.Inventory.Endpoint.serial | String | The serial number of the endpoint device. |
| Koi.Inventory.Endpoint.last_logged_on_user | String | The last logged on user of the endpoint. |
| Koi.Inventory.Endpoint.activation_status | String | The activation status of the endpoint. |
| Koi.Inventory.Endpoint.path | String | The installation path of the item on the endpoint. |
| Koi.Inventory.Endpoint.first_seen | Date | The date the item was first seen on this endpoint in ISO 8601 format. |
| Koi.Inventory.Endpoint.last_seen | Date | The date the item was last seen on this endpoint in ISO 8601 format. |
Command example
!koi-inventory-item-endpoints-list item_id=example-extension marketplace=vscode limit=50
Human Readable Output
KOI Inventory Item Endpoints
Id Hostname Os Platform Serial Last Logged On User Activation Status Path First Seen Last Seen device-123 laptop-01 windows chrome ABC123XYZ john.doe enabled /Applications/Google Chrome.app/Contents/Extensions/abc123 2024-01-01T10:00:00Z 2024-10-15T10:00:00Z device-456 desktop-02 macos chrome DEF456UVW jane.smith enabled /Users/jane/Library/Application Support/Google/Chrome/Extensions/abc123 2024-02-15T08:30:00Z 2024-11-01T14:00:00Z
koi-blocklist-items-remove
Removes one or more items from the global blocklist. Provide either the ‘item_id’ and ‘marketplace’ arguments for a single item, or the ‘items_list_raw_json_entry_id’ argument for bulk removal from a JSON file.
Base Command
koi-blocklist-items-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | The ID of the item to remove from the blocklist. Required when not using the ‘items_list_raw_json_entry_id’ argument. | Optional |
| marketplace | The source marketplace of the item. Required when not using the ‘items_list_raw_json_entry_id’ argument. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Optional |
| created_by | Email of the user who created this entry. | Optional |
| notes | Additional notes about the removal. | Optional |
| items_list_raw_json_entry_id | War Room entry ID of a JSON file containing a list of items to remove. Each item must have “item_id” and “marketplace” fields. Optional fields: “created_by”, “notes”. When provided, the ‘item_id’ and ‘marketplace’ arguments are ignored. | Optional |
Context Output
There is no context output for this command.
Command example
!koi-blocklist-items-remove item_id=malicious-ext marketplace=chrome_web_store
Human Readable Output
Blocklist item ‘malicious-ext’ (marketplace: chrome_web_store) was removed successfully.
koi-allowlist-items-remove
Removes one or more items from the global allowlist. Provide either the ‘item_id’ and ‘marketplace’ arguments for a single item, or the ‘items_list_raw_json_entry_id’ argument for bulk removal from a JSON file.
Base Command
koi-allowlist-items-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | The ID of the item to remove from the allowlist. Required when not using the ‘items_list_raw_json_entry_id’ argument. | Optional |
| marketplace | The source marketplace of the item. Required when not using the ‘items_list_raw_json_entry_id’ argument. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Optional |
| created_by | Email of the user who created this entry. | Optional |
| notes | Additional notes about the removal. | Optional |
| items_list_raw_json_entry_id | War Room entry ID of a JSON file containing a list of items to remove. Each item must have “item_id” and “marketplace” fields. Optional fields: “created_by”, “notes”. When provided, the ‘item_id’ and ‘marketplace’ arguments are ignored. | Optional |
Context Output
There is no context output for this command.
Command example
!koi-allowlist-items-remove item_id=example-extension marketplace=vscode
Human Readable Output
Allowlist item ‘example-extension’ (marketplace: vscode) was removed successfully.
koi-allowlist-items-add
Adds one or more items to the global allowlist. Provide either the ‘item_id’ and ‘marketplace’ arguments for a single item, or the ‘items_list_raw_json_entry_id’ argument for bulk addition from a JSON file.
Base Command
koi-allowlist-items-add
Input
| Argument Name | Description | Required |
|---|---|---|
| item_id | The ID of the item to add to the allowlist. Required when not using the ‘items_list_raw_json_entry_id’ argument. | Optional |
| marketplace | The source marketplace of the item. Required when not using the ‘items_list_raw_json_entry_id’ argument. Possible values are: chocolatey, chrome_web_store, claude_desktop_extensions, cursor, docker, edge_add_ons, firefox_add_ons, github_mcp_registry, homebrew, hugging_face, jetbrains, linux, mac, notepad++, npm, office_add_ins, open_vsx_registry, pypi, visual_studio, vscode, windows, windsurf. | Optional |
| created_by | Email of the user who created this entry. | Optional |
| notes | Additional notes about the entry. | Optional |
| items_list_raw_json_entry_id | War Room entry ID of a JSON file containing a list of items to add. Each item must have “item_id” and “marketplace” fields. Optional fields: “created_by”, “notes”. When provided, the ‘item_id’ and ‘marketplace’ arguments are ignored. | Optional |
Context Output
There is no context output for this command.
Command example
!koi-allowlist-items-add item_id=example-extension marketplace=vscode notes="Approved by security team"
Human Readable Output
Allowlist item ‘example-extension’ (marketplace: vscode) was added successfully.
Configuration parameters
url— Server URL (required)api_key— API Key (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetchEvents— Fetch eventsevent_types_to_fetch— Fetch event types (required)audit_types_filter— Audit log type filtermax_fetch— Maximum number of events per fetcheventFetchInterval— Events Fetch Interval
Commands (13)
-
koi-allowlist-getRetrieves all items in the allowlist.
-
koi-allowlist-items-addAdds one or more items to the global allowlist. Provide either 'item_id' and 'marketplace' for a single item, or 'items_list_raw_json_entry_id' for bulk addition from a JSON file.
-
koi-allowlist-items-removeRemoves one or more items from the global allowlist. Provide either 'item_id' and 'marketplace' for a single item, or 'items_list_raw_json_entry_id' for bulk removal from a JSON file.
-
koi-blocklist-getRetrieves all items in the blocklist.
-
koi-blocklist-items-addAdds one or more items to the global blocklist. Provide either 'item_id' and 'marketplace' for a single item, or 'items_list_raw_json_entry_id' for bulk addition from a JSON file.
-
koi-blocklist-items-removeRemoves one or more items from the global blocklist. Provide either 'item_id' and 'marketplace' for a single item, or 'items_list_raw_json_entry_id' for bulk removal from a JSON file.
-
koi-get-eventsGets events from KOI. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
-
koi-inventory-item-endpoints-listRetrieves a paginated list of endpoints that have a specific item installed.
-
koi-inventory-item-getRetrieves comprehensive details for a specific software item, extension, or package using its unique identifier, marketplace, and version.
-
koi-inventory-listRetrieves a paginated list of items installed across your organization's endpoints. Supports extensive filtering by marketplace, platform, risk level, publisher, and specific categories.
-
koi-inventory-searchSearches inventory items using advanced query builder filters. Provide a filter via 'filter_json' (inline JSON string) or 'filter_raw_json_entry_id' (War Room file entry ID). At least one filter source must be provided.
-
koi-policy-listRetrieves a list of all policies. Use 'page' and 'page_size' to fetch a specific page, or use 'limit' to auto-paginate and collect up to the specified number of policies. If 'page' is provided, 'limit' is ignored.
-
koi-policy-status-updateEnables or disables a policy by ID.
import json import traceback from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from datetime import datetime, timedelta, UTC from enum import Enum from typing import Any import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa from ContentClientApiModule import * # Disable insecure warnings urllib3.disable_warnings() """ KOI Integration for fetching Alerts and Audit Logs from the KOI API. """ # region Constants and helpers # ================================= # Constants and helpers # ================================= INTEGRATION_NAME = "KOI" class ApiPaths: """Centralized KOI API endpoint paths. All paths are relative to the KOI base URL configured in integration parameters. Use the classmethods for parameterized routes (e.g., a specific policy or item) so URL construction lives in exactly one place. """ BASE = "/api/external/v2" ALERTS = f"{BASE}/alerts" AUDIT_LOGS = f"{BASE}/audit-logs" POLICIES = f"{BASE}/policies" ALLOWLIST = f"{BASE}/policies/allowlist" BLOCKLIST = f"{BASE}/policies/blocklist" INVENTORY = f"{BASE}/inventory" INVENTORY_SEARCH = f"{BASE}/inventory/search" @classmethod def policy(cls, policy_id: int) -> str: """Return the path for a specific policy by ID.""" return f"{cls.POLICIES}/{policy_id}" @classmethod def inventory_item(cls, item_id: str) -> str: """Return the path for a specific inventory item by ID.""" return f"{cls.INVENTORY}/{item_id}" @classmethod def inventory_item_endpoints(cls, item_id: str) -> str: """Return the path for the endpoints of a specific inventory item.""" return f"{cls.INVENTORY}/{item_id}/endpoints" class Config: """Global static configuration.""" VENDOR = "koi" PRODUCT = "koi" # Date format for API requests (ISO 8601) DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # Pagination DEFAULT_PAGE_SIZE = 50 MAX_PAGE_SIZE = 500 MAX_PAGES_PER_FETCH = 10 DEFAULT_PAGE = 1 DEFAULT_LIMIT = 50 MAX_LIMIT = 1000 # Fetch defaults DEFAULT_MAX_FETCH = 5000 # Default lookback time for first fetch or get-events command DEFAULT_FROM_TIME = "5 minutes ago" # API sort direction for chronological ordering SORT_DIRECTION = "asc" # Test module settings TEST_MODULE_LOOKBACK_MINUTES = 5 TEST_MODULE_MAX_EVENTS = 1 class LogType(Enum): """Enum to hold all configuration for different log types.""" ALERTS = ("alerts", "Alerts", ApiPaths.ALERTS) AUDIT = ("audit", "Audit", ApiPaths.AUDIT_LOGS) def __init__(self, type_string: str, title: str, api_endpoint: str): self.type_string = type_string self.title = title self.api_endpoint = api_endpoint # Valid audit log type filters VALID_AUDIT_TYPES = [ "approval_requests", "devices", "endpoints", "extensions", "firewall", "guardrails", "notifications", "policies", "remediation", "requests", "settings", "vetting", ] # Valid marketplace values for allowlist operations VALID_MARKETPLACES = [ "chocolatey", "chrome_web_store", "claude_desktop_extensions", "cursor", "docker", "edge_add_ons", "firefox_add_ons", "github_mcp_registry", "homebrew", "hugging_face", "jetbrains", "linux", "mac", "notepad++", "npm", "office_add_ins", "open_vsx_registry", "pypi", "visual_studio", "vscode", "windows", "windsurf", ] def get_formatted_utc_time(date_input: str | None) -> str: """Parse input and return the formatted UTC time string for KOI API. Args: date_input: Date string to parse (e.g., '3 days ago', '2024-01-01T00:00:00Z') Returns: Formatted UTC time string in ISO 8601 format. """ parsed_dt = parse_date_or_use_current(date_input) formatted_time = parsed_dt.strftime(Config.DATE_FORMAT) demisto.debug(f"[Date Helper] Input: '{date_input}' -> Output: '{formatted_time}' (UTC)") return formatted_time def parse_date_or_use_current(date_string: str | None) -> datetime: """Parse a date string or return current UTC datetime if parsing fails. Uses arg_to_datetime from CommonServerPython for consistent date parsing. Args: date_string: Date string to parse, or None to use current UTC time. Returns: Parsed datetime object in UTC. """ if not date_string: current_time = datetime.now(UTC) demisto.debug(f"[Date Helper] No input provided. Using current UTC: {current_time}") return current_time demisto.debug(f"[Date Helper] Attempting to parse date string: '{date_string}'") parsed_datetime = arg_to_datetime(arg=date_string, is_utc=True) if not parsed_datetime: demisto.debug(f"[Date Helper] Failed to parse '{date_string}'. Fallback to current UTC.") return datetime.now(UTC) demisto.debug(f"[Date Helper] Final parsed date: {parsed_datetime.isoformat()}") return parsed_datetime def get_log_types_from_titles(event_types_to_fetch: list[str]) -> list[LogType]: """Convert user-facing event type titles into LogType Enum members. Args: event_types_to_fetch: List of event type titles (e.g., ["Alerts", "Audit"]). Raises: DemistoException: If any of the provided event type titles are invalid. Returns: List of LogType Enum members. """ valid_titles = {lt.title for lt in LogType} invalid_types = [title for title in event_types_to_fetch if title not in valid_titles] if invalid_types: valid_options = ", ".join(sorted(valid_titles)) raise DemistoException( f"Invalid event type(s) provided: {invalid_types}. " f"Please select from the following list: {valid_options}" ) return [lt for lt in LogType if lt.title in event_types_to_fetch] def extract_time_from_event(event: dict, log_type: LogType) -> str | None: """Extract the time field value from an event based on log type. For alerts: finding_info.created_time (epoch ms) -> converted to ISO 8601. For audit logs: created_at (ISO 8601 string). Args: event: The event dictionary. log_type: The LogType Enum member. Returns: ISO 8601 formatted time string, or None if not found. """ if log_type == LogType.ALERTS: finding_info = event.get("finding_info", {}) created_time_ms = finding_info.get("created_time") if created_time_ms: try: dt = datetime.fromtimestamp(created_time_ms / 1000, tz=UTC) return dt.strftime(Config.DATE_FORMAT) except (ValueError, TypeError, OSError): demisto.debug(f"[Time Extract] Failed to parse alert created_time: {created_time_ms}") return None else: return event.get("created_at") return None def add_time_to_events(events: list[dict], log_type: LogType) -> None: """Add _time and source_log_type fields to events for XSIAM ingestion. Uses extract_time_from_event for consistent time extraction across all code paths. Args: events: List of event dictionaries to enrich. log_type: The LogType Enum member representing the source. """ for event in events: event_time = extract_time_from_event(event, log_type) if event_time: event["_time"] = event_time else: demisto.debug(f"[Event Time] WARNING: Event missing time field: {event.get('id', 'unknown')}") event["source_log_type"] = log_type.title def get_event_id(event: dict) -> str | None: """Extract the event ID from an event dictionary. Args: event: The event dictionary. Returns: The event ID string, or None if not found. """ for id_field in ("id", "alert_id", "log_id", "uuid"): event_id = event.get(id_field) if event_id: return str(event_id) return None def deduplicate_events(events: list[dict], last_fetched_ids: list[str]) -> list[dict]: """Remove already-processed events based on previously fetched IDs. Args: events: List of events to deduplicate. last_fetched_ids: List of event IDs from the previous run. Returns: List of new (non-duplicate) events. """ if not events: demisto.debug("[Dedup] No events to process") return events if not last_fetched_ids: demisto.debug("[Dedup] No deduplication needed (first run - no previous IDs)") return events demisto.debug(f"[Dedup] Checking {len(events)} events against {len(last_fetched_ids)} previously fetched IDs") fetched_ids_set = set(last_fetched_ids) new_events = [event for event in events if get_event_id(event) not in fetched_ids_set] skipped_count = len(events) - len(new_events) if skipped_count > 0: demisto.debug(f"[Dedup] Skipped {skipped_count} duplicates. {len(new_events)} new events remain.") else: demisto.debug("[Dedup] No duplicates found.") return new_events def parse_list_items_from_entry_id(entry_id: str) -> list[dict[str, Any]]: """Read and parse a JSON file from a War Room entry ID containing list items. The JSON file must contain a list of item objects, each with at least 'item_id' and 'marketplace'. Args: entry_id: The War Room entry ID of the uploaded JSON file. Returns: List of item dictionaries parsed from the JSON file. Raises: DemistoException: If the file cannot be read, parsed, or has invalid structure. """ try: filepath_result = demisto.getFilePath(entry_id) except Exception as e: raise DemistoException(f"Could not find file for entry ID '{entry_id}': {e}") if not filepath_result or not (file_path := filepath_result.get("path")): raise DemistoException(f"Entry ID '{entry_id}' is not a valid file entry.") demisto.debug(f"[File Parse] Reading items from file: {file_path}") try: with open(file_path, encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError as e: raise DemistoException(f"Failed to parse JSON file from entry ID '{entry_id}': {e}") except OSError as e: raise DemistoException(f"Failed to read file from entry ID '{entry_id}': {e}") if not isinstance(data, list): raise DemistoException( f"Invalid JSON structure in entry ID '{entry_id}': expected a list of items, got {type(data).__name__}." ) for i, item in enumerate(data): if not isinstance(item, dict): raise DemistoException(f"Invalid item at index {i}: expected a dictionary, got {type(item).__name__}.") if "item_id" not in item or "marketplace" not in item: raise DemistoException(f"Invalid item at index {i}: each item must contain 'item_id' and 'marketplace'.") if item["marketplace"] not in VALID_MARKETPLACES: raise DemistoException( f"Invalid marketplace '{item['marketplace']}' at index {i}. Valid values: {VALID_MARKETPLACES}" ) demisto.debug(f"[File Parse] Parsed {len(data)} items from entry ID '{entry_id}'") return data def resolve_items_from_args(args: dict[str, Any]) -> list[dict[str, Any]]: """Resolve list items from command arguments. Supports two input modes: - Bulk from file: 'items_list_raw_json_entry_id' with a War Room entry ID. - Single item: 'item_id' and 'marketplace' (with optional 'created_by' and 'notes'). File entry ID takes priority when both modes are provided. Args: args: Command arguments dictionary. Returns: List of item dictionaries. Raises: DemistoException: If neither mode provides valid input, or marketplace is invalid. """ entry_id: str | None = args.get("items_list_raw_json_entry_id") item_id: str | None = args.get("item_id") marketplace: str | None = args.get("marketplace") if entry_id: return parse_list_items_from_entry_id(entry_id) if item_id and marketplace: if marketplace not in VALID_MARKETPLACES: raise DemistoException(f"Invalid marketplace '{marketplace}'. Valid values: {VALID_MARKETPLACES}") item: dict[str, Any] = { "item_id": item_id, "marketplace": marketplace, } created_by: str | None = args.get("created_by") notes: str | None = args.get("notes") if created_by: item["created_by"] = created_by if notes: item["notes"] = notes return [item] raise DemistoException( "Either 'item_id' and 'marketplace' must be provided, or 'items_list_raw_json_entry_id' must be provided." ) def parse_filter_from_args(args: dict[str, Any]) -> dict[str, Any]: """Resolve a filter object from command arguments. Supports two input modes: - Inline JSON: 'filter_json' with a JSON string. - File upload: 'filter_raw_json_entry_id' with a War Room entry ID of a JSON file. File entry ID takes priority when both are provided. Args: args: Command arguments dictionary. Returns: Parsed filter dictionary. Raises: DemistoException: If no filter is provided, the JSON cannot be parsed, or the file cannot be read. """ entry_id: str | None = args.get("filter_raw_json_entry_id") filter_json: str | None = args.get("filter_json") if entry_id: try: filepath_result = demisto.getFilePath(entry_id) except Exception as e: raise DemistoException(f"Could not find file for entry ID '{entry_id}': {e}") if not filepath_result or "path" not in filepath_result: raise DemistoException(f"Entry ID '{entry_id}' is not a valid file entry.") file_path = filepath_result["path"] demisto.debug(f"[Filter Parse] Reading filter from file: {file_path}") try: with open(file_path, encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError as e: raise DemistoException(f"Failed to parse JSON filter file from entry ID '{entry_id}': {e}") except OSError as e: raise DemistoException(f"Failed to read filter file from entry ID '{entry_id}': {e}") if not isinstance(data, dict): raise DemistoException( f"Invalid filter JSON structure in entry ID '{entry_id}': " f"expected a dictionary, got {type(data).__name__}." ) demisto.debug(f"[Filter Parse] Parsed filter from file: {data}") return data if filter_json: try: data = json.loads(filter_json) except json.JSONDecodeError as e: raise DemistoException(f"Failed to parse filter_json: {e}") if not isinstance(data, dict): raise DemistoException(f"Invalid filter_json structure: expected a dictionary, got {type(data).__name__}.") demisto.debug(f"[Filter Parse] Parsed inline filter: {data}") return data raise DemistoException("Either 'filter_json' or 'filter_raw_json_entry_id' must be provided.") def parse_integration_params(params: dict[str, Any]) -> dict[str, Any]: """Parse and validate integration configuration parameters. Extracts connection settings from the raw demisto.params() dictionary and validates audit type filters if provided. Args: params: Raw parameters from demisto.params(). Returns: Validated configuration dictionary with keys: base_url, api_key, verify, proxy. Raises: DemistoException: If audit type filter contains invalid values. """ base_url = params.get("url", "https://api.prod.koi.security/").rstrip("/") api_key = params.get("api_key", {}) if isinstance(api_key, dict): api_key = api_key.get("password", "") verify_certificate = not argToBoolean(params.get("insecure", False)) proxy = argToBoolean(params.get("proxy", False)) # Validate audit types filter if provided audit_types_filter = argToList(params.get("audit_types_filter")) if audit_types_filter: invalid = [t for t in audit_types_filter if t not in VALID_AUDIT_TYPES] if invalid: raise DemistoException(f"Invalid audit log type(s): {invalid}. Valid types: {VALID_AUDIT_TYPES}") demisto.debug(f"[Config] URL: {base_url}") return { "base_url": base_url, "api_key": api_key, "verify": verify_certificate, "proxy": proxy, } # endregion # region Client # ================================= # Client # ================================= class Client(ContentClient): """KOI API client. Extends ContentClient with KOI-specific functionality including Bearer token authentication and API methods for alerts and audit logs. """ def __init__( self, base_url: str, api_key: str, verify: bool, proxy: bool, ): """Initialize the KOI client. Args: base_url: KOI API server URL. api_key: KOI API key for Bearer token authentication. verify: Whether to verify SSL certificates. proxy: Whether to use proxy settings. """ auth_handler = BearerTokenAuthHandler(token=api_key) retry_policy = RetryPolicy( # type: ignore[call-arg] max_attempts=4, retryable_status_codes=(429, 500, 502, 503, 504), ) super().__init__( base_url=base_url, verify=verify, proxy=proxy, auth_handler=auth_handler, client_name="KOI", timeout=60, retry_policy=retry_policy, ) def get_events_page( self, log_type: LogType, created_at_gte: str | None = None, created_at_lte: str | None = None, page: int = 1, page_size: int = Config.DEFAULT_PAGE_SIZE, audit_types: list[str] | None = None, ) -> list[dict]: """Fetch a single page of events from the KOI API. This is the single unified method used by all commands (test-module, fetch-events, get-events) to retrieve events from the API. Args: log_type: The LogType to fetch (ALERTS or AUDIT). created_at_gte: Filter events created at or after this datetime (ISO 8601). created_at_lte: Filter events created at or before this datetime (ISO 8601). page: Page number (1-based). page_size: Number of results per page (max 500). audit_types: Optional list of audit log types to filter by (only for AUDIT). Returns: List of event dictionaries from the API response. """ params: dict[str, Any] = { "page": page, "page_size": min(page_size, Config.MAX_PAGE_SIZE), "sort_direction": Config.SORT_DIRECTION, } if created_at_gte: params["created_at_gte"] = created_at_gte if created_at_lte: params["created_at_lte"] = created_at_lte if log_type == LogType.AUDIT and audit_types: params["types"] = ",".join(audit_types) demisto.debug(f"[API Fetch] {log_type.type_string} | Page: {page} | Params: {params}") response = self._http_request( method="GET", url_suffix=log_type.api_endpoint, params=params, ) events = response.get("alerts") or response.get("data") or response.get("items") or response.get("results") or [] demisto.debug(f"[API Fetch] {log_type.type_string} | Page {page}: {len(events)} events returned") return events def get_policies( self, page: int, page_size: int, ) -> dict[str, Any]: """Fetch a single page of policies from the Koi API. Args: page: Page number for pagination (1-based). page_size: Number of results per page (max 500). Returns: The full API response dictionary containing 'policies' list and 'total_count'. """ params: dict[str, Any] = { "page": page, "page_size": page_size, } demisto.debug(f"[API] Fetching policies | Params: {params}") response = self._http_request( method="GET", url_suffix=ApiPaths.POLICIES, params=params, ) demisto.debug("[API] Policies response received") return response def update_policy_status(self, policy_id: int, enabled: bool) -> dict[str, Any]: """Update the enabled/disabled status of a policy. Args: policy_id: The ID of the policy to update. enabled: Whether to enable (True) or disable (False) the policy. Returns: The full updated policy object from the API. """ url_suffix = ApiPaths.policy(policy_id) body: dict[str, Any] = {"enabled": enabled} demisto.debug(f"[API] Updating policy {policy_id} status to enabled={enabled}") response = self._http_request( method="PUT", url_suffix=url_suffix, json_data=body, ) demisto.debug(f"[API] Policy {policy_id} status updated successfully") return response def get_allowlist(self) -> dict[str, Any]: """Fetch all items in the allowlist from the Koi API. Returns: The full API response dictionary containing 'items' list. """ demisto.debug("[API] Fetching allowlist") response = self._http_request( method="GET", url_suffix=ApiPaths.ALLOWLIST, ) items = response.get("items", []) demisto.debug(f"[API] Allowlist response received: {len(items)} items") return response def get_blocklist(self) -> dict[str, Any]: """Fetch all items in the blocklist from the Koi API. Returns: The full API response dictionary containing 'items' list. """ demisto.debug("[API] Fetching blocklist") response = self._http_request( method="GET", url_suffix=ApiPaths.BLOCKLIST, ) items = response.get("items", []) demisto.debug(f"[API] Blocklist response received: {len(items)} items") return response def remove_allowlist_items( self, items: list[dict[str, Any]], ) -> None: """Remove one or more items from the global allowlist. Args: items: List of item dictionaries, each containing at least 'item_id' and 'marketplace'. """ body: dict[str, Any] = {"items": items} demisto.debug(f"[API] Removing {len(items)} allowlist item(s): {items}") self._http_request( method="DELETE", url_suffix=ApiPaths.ALLOWLIST, json_data=body, resp_type="response", ok_codes=(204,), ) demisto.debug(f"[API] Successfully removed {len(items)} allowlist item(s)") def add_allowlist_items( self, items: list[dict[str, Any]], ) -> None: """Add one or more items to the global allowlist. Args: items: List of item dictionaries, each containing at least 'item_id' and 'marketplace'. """ body: dict[str, Any] = {"items": items} demisto.debug(f"[API] Adding {len(items)} allowlist item(s): {items}") self._http_request( method="POST", url_suffix=ApiPaths.ALLOWLIST, json_data=body, resp_type="response", ok_codes=(204,), ) demisto.debug(f"[API] Successfully added {len(items)} allowlist item(s)") def remove_blocklist_items( self, items: list[dict[str, Any]], ) -> None: """Remove one or more items from the global blocklist. Args: items: List of item dictionaries, each containing at least 'item_id' and 'marketplace'. """ body: dict[str, Any] = {"items": items} demisto.debug(f"[API] Removing {len(items)} blocklist item(s): {items}") self._http_request( method="DELETE", url_suffix=ApiPaths.BLOCKLIST, json_data=body, resp_type="response", ok_codes=(204,), ) demisto.debug(f"[API] Successfully removed {len(items)} blocklist item(s)") def add_blocklist_items( self, items: list[dict[str, Any]], ) -> None: """Add one or more items to the global blocklist. Args: items: List of item dictionaries, each containing at least 'item_id' and 'marketplace'. """ body: dict[str, Any] = {"items": items} demisto.debug(f"[API] Adding {len(items)} blocklist item(s): {items}") self._http_request( method="POST", url_suffix=ApiPaths.BLOCKLIST, json_data=body, resp_type="response", ok_codes=(204,), ) demisto.debug(f"[API] Successfully added {len(items)} blocklist item(s)") def get_inventory( self, page: int, page_size: int, brew_category_koi: str | None = None, browser_category_koi: str | None = None, chocolatey_category_koi: str | None = None, device_id: str | None = None, finding_id: str | None = None, first_seen: str | None = None, ide_category_koi: str | None = None, installation_method: str | None = None, item_display_name: str | None = None, item_id: str | None = None, marketplace: str | None = None, platform: str | None = None, publisher_name: str | None = None, risk_level: str | None = None, software_category_koi: str | None = None, sort_by: str | None = None, sort_direction: str | None = None, view: str | None = None, ) -> dict[str, Any]: """Fetch a single page of inventory items from the Koi API. Args: page: Page number for pagination (1-based). page_size: Number of results per page (max 500). brew_category_koi: Filter by Homebrew package category (Koi classification). browser_category_koi: Filter by browser extension category (Koi classification). chocolatey_category_koi: Filter by Chocolatey package category (Koi classification). device_id: Filter devices by device id. finding_id: Filter devices by finding id. first_seen: Filter by first seen date (ISO 8601 format). ide_category_koi: Filter by IDE extension category (Koi classification). installation_method: Filter by installation method. item_display_name: Filter by item display name (case-insensitive partial match). item_id: Filter by item ID. marketplace: Filter by marketplace. platform: Filter by platform. publisher_name: Filter by publisher name (case-insensitive partial match). risk_level: Filter by risk level. software_category_koi: Filter by software category (Koi classification). sort_by: Column to sort by. sort_direction: Sort direction (asc or desc). view: Filter by predefined view (marketplace group). Returns: The full API response dictionary containing 'items' list and 'total_count'. """ params: dict[str, Any] = assign_params( page=page, page_size=page_size, brew_category_koi=brew_category_koi, browser_category_koi=browser_category_koi, chocolatey_category_koi=chocolatey_category_koi, device_id=device_id, finding_id=finding_id, first_seen=first_seen, ide_category_koi=ide_category_koi, installation_method=installation_method, item_display_name=item_display_name, item_id=item_id, marketplace=marketplace, platform=platform, publisher_name=publisher_name, risk_level=risk_level, software_category_koi=software_category_koi, sort_by=sort_by, sort_direction=sort_direction, view=view, ) demisto.debug(f"[API] Fetching inventory | Params: {params}") response = self._http_request( method="GET", url_suffix=ApiPaths.INVENTORY, params=params, ) demisto.debug("[API] Inventory response received") return response def get_inventory_item( self, item_id: str, marketplace: str, version: str, ) -> dict[str, Any]: """Fetch details for a specific inventory item from the Koi API. Args: item_id: Unique identifier for the item. marketplace: The marketplace where the item is hosted. version: The specific version of the item to retrieve. Returns: The full API response dictionary with item details. """ params: dict[str, Any] = { "marketplace": marketplace, "version": version, } url_suffix = ApiPaths.inventory_item(item_id) demisto.debug(f"[API] Fetching inventory item {item_id} | Params: {params}") response = self._http_request( method="GET", url_suffix=url_suffix, params=params, ) demisto.debug(f"[API] Inventory item {item_id} response received") return response def get_inventory_item_endpoints( self, item_id: str, marketplace: str, version: str, page: int, page_size: int, ) -> dict[str, Any]: """Fetch endpoints that have a specific inventory item installed. Args: item_id: Unique identifier for the item. marketplace: The marketplace where the item is hosted. version: The specific version of the item. page: Page number for pagination (1-based). page_size: Number of results per page (max 500). Returns: The full API response dictionary containing 'endpoints' list and 'total_count'. """ params: dict[str, Any] = { "marketplace": marketplace, "version": version, "page": page, "page_size": page_size, } url_suffix = ApiPaths.inventory_item_endpoints(item_id) demisto.debug(f"[API] Fetching endpoints for item {item_id} | Params: {params}") response = self._http_request( method="GET", url_suffix=url_suffix, params=params, ) demisto.debug(f"[API] Endpoints for item {item_id} response received") return response def search_inventory( self, page: int, page_size: int, filter_obj: dict[str, Any], sort_by: str | None = None, sort_direction: str | None = None, ) -> dict[str, Any]: """Search inventory items using advanced filters via POST. Args: page: Page number for pagination (1-based). page_size: Number of results per page (max 500). filter_obj: Filter object using query builder syntax. sort_by: Column to sort by. sort_direction: Sort direction (asc or desc). Returns: The full API response dictionary containing 'items' list and 'total_count'. """ body: dict[str, Any] = { "page": page, "page_size": page_size, "filter": filter_obj, } if sort_by: body["sort_by"] = sort_by if sort_direction: body["sort_direction"] = sort_direction demisto.debug(f"[API] Searching inventory | Body: {body}") response = self._http_request( method="POST", url_suffix=ApiPaths.INVENTORY_SEARCH, json_data=body, ) demisto.debug("[API] Inventory search response received") return response def send_events(self, events: list[dict]) -> None: """Send events to XSIAM using the ContentClient context. Wraps send_events_to_xsiam to keep event sending encapsulated within the client class for consistent logging and diagnostics. Args: events: List of event dicts to send. """ demisto.debug(f"[API] Sending {len(events)} events to XSIAM") send_events_to_xsiam(events=events, vendor=Config.VENDOR, product=Config.PRODUCT) demisto.debug(f"[API] Successfully sent {len(events)} events to XSIAM") # endregion # region Command implementations # ================================= # Command implementations # ================================= def test_module(client: Client) -> str: """Test API connectivity by fetching a small number of events. Args: client: The KOI client. Returns: 'ok' if test passed, otherwise raises an exception. """ demisto.debug("[Test Module] Starting...") try: utc_now = datetime.now(UTC) test_time = (utc_now - timedelta(minutes=Config.TEST_MODULE_LOOKBACK_MINUTES)).strftime(Config.DATE_FORMAT) demisto.debug(f"[Test Module] Fetching alerts from: {test_time}") fetch_events_with_pagination( client, log_type=LogType.ALERTS, created_after=test_time, max_events=Config.TEST_MODULE_MAX_EVENTS, ) demisto.debug("[Test Module] Success") return "ok" except Exception as error: error_msg = str(error) demisto.debug(f"[Test Module] Failed: {error_msg}") if "401" in error_msg or "403" in error_msg: return "Authorization Error: Verify your API Key." raise def fetch_events_with_pagination( client: Client, log_type: LogType, created_after: str, created_before: str | None = None, max_events: int = Config.DEFAULT_MAX_FETCH, audit_types: list[str] | None = None, ) -> list[dict]: """Fetch events with pagination support. This is the single unified pagination function used by all commands (test-module, fetch-events, get-events). Args: client: The KOI client. log_type: The LogType to fetch. created_after: Start time (ISO 8601). created_before: End time (ISO 8601) or None. max_events: Maximum number of events to fetch. audit_types: Optional list of audit log types to filter by. Returns: List of event dictionaries. """ events: list[dict] = [] page = 1 page_size = min(Config.MAX_PAGE_SIZE, max_events) demisto.debug( f"[Pagination Loop] Start | Type: {log_type.type_string} | Goal: {max_events} | " f"Time: {created_after} -> {created_before or 'Now'}" ) while len(events) < max_events: page_events = client.get_events_page( log_type=log_type, created_at_gte=created_after, created_at_lte=created_before, page=page, page_size=page_size, audit_types=audit_types if log_type == LogType.AUDIT else None, ) if not page_events: demisto.debug(f"[Pagination Loop] Page {page}: Empty. Stopping.") break events.extend(page_events) demisto.debug(f"[Pagination Loop] Page {page}: +{len(page_events)} events. Total: {len(events)}") if len(page_events) < page_size: demisto.debug("[Pagination Loop] Last page (partial). Stopping.") break page += 1 if page > Config.MAX_PAGES_PER_FETCH: demisto.debug(f"[Pagination Loop] Max page limit reached ({Config.MAX_PAGES_PER_FETCH}). Stopping.") break if len(events) >= max_events: demisto.debug(f"[Pagination Loop] Threshold reached ({len(events)} >= {max_events}). Stopping.") break # Slice to limit if len(events) > max_events: demisto.debug(f"[Pagination Result] Slicing {len(events)} events to limit {max_events}") events = events[:max_events] demisto.debug(f"[Pagination Result] Returning {len(events)} {log_type.type_string} events") return events def get_events_command(client: Client, args: dict, params: dict) -> CommandResults | str: """Manual command to get events for debugging/development. Args: client: The KOI client. args: Command arguments. params: Integration parameters. Returns: CommandResults or string message. """ demisto.debug("[Command] koi-get-events triggered") limit = int(args.get("limit", "50")) start_time_input = args.get("start_time", Config.DEFAULT_FROM_TIME) end_time_input = args.get("end_time") should_push_events = resolve_should_push_events(args) event_type_arg = argToList(args.get("event_type")) event_types_to_fetch = argToList(params.get("event_types_to_fetch", ["Alerts", "Audit"])) log_types = get_log_types_from_titles(event_type_arg if event_type_arg else event_types_to_fetch) created_after = get_formatted_utc_time(start_time_input) created_before = get_formatted_utc_time(end_time_input) if end_time_input else None audit_types_filter = argToList(params.get("audit_types_filter")) or None demisto.debug(f"[Command Params] From: {created_after}, To: {created_before}, Limit: {limit}, Push: {should_push_events}") all_events: list[dict] = [] for log_type in log_types: events = fetch_events_with_pagination( client, log_type=log_type, created_after=created_after, created_before=created_before, max_events=limit, audit_types=audit_types_filter if log_type == LogType.AUDIT else None, ) add_time_to_events(events, log_type) all_events.extend(events) demisto.debug(f"[Command Result] Total events retrieved: {len(all_events)}") if should_push_events and all_events: client.send_events(all_events) return f"Successfully retrieved and pushed {len(all_events)} events to XSIAM" readable_output = tableToMarkdown(f"{INTEGRATION_NAME} Events", all_events, removeNull=True) return CommandResults( readable_output=readable_output, outputs_prefix="KOI.Event", outputs_key_field="id", outputs=all_events, ) @dataclass class FetchResult: """Result of fetching events for a single log type.""" log_type: LogType new_events: list[dict] = field(default_factory=list) last_run_updates: dict[str, str | list[str]] = field(default_factory=dict) error: str | None = None def _fetch_single_log_type( client: Client, log_type: LogType, last_run: dict[str, str | list[str]], max_events: int, audit_types: list[str] | None, ) -> FetchResult: """Fetch and process events for a single log type. This function is executed in a separate thread by fetch_events_command via ThreadPoolExecutor, enabling parallel fetching of multiple log types. Each thread receives an immutable copy of last_run to avoid shared mutable state. The function handles its own errors — if an API call fails, the error is captured in FetchResult.error and the thread returns gracefully without affecting other threads. Thread safety: - Receives a dict copy of last_run (no shared mutable state). - Returns a FetchResult with last_run_updates (merged by the main thread after completion). - Uses demisto.debug() for logging (thread-safe in XSOAR runtime). Args: client: The KOI client (thread-safe — ContentClient uses httpx which is thread-safe). log_type: The LogType to fetch (ALERTS or AUDIT). last_run: Immutable copy of the current last_run state dict. max_events: Maximum events to fetch per type. audit_types: Optional audit type filter (only applied for AUDIT log type). Returns: FetchResult containing new_events, last_run_updates, and any error message. """ result = FetchResult(log_type=log_type) try: last_fetch_key = f"last_fetch_{log_type.type_string}" previous_ids_key = f"previous_ids_{log_type.type_string}" raw_timestamp = last_run.get(last_fetch_key) last_fetch_timestamp: str | None = raw_timestamp if isinstance(raw_timestamp, str) else None raw_ids = last_run.get(previous_ids_key) last_fetched_ids: list[str] = raw_ids if isinstance(raw_ids, list) else [] if last_fetch_timestamp: time_input = last_fetch_timestamp demisto.debug( f"[Fetch] {log_type.type_string}: Continuing from {time_input}. " f"Prev ID count: {len(last_fetched_ids)}" ) else: time_input = Config.DEFAULT_FROM_TIME demisto.debug(f"[Fetch] {log_type.type_string}: First run - starting from default time") created_after = get_formatted_utc_time(time_input) # Fetch events using the unified pagination function events = fetch_events_with_pagination( client, log_type=log_type, created_after=created_after, max_events=max_events, audit_types=audit_types if log_type == LogType.AUDIT else None, ) if not events: demisto.debug(f"[Fetch] {log_type.type_string}: No events found.") return result # Pre-compute time values to avoid redundant extract_time_from_event calls. # Events are already sorted chronologically by the API (sort_direction=asc). event_times: list[str] = [extract_time_from_event(event, log_type) or "" for event in events] # Deduplicate new_events = deduplicate_events(events, last_fetched_ids) if new_events: add_time_to_events(new_events, log_type) result.new_events = new_events demisto.debug(f"[Fetch] {log_type.type_string}: {len(new_events)} new events after dedup") else: demisto.debug(f"[Fetch] {log_type.type_string}: All events were duplicates.") # Update Last Run - always update based on ALL fetched events (not just new_events) new_last_run_time = event_times[-1] if event_times else None if new_last_run_time: # Collect IDs for the new high-water mark timestamp using pre-computed times ids_at_last_timestamp: list[str] = [ event_id for event, event_time in zip(events, event_times) if event_time == new_last_run_time and (event_id := get_event_id(event)) ] # If the HWM timestamp hasn't changed, merge with previous IDs to prevent duplicates if new_last_run_time == last_fetch_timestamp: ids_at_last_timestamp = list(set(last_fetched_ids) | set(ids_at_last_timestamp)) result.last_run_updates[last_fetch_key] = new_last_run_time result.last_run_updates[previous_ids_key] = ids_at_last_timestamp demisto.debug(f"[Fetch] {log_type.type_string}: State updated. New HWM: {new_last_run_time}") else: demisto.debug(f"[Fetch] {log_type.type_string}: Warning: Last event missing time. State not updated.") except Exception as e: result.error = str(e) demisto.debug(f"[Fetch] {log_type.type_string}: Error fetching events: {e!s}.") return result def fetch_events_command(client: Client) -> None: """Scheduled command to fetch events using parallel threads. Uses ThreadPoolExecutor to fetch all configured log types (Alerts, Audit) simultaneously. This ensures that if one type takes a long time or fails, the other type still completes within the XSOAR execution timeout. Architecture: 1. Single getLastRun() read at the start. 2. Each log type is fetched in a separate thread via _fetch_single_log_type(). Each thread receives an immutable copy of last_run (no shared mutable state). 3. After all threads complete, results are merged sequentially: - New events from successful types are collected. - last_run updates from successful types are applied. - Failed types are skipped (their previous state is preserved). 4. All events are sent to XSIAM in a single batch. 5. Single setLastRun() write at the end. Race condition prevention: - One getLastRun() call, one setLastRun() call. - Threads don't share mutable state — each gets a dict copy. - Merge happens after all threads complete (no concurrent writes). Args: client: The KOI client. """ params = demisto.params() max_events_to_fetch = int(params.get("max_fetch", Config.DEFAULT_MAX_FETCH)) event_types_to_fetch = argToList(params.get("event_types_to_fetch", ["Alerts", "Audit"])) log_types = get_log_types_from_titles(event_types_to_fetch) audit_types_filter = argToList(params.get("audit_types_filter")) or None # Single read of last_run state — no race condition last_run = demisto.getLastRun() demisto.debug(f"[Fetch] Starting with last_run: {last_run}") # Guard against an empty log_types selection — ThreadPoolExecutor(max_workers=0) raises ValueError. if not log_types: demisto.debug("[Fetch] No event types selected. Nothing to fetch. Preserving last_run as-is.") demisto.setLastRun(last_run) return # Fetch all log types in parallel so one slow type doesn't block the other results: list[FetchResult] = [] with ThreadPoolExecutor(max_workers=len(log_types)) as executor: futures = { executor.submit( _fetch_single_log_type, client=client, log_type=log_type, last_run=dict(last_run), max_events=max_events_to_fetch, audit_types=audit_types_filter, ): log_type for log_type in log_types } for future in as_completed(futures): log_type = futures[future] try: result = future.result() results.append(result) except Exception as e: demisto.debug(f"[Fetch] {log_type.type_string}: Thread failed: {e!s}") # Merge results — collect all new events and last_run updates all_new_events: list[dict] = [] updated_last_run: dict[str, str | list[str]] = dict(last_run) for result in results: if result.error: demisto.debug(f"[Fetch] {result.log_type.type_string}: Skipped due to error: {result.error}") continue all_new_events.extend(result.new_events) updated_last_run.update(result.last_run_updates) # Send all successfully fetched events to XSIAM if all_new_events: client.send_events(all_new_events) # Single write of last_run state — preserves progress from successful types demisto.setLastRun(updated_last_run) demisto.debug(f"[Fetch] Last run updated: {updated_last_run}") def koi_policy_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """List policies with pagination support. Supports two modes: - Single page: provide 'page' and/or 'page_size' to fetch a specific page. - Auto-paginate: provide 'limit' to automatically paginate and collect up to 'limit' policies. If 'page' is provided, single-page mode is used (limit is ignored). If only 'limit' is provided, auto-pagination mode is used. Args: client: The KOI client. args: Command arguments (page, page_size, limit). Returns: CommandResults with the policy list. """ demisto.debug("[Command] koi-policy-list triggered") page_arg = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) or Config.DEFAULT_PAGE_SIZE limit_arg = arg_to_number(args.get("limit")) if page_size > Config.MAX_PAGE_SIZE: raise DemistoException(f"page_size ({page_size}) exceeds the maximum allowed value of {Config.MAX_PAGE_SIZE}.") if limit_arg and limit_arg > Config.MAX_LIMIT: raise DemistoException(f"limit ({limit_arg}) exceeds the maximum allowed value of {Config.MAX_LIMIT}.") if page_arg: # Single-page mode: fetch the requested page demisto.debug(f"[Command] Single-page mode: page={page_arg}, page_size={page_size}") response = client.get_policies(page=page_arg, page_size=page_size) policies = response.get("policies", []) total_count = response.get("total_count") demisto.debug(f"[Command Result] Retrieved {len(policies)} policies (total_count={total_count})") else: # Auto-paginate mode: fetch pages until limit is reached limit = limit_arg or Config.DEFAULT_LIMIT demisto.debug(f"[Command] Auto-paginate mode: limit={limit}") policies = _fetch_policies_with_pagination(client, limit=limit) readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Policies", policies, headers=["id", "name", "description", "action", "enabled", "group_ids", "creator_fullname", "created_at", "updated_at"], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Policy", outputs_key_field="id", outputs=policies, ) def _fetch_policies_with_pagination( client: Client, limit: int, page_size: int = Config.MAX_PAGE_SIZE, ) -> list[dict]: """Auto-paginate through policies until limit is reached. Args: client: The Koi client. limit: Maximum total number of policies to collect. page_size: Number of results per API page. Returns: List of policy dictionaries. """ policies: list[dict] = [] page = Config.DEFAULT_PAGE while len(policies) < limit: response = client.get_policies(page=page, page_size=page_size) page_policies = response.get("policies", []) if not page_policies: demisto.debug(f"[Pagination] Page {page}: Empty. Stopping.") break policies.extend(page_policies) demisto.debug(f"[Pagination] Page {page}: +{len(page_policies)} policies. Total: {len(policies)}") if len(page_policies) < page_size: demisto.debug("[Pagination] Last page (partial). Stopping.") break page += 1 # Trim to limit if len(policies) > limit: demisto.debug(f"[Pagination] Trimming {len(policies)} policies to limit {limit}") policies = policies[:limit] demisto.debug(f"[Pagination] Returning {len(policies)} policies") return policies def koi_allowlist_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """Retrieve all items in the allowlist. Args: client: The KOI client. args: Command arguments (unused, no inputs for this command). Returns: CommandResults with the allowlist items. """ demisto.debug("[Command] koi-allowlist-get triggered") response = client.get_allowlist() items = response.get("items", []) demisto.debug(f"[Command Result] Retrieved {len(items)} allowlist items") readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Allowlist", items, headers=[ "item_id", "item_name", "item_display_name", "marketplace", "publisher_name", "package_name", "notes", "created_by", "created_at", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Allowlist", outputs_key_field="item_id", outputs=items, ) def koi_allowlist_items_remove_command(client: Client, args: dict[str, Any]) -> CommandResults: """Remove one or more items from the global allowlist. Supports two input modes: - Single item: provide 'item_id' and 'marketplace' (with optional 'created_by' and 'notes'). - Bulk from file: provide 'items_list_raw_json_entry_id' with a War Room entry ID of a JSON file containing a list of item objects. Args: client: The KOI client. args: Command arguments. Returns: CommandResults with a success message. """ demisto.debug("[Command] koi-allowlist-items-remove triggered") items = resolve_items_from_args(args) client.remove_allowlist_items(items) item_count = len(items) demisto.debug(f"[Command Result] {item_count} allowlist item(s) removed successfully") if item_count == 1: readable = f"Allowlist item '{items[0]['item_id']}' (marketplace: {items[0]['marketplace']}) was removed successfully." else: readable = f"{item_count} allowlist items were removed successfully." return CommandResults(readable_output=readable) def koi_allowlist_items_add_command(client: Client, args: dict[str, Any]) -> CommandResults: """Add one or more items to the global allowlist. Supports two input modes: - Single item: provide 'item_id' and 'marketplace' (with optional 'created_by' and 'notes'). - Bulk from file: provide 'items_list_raw_json_entry_id' with a War Room entry ID of a JSON file containing a list of item objects. Args: client: The KOI client. args: Command arguments. Returns: CommandResults with a success message. """ demisto.debug("[Command] koi-allowlist-items-add triggered") items = resolve_items_from_args(args) client.add_allowlist_items(items) item_count = len(items) demisto.debug(f"[Command Result] {item_count} allowlist item(s) added successfully") if item_count == 1: readable = f"Allowlist item '{items[0]['item_id']}' (marketplace: {items[0]['marketplace']}) was added successfully." else: readable = f"{item_count} allowlist items were added successfully." return CommandResults(readable_output=readable) def koi_blocklist_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """Retrieve all items in the blocklist. Args: client: The KOI client. args: Command arguments (unused, no inputs for this command). Returns: CommandResults with the blocklist items. """ demisto.debug("[Command] koi-blocklist-get triggered") response = client.get_blocklist() items = response.get("items", []) demisto.debug(f"[Command Result] Retrieved {len(items)} blocklist items") readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Blocklist", items, headers=[ "item_id", "item_name", "item_display_name", "marketplace", "publisher_name", "package_name", "notes", "created_by", "created_at", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Blocklist", outputs_key_field="item_id", outputs=items, ) def koi_blocklist_items_remove_command(client: Client, args: dict[str, Any]) -> CommandResults: """Remove one or more items from the global blocklist. Supports two input modes: - Single item: provide 'item_id' and 'marketplace' (with optional 'created_by' and 'notes'). - Bulk from file: provide 'items_list_raw_json_entry_id' with a War Room entry ID of a JSON file containing a list of item objects. Args: client: The KOI client. args: Command arguments. Returns: CommandResults with a success message. """ demisto.debug("[Command] koi-blocklist-items-remove triggered") items = resolve_items_from_args(args) client.remove_blocklist_items(items) item_count = len(items) demisto.debug(f"[Command Result] {item_count} blocklist item(s) removed successfully") if item_count == 1: readable = f"Blocklist item '{items[0]['item_id']}' (marketplace: {items[0]['marketplace']}) was removed successfully." else: readable = f"{item_count} blocklist items were removed successfully." return CommandResults(readable_output=readable) def koi_blocklist_items_add_command(client: Client, args: dict[str, Any]) -> CommandResults: """Add one or more items to the global blocklist. Supports two input modes: - Single item: provide 'item_id' and 'marketplace' (with optional 'created_by' and 'notes'). - Bulk from file: provide 'items_list_raw_json_entry_id' with a War Room entry ID of a JSON file containing a list of item objects. Args: client: The KOI client. args: Command arguments. Returns: CommandResults with a success message. """ demisto.debug("[Command] koi-blocklist-items-add triggered") items = resolve_items_from_args(args) client.add_blocklist_items(items) item_count = len(items) demisto.debug(f"[Command Result] {item_count} blocklist item(s) added successfully") if item_count == 1: readable = f"Blocklist item '{items[0]['item_id']}' (marketplace: {items[0]['marketplace']}) was added successfully." else: readable = f"{item_count} blocklist items were added successfully." return CommandResults(readable_output=readable) def koi_policy_status_update_command(client: Client, args: dict[str, Any]) -> CommandResults: """Enable or disable a policy by ID. Args: client: The KOI client. args: Command arguments (policy_id, enabled). Returns: CommandResults with the updated policy. """ demisto.debug("[Command] koi-policy-status-update triggered") policy_id = arg_to_number(args.get("policy_id")) if policy_id is None: raise DemistoException("policy_id is required and must be a valid integer.") enabled = argToBoolean(args.get("enabled")) response = client.update_policy_status(policy_id=policy_id, enabled=enabled) status_text = "enabled" if enabled else "disabled" demisto.debug(f"[Command Result] Policy {policy_id} {status_text} successfully") readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Policy Updated", response, headers=[ "id", "name", "description", "action", "enabled", "group_ids", "creator_fullname", "created_at", "updated_at", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Policy", outputs_key_field="id", outputs=response, ) def koi_inventory_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """List inventory items with pagination and filtering support. Supports two modes: - Single page: provide 'page' and/or 'page_size' to fetch a specific page. - Auto-paginate: provide 'limit' to automatically paginate and collect up to 'limit' items. If 'page' is provided, single-page mode is used (limit is ignored). If only 'limit' is provided, auto-pagination mode is used. Args: client: The KOI client. args: Command arguments including pagination and filter parameters. Returns: CommandResults with the inventory item list. """ demisto.debug("[Command] koi-inventory-list triggered") page_arg = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) or Config.DEFAULT_PAGE_SIZE limit_arg = arg_to_number(args.get("limit")) if page_size > Config.MAX_PAGE_SIZE: raise DemistoException(f"page_size ({page_size}) exceeds the maximum allowed value of {Config.MAX_PAGE_SIZE}.") if limit_arg and limit_arg > Config.MAX_LIMIT: raise DemistoException(f"limit ({limit_arg}) exceeds the maximum allowed value of {Config.MAX_LIMIT}.") # Extract filter arguments filter_kwargs: dict[str, Any] = assign_params( brew_category_koi=args.get("brew_category_koi"), browser_category_koi=args.get("browser_category_koi"), chocolatey_category_koi=args.get("chocolatey_category_koi"), device_id=args.get("device_id"), finding_id=args.get("finding_id"), first_seen=args.get("first_seen"), ide_category_koi=args.get("ide_category_koi"), installation_method=args.get("installation_method"), item_display_name=args.get("item_display_name"), item_id=args.get("item_id"), marketplace=args.get("marketplace"), platform=args.get("platform"), publisher_name=args.get("publisher_name"), risk_level=args.get("risk_level"), software_category_koi=args.get("software_category_koi"), sort_by=args.get("sort_by"), sort_direction=args.get("sort_direction"), view=args.get("view"), ) if page_arg: # Single-page mode: fetch the requested page demisto.debug(f"[Command] Single-page mode: page={page_arg}, page_size={page_size}") response = client.get_inventory(page=page_arg, page_size=page_size, **filter_kwargs) items = response.get("items", []) total_count = response.get("total_count") demisto.debug(f"[Command Result] Retrieved {len(items)} inventory items (total_count={total_count})") else: # Auto-paginate mode: fetch pages until limit is reached limit = limit_arg or Config.DEFAULT_LIMIT demisto.debug(f"[Command] Auto-paginate mode: limit={limit}") items = _fetch_inventory_with_pagination(client, limit=limit, filter_kwargs=filter_kwargs) readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Inventory", items, headers=[ "item_id", "item_display_name", "marketplace", "platforms", "publisher_name", "risk", "risk_level", "version", "status", "endpoint_count", "installs_count", "installation_method", "is_first_party", "is_signed", "first_seen", "last_seen", "last_used", "released_at", "short_description", "categories", "findings", "brew_category_koi", "browser_category_koi", "chocolatey_category_koi", "ide_category_koi", "software_category_koi", "governed_details", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Inventory", outputs_key_field="item_id", outputs=items, ) def _fetch_inventory_with_pagination( client: Client, limit: int, filter_kwargs: dict[str, Any], page_size: int = Config.MAX_PAGE_SIZE, ) -> list[dict]: """Auto-paginate through inventory items until limit is reached. Args: client: The Koi client. limit: Maximum total number of items to collect. filter_kwargs: Filter parameters to pass to the API. page_size: Number of results per API page. Returns: List of inventory item dictionaries. """ items: list[dict] = [] page = Config.DEFAULT_PAGE while len(items) < limit: response = client.get_inventory(page=page, page_size=page_size, **filter_kwargs) page_items = response.get("items", []) if not page_items: demisto.debug(f"[Pagination] Page {page}: Empty. Stopping.") break items.extend(page_items) demisto.debug(f"[Pagination] Page {page}: +{len(page_items)} items. Total: {len(items)}") if len(page_items) < page_size: demisto.debug("[Pagination] Last page (partial). Stopping.") break page += 1 # Trim to limit if len(items) > limit: demisto.debug(f"[Pagination] Trimming {len(items)} items to limit {limit}") items = items[:limit] demisto.debug(f"[Pagination] Returning {len(items)} inventory items") return items def koi_inventory_item_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """Retrieve comprehensive details for a specific inventory item. Args: client: The KOI client. args: Command arguments (item_id, marketplace, version). Returns: CommandResults with the inventory item details. """ demisto.debug("[Command] koi-inventory-item-get triggered") item_id: str = args["item_id"] marketplace: str = args["marketplace"] version: str = args["version"] response = client.get_inventory_item( item_id=item_id, marketplace=marketplace, version=version, ) demisto.debug(f"[Command Result] Retrieved inventory item {item_id}") readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Inventory Item", response, headers=[ "item_id", "item_display_name", "marketplace", "platforms", "publisher_name", "risk", "risk_level", "version", "status", "endpoint_count", "installs_count", "installation_method", "is_first_party", "is_signed", "first_seen", "last_seen", "last_used", "released_at", "short_description", "categories", "findings", "brew_category_koi", "browser_category_koi", "chocolatey_category_koi", "ide_category_koi", "software_category_koi", "governed_details", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Inventory", outputs_key_field="item_id", outputs=response, ) def koi_inventory_search_command(client: Client, args: dict[str, Any]) -> CommandResults: """Search inventory items using advanced filters. Supports two modes: - Single page: provide 'page' and/or 'page_size' to fetch a specific page. - Auto-paginate: provide 'limit' to automatically paginate and collect up to 'limit' items. If 'page' is provided, single-page mode is used (limit is ignored). If only 'limit' is provided, auto-pagination mode is used. Args: client: The KOI client. args: Command arguments including filter, pagination, and sorting parameters. Returns: CommandResults with the search results. """ demisto.debug("[Command] koi-inventory-search triggered") page_arg = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) or Config.DEFAULT_PAGE_SIZE limit_arg = arg_to_number(args.get("limit")) if page_size > Config.MAX_PAGE_SIZE: raise DemistoException(f"page_size ({page_size}) exceeds the maximum allowed value of {Config.MAX_PAGE_SIZE}.") if limit_arg and limit_arg > Config.MAX_LIMIT: raise DemistoException(f"limit ({limit_arg}) exceeds the maximum allowed value of {Config.MAX_LIMIT}.") filter_obj: dict[str, Any] = parse_filter_from_args(args) sort_by: str | None = args.get("sort_by") sort_direction: str | None = args.get("sort_direction") if page_arg: # Single-page mode demisto.debug(f"[Command] Single-page mode: page={page_arg}, page_size={page_size}") response = client.search_inventory( page=page_arg, page_size=page_size, filter_obj=filter_obj, sort_by=sort_by, sort_direction=sort_direction, ) items = response.get("items", []) total_count = response.get("total_count") demisto.debug(f"[Command Result] Retrieved {len(items)} items (total_count={total_count})") else: # Auto-paginate mode limit = limit_arg or Config.DEFAULT_LIMIT demisto.debug(f"[Command] Auto-paginate mode: limit={limit}") items = _search_inventory_with_pagination( client, limit=limit, filter_obj=filter_obj, sort_by=sort_by, sort_direction=sort_direction, ) readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Inventory Search", items, headers=[ "item_id", "item_display_name", "marketplace", "platforms", "publisher_name", "risk", "risk_level", "version", "status", "endpoint_count", "installs_count", "installation_method", "is_first_party", "is_signed", "first_seen", "last_seen", "last_used", "released_at", "short_description", "categories", "findings", "brew_category_koi", "browser_category_koi", "chocolatey_category_koi", "ide_category_koi", "software_category_koi", "governed_details", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Inventory", outputs_key_field="item_id", outputs=items, ) def _search_inventory_with_pagination( client: Client, limit: int, filter_obj: dict[str, Any], sort_by: str | None = None, sort_direction: str | None = None, page_size: int = Config.MAX_PAGE_SIZE, ) -> list[dict]: """Auto-paginate through inventory search results until limit is reached. Args: client: The Koi client. limit: Maximum total number of items to collect. filter_obj: Filter object for the search. sort_by: Column to sort by. sort_direction: Sort direction. page_size: Number of results per API page. Returns: List of inventory item dictionaries. """ items: list[dict] = [] page = Config.DEFAULT_PAGE while len(items) < limit: response = client.search_inventory( page=page, page_size=page_size, filter_obj=filter_obj, sort_by=sort_by, sort_direction=sort_direction, ) page_items = response.get("items", []) if not page_items: demisto.debug(f"[Pagination] Page {page}: Empty. Stopping.") break items.extend(page_items) demisto.debug(f"[Pagination] Page {page}: +{len(page_items)} items. Total: {len(items)}") if len(page_items) < page_size: demisto.debug("[Pagination] Last page (partial). Stopping.") break page += 1 # Trim to limit if len(items) > limit: demisto.debug(f"[Pagination] Trimming {len(items)} items to limit {limit}") items = items[:limit] demisto.debug(f"[Pagination] Returning {len(items)} inventory search results") return items def koi_inventory_item_endpoints_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """List endpoints that have a specific inventory item installed. Supports two modes: - Single page: provide 'page' and/or 'page_size' to fetch a specific page. - Auto-paginate: provide 'limit' to automatically paginate and collect up to 'limit' endpoints. If 'page' is provided, single-page mode is used (limit is ignored). If only 'limit' is provided, auto-pagination mode is used. Args: client: The KOI client. args: Command arguments (item_id, marketplace, version, page, page_size, limit). Returns: CommandResults with the endpoint list. """ demisto.debug("[Command] koi-inventory-item-endpoints-list triggered") item_id: str = args["item_id"] marketplace: str = args["marketplace"] version: str = args["version"] page_arg = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) or Config.DEFAULT_PAGE_SIZE limit_arg = arg_to_number(args.get("limit")) if page_size > Config.MAX_PAGE_SIZE: raise DemistoException(f"page_size ({page_size}) exceeds the maximum allowed value of {Config.MAX_PAGE_SIZE}.") if limit_arg and limit_arg > Config.MAX_LIMIT: raise DemistoException(f"limit ({limit_arg}) exceeds the maximum allowed value of {Config.MAX_LIMIT}.") if page_arg: # Single-page mode demisto.debug(f"[Command] Single-page mode: page={page_arg}, page_size={page_size}") response = client.get_inventory_item_endpoints( item_id=item_id, marketplace=marketplace, version=version, page=page_arg, page_size=page_size, ) endpoints = response.get("endpoints", []) total_count = response.get("total_count") demisto.debug(f"[Command Result] Retrieved {len(endpoints)} endpoints (total_count={total_count})") else: # Auto-paginate mode limit = limit_arg or Config.DEFAULT_LIMIT demisto.debug(f"[Command] Auto-paginate mode: limit={limit}") endpoints = _fetch_item_endpoints_with_pagination( client, item_id=item_id, marketplace=marketplace, version=version, limit=limit, ) readable_output = tableToMarkdown( f"{INTEGRATION_NAME} Inventory Item Endpoints", endpoints, headers=[ "id", "hostname", "os", "platform", "serial", "last_logged_on_user", "activation_status", "path", "first_seen", "last_seen", ], headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix="Koi.Inventory.Endpoint", outputs_key_field="id", outputs=endpoints, ) def _fetch_item_endpoints_with_pagination( client: Client, item_id: str, marketplace: str, version: str, limit: int, page_size: int = Config.MAX_PAGE_SIZE, ) -> list[dict]: """Auto-paginate through item endpoints until limit is reached. Args: client: The Koi client. item_id: Unique identifier for the item. marketplace: The marketplace where the item is hosted. version: The specific version of the item. limit: Maximum total number of endpoints to collect. page_size: Number of results per API page. Returns: List of endpoint dictionaries. """ endpoints: list[dict] = [] page = Config.DEFAULT_PAGE while len(endpoints) < limit: response = client.get_inventory_item_endpoints( item_id=item_id, marketplace=marketplace, version=version, page=page, page_size=page_size, ) page_endpoints = response.get("endpoints", []) if not page_endpoints: demisto.debug(f"[Pagination] Page {page}: Empty. Stopping.") break endpoints.extend(page_endpoints) demisto.debug(f"[Pagination] Page {page}: +{len(page_endpoints)} endpoints. Total: {len(endpoints)}") if len(page_endpoints) < page_size: demisto.debug("[Pagination] Last page (partial). Stopping.") break page += 1 # Trim to limit if len(endpoints) > limit: demisto.debug(f"[Pagination] Trimming {len(endpoints)} endpoints to limit {limit}") endpoints = endpoints[:limit] demisto.debug(f"[Pagination] Returning {len(endpoints)} endpoints") return endpoints # endregion # region Main router # ================================= # Main router # ================================= COMMAND_MAP: dict[str, Any] = { "test-module": test_module, "koi-get-events": get_events_command, "fetch-events": fetch_events_command, "koi-policy-list": koi_policy_list_command, "koi-allowlist-get": koi_allowlist_get_command, "koi-allowlist-items-remove": koi_allowlist_items_remove_command, "koi-allowlist-items-add": koi_allowlist_items_add_command, "koi-blocklist-get": koi_blocklist_get_command, "koi-blocklist-items-remove": koi_blocklist_items_remove_command, "koi-blocklist-items-add": koi_blocklist_items_add_command, "koi-policy-status-update": koi_policy_status_update_command, "koi-inventory-list": koi_inventory_list_command, "koi-inventory-item-get": koi_inventory_item_get_command, "koi-inventory-search": koi_inventory_search_command, "koi-inventory-item-endpoints-list": koi_inventory_item_endpoints_list_command, } def main() -> None: """Main entry point for KOI integration.""" demisto.debug(f"{INTEGRATION_NAME} integration started") command = demisto.command() try: if command not in COMMAND_MAP: raise DemistoException(f"Command '{command}' is not implemented") params = demisto.params() args = demisto.args() config = parse_integration_params(params) client = Client( base_url=config["base_url"], api_key=config["api_key"], verify=config["verify"], proxy=config["proxy"], ) command_func = COMMAND_MAP[command] if command == "test-module": result = command_func(client) return_results(result) elif command == "fetch-events": command_func(client) elif command == "koi-get-events": result = command_func(client, args, params) return_results(result) else: result = command_func(client, args) return_results(result) except Exception as error: error_msg = f"Failed to execute {command}. Error: {error!s}" demisto.error(f"{error_msg}\n{traceback.format_exc()}") return_error(error_msg) demisto.debug(f"{INTEGRATION_NAME} integration finished") if __name__ in ("__main__", "__builtin__", "builtins"): main()