DFIRe

Integration with DFIRe (Digital Forensics and Incident Response) platform for case management and IOC indicator tracking.

Forensics & Malware Analysis · DFIRe

Details

IDDFIRe
ProviderDFIRe
CategoryForensics & Malware Analysis
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658

README

DFIRe

DFIRe is a self-hosted Digital Forensics and Incident Response (DFIR) case management platform built for security professionals. It provides structured case management, evidence tracking with chain of custody, IOC indicator management, and incident response workflows aligned with the NIST Incident Response framework — all running on your own infrastructure with AES-256 encryption.

This integration connects Cortex XSIAM and Cortex XSOAR to a DFIRe instance, enabling automated case creation and updates, bi-directional IOC indicator synchronization, evidence item tracking, file attachment uploads, and timeline enrichment directly from playbooks.

Configure DFIRe in Cortex

Parameter Description Required
Server URL URL of your DFIRe instance (e.g. https://dfire.example.com). True
API Key Bearer API key (dfire_ak_...). Create under Settings > API Keys in DFIRe. True
Trust any certificate (not secure) Skip TLS verification for self-signed certs. False
Use system proxy settings Route requests through the configured proxy. False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

dfire-search


Searches across all DFIRe data (cases, indicators, notes, items). Supports AND, OR, NOT operators.

Base Command

dfire-search

Input

Argument Name Description Required
query The search query (min 2 characters). Required

Context Output

Path Type Description
DFIRe.Search.id String The result ID.
DFIRe.Search.type String The result type (e.g. case, indicator, note).
DFIRe.Search.title String The result title.
DFIRe.Search.snippet String The matching text snippet.
DFIRe.Search.rank Number The search relevance rank.
DFIRe.Search.url String The URL to the result in DFIRe.
DFIRe.Search.date Date The result date.

dfire-case-type-list


Lists available case types and their IDs.

Base Command

dfire-case-type-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.CaseType.id Number The case type ID.
DFIRe.CaseType.name String The case type name.

dfire-case-list


Lists cases from DFIRe.

Base Command

dfire-case-list

Input

Argument Name Description Required
limit The maximum number of cases to return. Default is 50. Optional
page The page number for pagination. Optional
status The status by which to filter cases. Possible values are: OPEN, CLOSED, ARCHIVED. Optional
status_in A comma-separated list of statuses by which to filter cases, for example, “OPEN,CLOSED”. Optional
severity The severity by which to filter cases. Possible values are: critical, high, medium, low, info. Optional
case_mode The mode by which to filter cases. Possible values are: investigation, incident. Optional
lead_investigator The lead investigator user ID by which to filter cases. Optional
created_at_gte The ISO-8601 datetime on or after which to filter cases, for example, “2026-05-01T00:00:00Z”. Optional
created_at_lte The ISO-8601 datetime on or before which to filter cases. Optional
ordering The field by which to order results, for example, “created_at”, or “-created_at” for descending order. Optional

Context Output

Path Type Description
DFIRe.Case.id Number The case ID.
DFIRe.Case.title String The case title.
DFIRe.Case.case_number String The case number.
DFIRe.Case.status String The case status.
DFIRe.Case.severity String The case severity.
DFIRe.Case.case_mode String The case mode, investigation or incident.
DFIRe.Case.case_type_name String The case type name.
DFIRe.Case.lead_investigator Number The lead investigator user ID.
DFIRe.Case.created_at Date Case creation timestamp.

dfire-case-get


Retrieves details of a specific case.

Base Command

dfire-case-get

Input

Argument Name Description Required
case_id The ID of the case. Required

Context Output

Path Type Description
DFIRe.Case.id Number The case ID.
DFIRe.Case.title String The case title.
DFIRe.Case.case_number String The case number.
DFIRe.Case.description String The case description.
DFIRe.Case.notes String The high-level case notes.
DFIRe.Case.status String The case status.
DFIRe.Case.severity String The case severity.
DFIRe.Case.case_mode String The case mode, investigation or incident.
DFIRe.Case.case_type Number The case type ID.
DFIRe.Case.case_type_name String The case type name.
DFIRe.Case.external_id String The external reference ID.
DFIRe.Case.lead_investigator Number The lead investigator user ID.
DFIRe.Case.project_id Number The associated project ID.
DFIRe.Case.current_phase_name String The current case phase name.
DFIRe.Case.item_count Number The number of evidence items.
DFIRe.Case.indicator_count Number The number of indicators.
DFIRe.Case.created_at Date The case creation timestamp.
DFIRe.Case.closed_at Date The case closure timestamp.

dfire-case-create


Creates a new case in DFIRe.

Base Command

dfire-case-create

Input

Argument Name Description Required
title The case title. Required
case_type The case type ID. Required
description The case description. Optional
notes The high-level case summary or notes. Optional
severity The case severity. Possible values are: critical, high, medium, low, info. Optional
case_mode The case mode, investigation or incident. Possible values are: investigation, incident. Optional
lead_investigator The user ID of the lead investigator. Optional
investigators A comma-separated list of investigator user IDs. Optional
viewers A comma-separated list of viewer user IDs. Optional
investigator_ids A comma-separated list of investigator IDs (alternate write field). Optional
viewer_ids A comma-separated list of viewer IDs (alternate write field). Optional
incident_category The ENISA incident category ID. Optional
outcome_verdict The outcome verdict ID (true positive, false positive, etc.). Optional
external_id The external reference ID (e.g. ticket number). Optional
project_id The project ID to associate the case with. Optional
attributes The JSON string of custom attributes to attach to the case. Optional
create_slack_channel Whether to auto-create a Slack channel for this case. Defaults to false to avoid unintended channel creation from automated workflows. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
DFIRe.Case.id Number The ID of the created case.
DFIRe.Case.title String The title of the created case.
DFIRe.Case.case_number String The assigned case number.
DFIRe.Case.status String The case status.

dfire-case-update


Updates an existing case in DFIRe.

Base Command

dfire-case-update

Input

Argument Name Description Required
case_id The ID of the case to update. Required
title The new title for the case. Optional
description The new case description. Optional
notes The high-level case summary or notes. Optional
status The new case status. Possible values are: OPEN, CLOSED, ARCHIVED. Optional
severity The new case severity. Possible values are: critical, high, medium, low, info. Optional
case_mode The new case mode, investigation or incident. Possible values are: investigation, incident. Optional
lead_investigator The new case lead investigator user ID. Optional
investigators A comma-separated list of user IDs with which to replace the investigator list.. Optional
viewers A comma-separated list of user IDs with which to replace the viewer list. Optional
investigator_ids A comma-separated list of investigator IDs (alternate write field). Optional
viewer_ids A comma-separated list of viewer IDs (alternate write field). Optional
incident_category The ENISA incident category ID. Optional
outcome_verdict The outcome verdict ID. Optional
external_id The new external reference ID. Optional
attributes The JSON string of custom attributes to attach to the case. Optional

Context Output

Path Type Description
DFIRe.Case.id Number The case ID.
DFIRe.Case.title String The updated case title.
DFIRe.Case.status String The updated case status.

dfire-case-delete


Deletes a case from DFIRe.

Base Command

dfire-case-delete

Input

Argument Name Description Required
case_id The ID of the case to delete. Required

Context Output

There is no context output for this command.

dfire-case-note-list


Lists notes for a case.

Base Command

dfire-case-note-list

Input

Argument Name Description Required
case_id The case ID to list notes for. Required

Context Output

Path Type Description
DFIRe.CaseNote.id Number The note ID.
DFIRe.CaseNote.case Number The case ID.
DFIRe.CaseNote.note String The note content.
DFIRe.CaseNote.author_name String The note author.
DFIRe.CaseNote.created_at Date The note creation timestamp.

dfire-case-note-create


Creates a note on a case.

Base Command

dfire-case-note-create

Input

Argument Name Description Required
case_id The case ID to add the note to. Required
note The note content. Required
show_on_timeline Whether to show this note on the case timeline. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
DFIRe.CaseNote.id Number The created note ID.
DFIRe.CaseNote.case Number The case ID.
DFIRe.CaseNote.note String The note content.

dfire-indicator-list


Lists indicators from the global IOC registry.

Base Command

dfire-indicator-list

Input

Argument Name Description Required
limit The maximum number of indicators to return. Default is 50. Optional
offset The offset for pagination. Default is 0. Optional
search The search term by which to filter indicators. Optional
stix_type The STIX type by which to filter indicators. Possible values are: ipv4-addr, ipv6-addr, domain-name, url, email-addr, email-message, file, process, windows-registry-key, network-traffic, user-account, mac-addr, software, artifact, autonomous-system, directory, mutex, x509-certificate. Optional
classification The classification by which to filter indicators. Possible values are: unknown, benign, suspicious, malicious. Optional
confidence The confidence level by which to filter indicators. Possible values are: low, medium, high. Optional
tlp The TLP designation by which to filter indicators. Possible values are: clear, green, amber, amber_strict, red. Optional
is_published Whether to filter indicators by published status. Possible values are: true, false. Optional
is_revoked Whether to filter indicators by revoked status. Possible values are: true, false. Optional
parent The ID of the parent indicator by which to filter results to return its child indicators. Optional
ordering The field by which to order results, for example, “created_at”, or “-confidence” for descending order. Optional

Context Output

Path Type Description
DFIRe.Indicator.id Number The indicator ID.
DFIRe.Indicator.value String The IOC value.
DFIRe.Indicator.stix_type String The STIX 2.1 SCO type.
DFIRe.Indicator.classification String The indicator classification (unknown/benign/suspicious/malicious).
DFIRe.Indicator.confidence String The indicator confidence level.
DFIRe.Indicator.tlp String The indicator TLP designation.
DFIRe.Indicator.is_published Boolean Whether the indicator is published.
DFIRe.Indicator.is_revoked Boolean Whether the indicator is revoked.
DFIRe.Indicator.case_count Number The number of associated cases for the indicator.
DFIRe.Indicator.first_seen Date The indicator first seen timestamp.
DFIRe.Indicator.created_at Date The indicator creation timestamp.

dfire-indicator-get


Retrieves details of a specific indicator.

Base Command

dfire-indicator-get

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.value String The IOC value.
DFIRe.Indicator.value_normalized String The normalized IOC value.
DFIRe.Indicator.stix_type String The STIX 2.1 SCO type.
DFIRe.Indicator.classification String The indicator classification.
DFIRe.Indicator.confidence String The indicator confidence level.
DFIRe.Indicator.tlp String The indicator TLP designation.
DFIRe.Indicator.tags Unknown The tags assigned to the indicator.
DFIRe.Indicator.public_notes String The indicator public notes.
DFIRe.Indicator.is_published Boolean Whether the indicator is published.
DFIRe.Indicator.is_revoked Boolean Whether the indicator is revoked.
DFIRe.Indicator.parent Number The parent indicator ID.
DFIRe.Indicator.case_count Number The number of associated cases for the indicator.
DFIRe.Indicator.children_count Number The number of child indicators.
DFIRe.Indicator.first_seen Date The indicator first seen timestamp.
DFIRe.Indicator.last_seen Date The indicator last seen timestamp.
DFIRe.Indicator.created_at Date The indicator creation timestamp.

dfire-indicator-create


Creates a new indicator in the global IOC registry.

Base Command

dfire-indicator-create

Input

Argument Name Description Required
value The IOC value (IP, domain, hash, URL, etc.). Required
stix_type The STIX 2.1 SCO type. Possible values are: ipv4-addr, ipv6-addr, domain-name, url, email-addr, email-message, file, process, windows-registry-key, network-traffic, user-account, mac-addr, software, artifact, autonomous-system, directory, mutex, x509-certificate. Required
classification The indicator classification. Possible values are: unknown, benign, suspicious, malicious. Optional
confidence The indicator confidence level. Possible values are: low, medium, high. Optional
tlp The incidator TLP designation. Possible values are: clear, green, amber, amber_strict, red. Optional
tags A comma-separated list of indicator tags. Optional
public_notes The public notes about the indicator. Optional
valid_until The indicator auto-revoke date (ISO 8601). Optional

Context Output

Path Type Description
DFIRe.Indicator.id Number The created indicator ID.
DFIRe.Indicator.value String The indicator value.
DFIRe.Indicator.stix_type String The indicator STIX type.
DFIRe.Indicator.is_existing Boolean Whether the indicator already exists.

dfire-indicator-update


Updates an existing indicator.

Base Command

dfire-indicator-update

Input

Argument Name Description Required
indicator_id The indicator ID to update. Required
classification The new indicator classification. Possible values are: unknown, benign, suspicious, malicious. Optional
confidence The new indicator confidence level. Possible values are: low, medium, high. Optional
tlp The new indicator TLP designation. Possible values are: clear, green, amber, amber_strict, red. Optional
tags A comma-separated list of new indicator tags (replaces the existing). Optional
public_notes The new indicator public notes. Optional
valid_until The new indicator auto-revoke date (ISO 8601). Set it to empty to clear. Optional

Context Output

Path Type Description
DFIRe.Indicator.id Number The indicator ID.
DFIRe.Indicator.value String The IOC value.
DFIRe.Indicator.classification String The updated indicator classification.

dfire-indicator-delete


Deletes an indicator from the global IOC registry.

Base Command

dfire-indicator-delete

Input

Argument Name Description Required
indicator_id The indicator ID to delete. Required

Context Output

There is no context output for this command.

dfire-item-type-list


Lists available evidence item types and their IDs.

Base Command

dfire-item-type-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.ItemType.id Number The item type ID.
DFIRe.ItemType.name String The item type name.
DFIRe.ItemType.icon String The item type icon.

dfire-item-flag-list


Lists available item flags and their IDs.

Base Command

dfire-item-flag-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.ItemFlag.id Number The flag ID.
DFIRe.ItemFlag.name String The flag name.
DFIRe.ItemFlag.color String The flag color.
DFIRe.ItemFlag.description String The flag description.

dfire-item-list


Lists evidence items, optionally filtered by case.

Base Command

dfire-item-list

Input

Argument Name Description Required
case_id The case ID by which to filter evidence items. Optional

Context Output

Path Type Description
DFIRe.Item.uuid String The evidence item UUID.
DFIRe.Item.name String The evidence item name.
DFIRe.Item.display_title String The evidence item display title.
DFIRe.Item.item_type_name String The evidence item type name.
DFIRe.Item.case Number The evidence item associated case ID.
DFIRe.Item.location String The evidence item location.
DFIRe.Item.attachment_count Number The number of attachments to the evidence item.
DFIRe.Item.created_at Date The evidence item creation timestamp.

dfire-item-get


Retrieves details of a specific evidence item.

Base Command

dfire-item-get

Input

Argument Name Description Required
item_id The evidence item ID. Required

Context Output

Path Type Description
DFIRe.Item.uuid String The evidence item UUID.
DFIRe.Item.name String The evidence item name.
DFIRe.Item.display_title String The evidence item display title.
DFIRe.Item.item_type_name String The evidence item type name.
DFIRe.Item.case Number The evidence item associated case ID.
DFIRe.Item.location String The evidence item location.
DFIRe.Item.attachment_count Number The number of attachments for the evidence item.
DFIRe.Item.created_at Date The evidence item creation timestamp.

dfire-item-create


Creates a new evidence item on a case.

Base Command

dfire-item-create

Input

Argument Name Description Required
case_id The case ID to add the evidence item to. Required
item_type The evidence item type ID. Required
location The evidence item location (e.g. storage location, lab). Required
name The friendly name/label for the evidence item. Optional
owner_id The legal entity ID of the evidence item owner. Optional
primary_user_id The legal entity ID of the primary user. Optional
collected_by The user ID of the collector. Optional
parent_item The UUID of the parent evidence item. Optional

Context Output

Path Type Description
DFIRe.Item.uuid String The created evidence item UUID.
DFIRe.Item.name String The evidence item name.
DFIRe.Item.case Number The case ID.

dfire-attachment-list


Lists attachments, optionally filtered by evidence item UUID.

Base Command

dfire-attachment-list

Input

Argument Name Description Required
item_uuid The evidence item UUID by which to filter attachments. Optional

Context Output

Path Type Description
DFIRe.Attachment.id Number The attachment ID.
DFIRe.Attachment.filename String The attachment filename.
DFIRe.Attachment.mime_type String The attachment MIME type.
DFIRe.Attachment.size Number The attachment file size in bytes.
DFIRe.Attachment.category String The attachment category.
DFIRe.Attachment.case Number The attachment associated case ID.
DFIRe.Attachment.item String The attachment associated evidence item UUID.
DFIRe.Attachment.hash_sha256 String The SHA-256 hash of the plaintext file attachment.
DFIRe.Attachment.uploaded_by_name String Who uploaded the file attachment.
DFIRe.Attachment.uploaded_at Date The attachment upload timestamp.

dfire-attachment-get


Gets details of a specific attachment.

Base Command

dfire-attachment-get

Input

Argument Name Description Required
attachment_id The attachment ID. Required

Context Output

Path Type Description
DFIRe.Attachment.id Number The DFIRe attachment ID.
DFIRe.Attachment.filename String The attachment filename.
DFIRe.Attachment.mime_type String The attachment MIME type.
DFIRe.Attachment.size Number The attachment file size in bytes.
DFIRe.Attachment.category String The attachment category.
DFIRe.Attachment.description String The attachment user-provided description.
DFIRe.Attachment.hash_sha256 String The SHA-256 hash of the plaintext file attachment.
DFIRe.Attachment.status String The attachment upload/encryption status.
DFIRe.Attachment.storage_location String The attachment storage location (local, s3, smb).
DFIRe.Attachment.uploaded_at Date The attachment upload timestamp.

dfire-attachment-upload


Uploads a file as an attachment to a case or evidence item.

Base Command

dfire-attachment-upload

Input

Argument Name Description Required
entry_id The War Room entry ID of the file to upload. Required
case_id The case ID to associate the attachment with. Optional
item_uuid The evidence item UUID to associate the attachment with. Optional
filename The name with which to override the filename. (default is the uploaded file name). Optional
category The attachment category. general routes to the encrypted file store and is relevant for most playbooks. evidence is reserved for evidence photos and routes to the image gallery, not the file store. Possible values are: general, evidence. Default is general. Optional

Context Output

Path Type Description
DFIRe.Attachment.id Number The created attachment ID.
DFIRe.Attachment.filename String The attachment filename.
DFIRe.Attachment.size Number The attachment file size in bytes.

dfire-attachment-delete


Deletes an attachment.

Base Command

dfire-attachment-delete

Input

Argument Name Description Required
attachment_id The attachment ID to delete. Required

Context Output

There is no context output for this command.

dfire-timeline-list


Lists timeline events for a case (newest first).

Base Command

dfire-timeline-list

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.TimelineEvent.id Number The timeline event ID.
DFIRe.TimelineEvent.event_type String The timeline event type.
DFIRe.TimelineEvent.subject String The timeline event subject.
DFIRe.TimelineEvent.details String The timeline event details.
DFIRe.TimelineEvent.event_datetime Date When the timeline event occurred.
DFIRe.TimelineEvent.created_by_name String Who created the timeline event.

dfire-timeline-create


Adds a manual timeline event to a case.

Base Command

dfire-timeline-create

Input

Argument Name Description Required
case_id The case ID. Required
subject The timeline event subject line. Required
details The timeline event description. Optional
event_datetime When the timeline event occurred (ISO 8601). Default is now. Optional

Context Output

Path Type Description
DFIRe.TimelineEvent.id Number The created timeline event ID.
DFIRe.TimelineEvent.subject String The timeline event subject.
DFIRe.TimelineEvent.event_datetime Date The timeline event timestamp.

dfire-user-list


Lists users in the DFIRe tenant. Useful for looking up user IDs for assignments.

Base Command

dfire-user-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.User.id Number The DFIRe user ID.
DFIRe.User.username String The DFIRe username.
DFIRe.User.full_name String The DFIRe user’s full name.
DFIRe.User.email String The DFIRe user’s email.
DFIRe.User.is_active Boolean Whether the DFIRe user is active.
DFIRe.User.groups Unknown Groups the DFIRe user belongs to.

dfire-case-indicator-list


Lists indicators associated with a case.

Base Command

dfire-case-indicator-list

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.CaseIndicator.id Number The association ID.
DFIRe.CaseIndicator.case Number The case ID.
DFIRe.CaseIndicator.indicator.id Number The indicator ID.
DFIRe.CaseIndicator.indicator.value String The indicator value.
DFIRe.CaseIndicator.indicator.stix_type String The indicator STIX type.
DFIRe.CaseIndicator.context String The case private notes about the associated indicator.
DFIRe.CaseIndicator.source String How the associated indicator was added.
DFIRe.CaseIndicator.created_at Date The association timestamp.
DFIRe.CaseIndicator.case_count Number The number of cases this indicator appears in.

dfire-case-indicator-add


Adds an indicator to a case. Creates the indicator if it does not exist.

Base Command

dfire-case-indicator-add

Input

Argument Name Description Required
case_id The case ID. Required
value The indicator value. Required
stix_type The indicator STIX 2.1 SCO type. Possible values are: ipv4-addr, ipv6-addr, domain-name, url, email-addr, email-message, file, process, windows-registry-key, network-traffic, user-account, mac-addr, software, artifact, autonomous-system, directory, mutex, x509-certificate. Required
classification The indicator classification. Possible values are: unknown, benign, suspicious, malicious. Default is unknown. Optional
confidence The indicator confidence level. Possible values are: low, medium, high. Default is low. Optional
tlp The indicator TLP designation. Possible values are: clear, green, amber, amber_strict, red. Default is amber. Optional
context The case private notes about the indicator. Optional
tags A comma-separated list of indicator tags. Optional
source The source from which the indicator was obtained. Possible values are: manual, automated, threat_intel, sandbox, enrichment, import. Optional
source_reference The free-form reference identifying the source (URL, ticket, report name, etc.). Optional
valid_until The ISO-8601 datetime after which to automatically invalidate the indicator, for example, “2026-12-31T00:00:00Z”. Optional
decompose Whether to auto-decompose the indicator (URL→domain, email→domain). Possible values are: true, false. Default is true. Optional
publish Whether to publish the indicator immediately after creation. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
DFIRe.CaseIndicator.id Number The association ID.
DFIRe.CaseIndicator.indicator.id Number The indicator ID.
DFIRe.CaseIndicator.indicator.value String The indicator value.

dfire-case-indicator-remove


Removes an indicator association from a case.

Base Command

dfire-case-indicator-remove

Input

Argument Name Description Required
case_id The case ID. Required
association_id The case indicator association ID. Required

Context Output

There is no context output for this command.

dfire-ioc-extract


Extracts candidate IOCs from a block of text. Returns suggestions only — does not add them to any case.

Base Command

dfire-ioc-extract

Input

Argument Name Description Required
text The text to scan to extract indicators from. Required

Context Output

Path Type Description
DFIRe.IOCExtraction.candidates Unknown The list of extracted indicator candidates.

dfire-indicator-check


Batch-checks whether IOCs already exist in the global registry.

Base Command

dfire-indicator-check

Input

Argument Name Description Required
indicators The JSON array of {value, stix_type} objects (mutually exclusive with values+stix_type). Optional
values A comma-separated indicator values to check (used with stix_type). Optional
stix_type The STIX type to use when checking the values argument. Possible values are: ipv4-addr, ipv6-addr, domain-name, url, email-addr, email-message, file, process, windows-registry-key, network-traffic, user-account, mac-addr, software, artifact, autonomous-system, directory, mutex, x509-certificate. Optional

Context Output

Path Type Description
DFIRe.IndicatorCheck.results Unknown The status and details of each submitted indicator.

dfire-indicator-enrich


Triggers external enrichment for an indicator.

Base Command

dfire-indicator-enrich

Input

Argument Name Description Required
indicator_id The indicator ID. Required
providers A comma-separated list of provider names (omit this to run all providers). Optional
force Whether to re-enrich even if cached results exist. Possible values are: true, false. Optional

Context Output

Path Type Description
DFIRe.Indicator.id Number The indicator ID.

dfire-indicator-enrichment-list


Retrieves cached enrichment results for an indicator.

Base Command

dfire-indicator-enrichment-list

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Enrichment.enrichments Unknown The enrichment records.

dfire-indicator-publish


Publishes an indicator (making it visible to TAXII consumers and STIX exports).

Base Command

dfire-indicator-publish

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.is_published Boolean Whether the indicator is published.

dfire-indicator-unpublish


Unpublishes an indicator.

Base Command

dfire-indicator-unpublish

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.is_published Boolean Whether the indicator is published.

dfire-indicator-revoke


Revokes an indicator.

Base Command

dfire-indicator-revoke

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.is_revoked Boolean Whether the indicator is revoked.

dfire-indicator-unrevoke


Unrevokes an indicator.

Base Command

dfire-indicator-unrevoke

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.is_revoked Boolean Whether the indicator is revoked.

dfire-indicator-decompose


Auto-decomposes an indicator (URL→domain, email→domain, etc.).

Base Command

dfire-indicator-decompose

Input

Argument Name Description Required
indicator_id The indicator ID. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.

dfire-indicator-add-tags


Merges a list of tags into the indicator’s existing tag set.

Base Command

dfire-indicator-add-tags

Input

Argument Name Description Required
indicator_id The indicator ID. Required
tags A comma-separated list of tags to add. Required

Context Output

Path Type Description
DFIRe.Indicator.id Number The DFIRe indicator ID.
DFIRe.Indicator.tags Unknown The updated tag list.

dfire-indicator-correlated-list


Lists indicators that appear in multiple cases.

Base Command

dfire-indicator-correlated-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.IndicatorCorrelated.results Unknown The indicators correlated across cases.

dfire-indicator-bulk-classify


Bulk-updates classification for multiple indicators.

Base Command

dfire-indicator-bulk-classify

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required
classification The new indicator classification. Possible values are: unknown, benign, suspicious, malicious. Required

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-indicator-bulk-confidence


The bulk-update confidence for multiple indicators.

Base Command

dfire-indicator-bulk-confidence

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required
confidence The new indicator confidence level. Possible values are: low, medium, high. Required

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-indicator-bulk-tag


Bulk adds/removes/sets tags on multiple indicators.

Base Command

dfire-indicator-bulk-tag

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required
tags A comma-separated list of indicator tags. Required
mode The action to perform on the tags (add, remove, set). Possible values are: add, remove, set. Default is add. Optional

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-indicator-bulk-tlp


Bulk-updates TLP designation for multiple indicators.

Base Command

dfire-indicator-bulk-tlp

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required
tlp The new indicator TLP designation. Possible values are: clear, green, amber, amber_strict, red. Required

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-indicator-bulk-publish


Bulk-publishes indicators.

Base Command

dfire-indicator-bulk-publish

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required

Context Output

Path Type Description
DFIRe.BulkPublishResponse.published_count Number The number of indicators published.
DFIRe.BulkPublishResponse.skipped_revoked Number The number of indicators skipped because they were revoked.
DFIRe.BulkPublishResponse.skipped_red Number The number of indicators skipped because of TLP status RED.

dfire-indicator-bulk-revoke


Bulk-revokes indicators.

Base Command

dfire-indicator-bulk-revoke

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-indicator-bulk-delete


Bulk-deletes indicators.

Base Command

dfire-indicator-bulk-delete

Input

Argument Name Description Required
indicator_ids A comma-separated list of indicator IDs. Required

Context Output

Path Type Description
DFIRe.BulkResult Unknown The bulk operation result.

dfire-case-generate-summary


Triggers an AI-generated executive summary for a case.

Base Command

dfire-case-generate-summary

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.CaseSummary Unknown The summary result.

dfire-case-chat


Sends a chat message to the case AI assistant.

Base Command

dfire-case-chat

Input

Argument Name Description Required
case_id The case ID. Required
message The user message to send. Required

Context Output

Path Type Description
DFIRe.CaseChat Unknown The chat response.

dfire-case-update-report


Updates the text of an AI-generated report attached to a case (e.g. an executive summary).

Base Command

dfire-case-update-report

Input

Argument Name Description Required
case_id The case ID the report belongs to. Required
report_id The ID of the generated report to update. Required
report_text The new report text content. Required

Context Output

Path Type Description
DFIRe.CaseReport Unknown The updated report.

dfire-case-can-report-list


Lists CAN (Case Activity Notice) reports for a case.

Base Command

dfire-case-can-report-list

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.CANReport.id Number The CAN report ID.

dfire-case-can-report-generate


Generates a new CAN report for a case.

Base Command

dfire-case-can-report-generate

Input

Argument Name Description Required
case_id The case ID. Required
body The optional JSON body describing the report parameters. Optional

Context Output

Path Type Description
DFIRe.CANReport.id Number The generated report ID.

dfire-case-investigation-report-get


Retrieves the investigation report for a case.

Base Command

dfire-case-investigation-report-get

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.InvestigationReport Unknown The investigation report.

dfire-case-investigation-report-generate


Generates AI content for a single section of a case’s investigation report. Returns preview content; does not auto-save.

Base Command

dfire-case-investigation-report-generate

Input

Argument Name Description Required
case_id The case ID. Required
section_id The ID of the report section to generate content for. Required

Context Output

Path Type Description
DFIRe.InvestigationReport.content String The generated section content.
DFIRe.InvestigationReport.model String The model used to generate the content.

dfire-case-investigation-report-finalize


Finalizes the investigation report for a case.

Base Command

dfire-case-investigation-report-finalize

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.InvestigationReport Unknown The finalized investigation report.

dfire-case-investigation-report-ready-for-qa


Marks a single section of the investigation report as ready for QA review.

Base Command

dfire-case-investigation-report-ready-for-qa

Input

Argument Name Description Required
case_id The case ID. Required
section_id The ID of the report section to mark ready for QA. Required

Context Output

Path Type Description
DFIRe.InvestigationReport Unknown The investigation report section after the state change.

dfire-case-timeline-change-phase


Moves a case to a new response phase in the timeline.

Base Command

dfire-case-timeline-change-phase

Input

Argument Name Description Required
case_id The case ID. Required
phase_id The target phase ID. Optional
phase_name The target phase name (used if phase_id is omitted). Optional
note The optional note explaining the phase change. Optional

Context Output

Path Type Description
DFIRe.TimelineEvent.id Number The created phase change timeline event ID.

dfire-case-todo-list


Lists todos for a case.

Base Command

dfire-case-todo-list

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.
DFIRe.CaseTodo.title String The todo title.
DFIRe.CaseTodo.status String The todo status.

dfire-case-todo-get


Retrieves a single todo with full details.

Base Command

dfire-case-todo-get

Input

Argument Name Description Required
case_id The case ID. Required
todo_id The todo ID. Required

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.

dfire-case-todo-assign


Assigns a todo to a user.

Base Command

dfire-case-todo-assign

Input

Argument Name Description Required
case_id The case ID. Required
todo_id The todo ID. Required
user_id The user ID of the assignee. Optional

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.
DFIRe.CaseTodo.assignee_name String The assignee display name.

dfire-case-todo-note-set


Sets or replaces the note on a todo.

Base Command

dfire-case-todo-note-set

Input

Argument Name Description Required
case_id The case ID. Required
todo_id The todo ID. Required
note The new note content. Required

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.

dfire-case-todo-attach-runbook


Attaches a runbook to a todo.

Base Command

dfire-case-todo-attach-runbook

Input

Argument Name Description Required
case_id The case ID. Required
todo_id The todo ID. Required
runbook_slug The runbook slug to attach. Required

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.
DFIRe.CaseTodo.runbook_slug String The attached runbook slug.

dfire-case-todo-detach-runbook


Detaches the runbook from a todo.

Base Command

dfire-case-todo-detach-runbook

Input

Argument Name Description Required
case_id The case ID. Required
todo_id The todo ID. Required

Context Output

Path Type Description
DFIRe.CaseTodo.id Number The todo ID.

dfire-case-timer-list


Lists the SLA timers for a case.

Base Command

dfire-case-timer-list

Input

Argument Name Description Required
case_id The case ID. Required

Context Output

Path Type Description
DFIRe.CaseTimer.id Number The timer ID.
DFIRe.CaseTimer.name String The timer name.
DFIRe.CaseTimer.framework String The compliance framework.

dfire-case-timer-get


Retrieves a single SLA timer.

Base Command

dfire-case-timer-get

Input

Argument Name Description Required
case_id The case ID. Required
timer_id The timer ID. Required

Context Output

Path Type Description
DFIRe.CaseTimer.id Number The timer ID.

dfire-case-timer-complete


Marks a case SLA timer as complete.

Base Command

dfire-case-timer-complete

Input

Argument Name Description Required
case_id The case ID. Required
timer_id The timer ID. Required

Context Output

Path Type Description
DFIRe.CaseTimer.id Number The timer ID.

dfire-case-timer-reset


Resets a case SLA timer.

Base Command

dfire-case-timer-reset

Input

Argument Name Description Required
case_id The case ID. Required
timer_id The timer ID. Required

Context Output

Path Type Description
DFIRe.CaseTimer.id Number The timer ID.

dfire-case-get-by-number


Looks up a case by its human-readable case number.

Base Command

dfire-case-get-by-number

Input

Argument Name Description Required
case_number The case number (e.g. “CASE-2026-0001”). Required

Context Output

Path Type Description
DFIRe.Case.id Number The case ID.
DFIRe.Case.case_number String The case number.

dfire-item-resolve-short-id


Resolves an 8-character item short ID to its full UUID and parent case ID.

Base Command

dfire-item-resolve-short-id

Input

Argument Name Description Required
short_id The first 8 characters of the item UUID. Required

Context Output

Path Type Description
DFIRe.Item.uuid String The full item UUID.
DFIRe.Item.case Number The parent case ID.

dfire-incident-category-list


Lists ENISA incident categories (useful for picklists).

Base Command

dfire-incident-category-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.IncidentCategory.id Number The category ID.
DFIRe.IncidentCategory.name String The category name.

dfire-incident-phase-list


Lists configured incident-response phases.

Base Command

dfire-incident-phase-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.IncidentPhase.id Number The phase ID.
DFIRe.IncidentPhase.name String The phase name.

dfire-outcome-verdict-list


Lists case outcome verdicts (true positive, false positive, etc.).

Base Command

dfire-outcome-verdict-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.OutcomeVerdict.id Number The verdict ID.
DFIRe.OutcomeVerdict.name String The verdict name.

dfire-project-list


Lists projects.

Base Command

dfire-project-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.Project.id Number The project ID.
DFIRe.Project.name String The project name.

dfire-runbook-list


Lists available runbooks (used for todo runbook attachments).

Base Command

dfire-runbook-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.Runbook.slug String The runbook slug.
DFIRe.Runbook.name String The runbook name.

dfire-group-list


Lists user groups.

Base Command

dfire-group-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
DFIRe.Group.id Number The user group ID.
DFIRe.Group.name String The user group name.

Configuration parameters

  • url — Server URL (required)
  • apikey — API Key (required)
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (75)

  • dfire-attachment-delete

    Deletes an attachment.

  • dfire-attachment-get

    Gets details of a specific attachment.

  • dfire-attachment-list

    Lists attachments, optionally filtered by evidence item UUID.

  • dfire-attachment-upload

    Uploads a file as an attachment to a case or evidence item.

  • dfire-case-can-report-generate

    Generates a new CAN report for a case.

  • dfire-case-can-report-list

    Lists CAN (Case Activity Notice) reports for a case.

  • dfire-case-chat

    Sends a chat message to the case AI assistant.

  • dfire-case-create

    Creates a new case in DFIRe.

  • dfire-case-delete

    Deletes a case from DFIRe.

  • dfire-case-generate-summary

    Triggers an AI-generated executive summary for a case.

  • dfire-case-get

    Retrieves details of a specific case.

  • dfire-case-get-by-number

    Looks up a case by its human-readable case number.

  • dfire-case-indicator-add

    Adds an indicator to a case. Creates the indicator if it does not exist.

  • dfire-case-indicator-list

    Lists indicators associated with a case.

  • dfire-case-indicator-remove

    Removes an indicator association from a case.

  • dfire-case-investigation-report-finalize

    Finalizes the investigation report for a case.

  • dfire-case-investigation-report-generate

    Generates AI content for a single section of a case's investigation report. Returns preview content; does not auto-save.

  • dfire-case-investigation-report-get

    Retrieves the investigation report for a case.

  • dfire-case-investigation-report-ready-for-qa

    Marks a single section of the investigation report as ready for QA review.

  • dfire-case-list

    Lists cases from DFIRe.

  • dfire-case-note-create

    Creates a note on a case.

  • dfire-case-note-list

    Lists notes for a case.

  • dfire-case-timeline-change-phase

    Moves a case to a new response phase in the timeline.

  • dfire-case-timer-complete

    Marks a case SLA timer as complete.

  • dfire-case-timer-get

    Retrieves a single SLA timer.

  • dfire-case-timer-list

    Lists the SLA timers for a case.

  • dfire-case-timer-reset

    Resets a case SLA timer.

  • dfire-case-todo-assign

    Assigns a todo to a user.

  • dfire-case-todo-attach-runbook

    Attaches a runbook to a todo.

  • dfire-case-todo-detach-runbook

    Detaches the runbook from a todo.

  • dfire-case-todo-get

    Retrieves a single todo with full details.

  • dfire-case-todo-list

    Lists todos for a case.

  • dfire-case-todo-note-set

    Sets or replaces the note on a todo.

  • dfire-case-type-list

    Lists available case types and their IDs.

  • dfire-case-update

    Updates an existing case in DFIRe.

  • dfire-case-update-report

    Updates the text of an AI-generated report attached to a case (e.g. an executive summary).

  • dfire-group-list

    Lists user groups.

  • dfire-incident-category-list

    Lists ENISA incident categories (useful for picklists).

  • dfire-incident-phase-list

    Lists configured incident-response phases.

  • dfire-indicator-add-tags

    Merges a list of tags into the indicator's existing tag set.

  • dfire-indicator-bulk-classify

    Bulk-updates classification for multiple indicators.

  • dfire-indicator-bulk-confidence

    The bulk-update confidence for multiple indicators.

  • dfire-indicator-bulk-delete

    Bulk-deletes indicators.

  • dfire-indicator-bulk-publish

    Bulk-publishes indicators.

  • dfire-indicator-bulk-revoke

    Bulk-revokes indicators.

  • dfire-indicator-bulk-tag

    Bulk adds/removes/sets tags on multiple indicators.

  • dfire-indicator-bulk-tlp

    Bulk-updates TLP designation for multiple indicators.

  • dfire-indicator-check

    Batch-checks whether IOCs already exist in the global registry.

  • dfire-indicator-correlated-list

    Lists indicators that appear in multiple cases.

  • dfire-indicator-create

    Creates a new indicator in the global IOC registry.

  • dfire-indicator-decompose

    Auto-decomposes an indicator (URL→domain, email→domain, etc.).

  • dfire-indicator-delete

    Deletes an indicator from the global IOC registry.

  • dfire-indicator-enrich

    Triggers external enrichment for an indicator.

  • dfire-indicator-enrichment-list

    Retrieves cached enrichment results for an indicator.

  • dfire-indicator-get

    Retrieves details of a specific indicator.

  • dfire-indicator-list

    Lists indicators from the global IOC registry.

  • dfire-indicator-publish

    Publishes an indicator (making it visible to TAXII consumers and STIX exports).

  • dfire-indicator-revoke

    Revokes an indicator.

  • dfire-indicator-unpublish

    Unpublishes an indicator.

  • dfire-indicator-unrevoke

    Unrevokes an indicator.

  • dfire-indicator-update

    Updates an existing indicator.

  • dfire-ioc-extract

    Extracts candidate IOCs from a block of text. Returns suggestions only — does not add them to any case.

  • dfire-item-create

    Creates a new evidence item on a case.

  • dfire-item-flag-list

    Lists available item flags and their IDs.

  • dfire-item-get

    Retrieves details of a specific evidence item.

  • dfire-item-list

    Lists evidence items, optionally filtered by case.

  • dfire-item-resolve-short-id

    Resolves an 8-character item short ID to its full UUID and parent case ID.

  • dfire-item-type-list

    Lists available evidence item types and their IDs.

  • dfire-outcome-verdict-list

    Lists case outcome verdicts (true positive, false positive, etc.).

  • dfire-project-list

    Lists projects.

  • dfire-runbook-list

    Lists available runbooks (used for todo runbook attachments).

  • dfire-search

    Searches across all DFIRe data (cases, indicators, notes, items). Supports AND, OR, NOT operators.

  • dfire-timeline-create

    Adds a manual timeline event to a case.

  • dfire-timeline-list

    Lists timeline events for a case (newest first).

  • dfire-user-list

    Lists users in the DFIRe tenant. Useful for looking up user IDs for assignments.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

"""DFIRe Integration for Cortex XSOAR / XSIAM

Integrates with DFIRe (Digital Forensics and Incident Response) platform
to manage cases and IOC indicators.

API reference: OpenAPI 3.0.3 — DFIRe API v1.2.8
Auth: Bearer API key (Authorization: Bearer dfire_ak_...)
"""

import os
import traceback
from datetime import datetime, UTC
from typing import Any


class Client(BaseClient):
    """Client class to interact with the DFIRe API."""

    # ── Cases ────────────────────────────────────────────

    def list_cases(
        self,
        page_size: int = 50,
        page: int | None = None,
        status: str | None = None,
        status__in: str | None = None,
        severity: str | None = None,
        case_mode: str | None = None,
        lead_investigator: int | None = None,
        created_at__gte: str | None = None,
        created_at__lte: str | None = None,
        ordering: str | None = None,
    ) -> dict[str, Any]:
        params = assign_params(
            page_size=page_size,
            page=page,
            status=status,
            status__in=status__in,
            severity=severity,
            case_mode=case_mode,
            lead_investigator=lead_investigator,
            created_at__gte=created_at__gte,
            created_at__lte=created_at__lte,
            ordering=ordering,
        )
        return self._http_request("GET", "/cases/", params=params)

    def get_case(self, case_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/cases/{case_id}/")

    def create_case(self, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", "/cases/", json_data=body)

    def update_case(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("PATCH", f"/cases/{case_id}/", json_data=body)

    def delete_case(self, case_id: int) -> None:
        self._http_request("DELETE", f"/cases/{case_id}/", resp_type="response")

    # ── Search ────────────────────────────────────────────

    def search(self, query: str) -> dict[str, Any]:
        return self._http_request("GET", "/search/", params={"q": query})

    # ── Case Types ────────────────────────────────────────

    def list_case_types(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/case-types/")

    # ── Case Notes ───────────────────────────────────────

    def list_case_notes(self, case_id: int) -> list[dict[str, Any]]:
        return self._http_request("GET", "/case-notes/", params={"case": case_id})

    def create_case_note(self, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", "/case-notes/", json_data=body)

    # ── Indicators (global) ──────────────────────────────

    def list_indicators(
        self,
        limit: int = 50,
        offset: int = 0,
        search: str | None = None,
        stix_type: str | None = None,
        classification: str | None = None,
        confidence: str | None = None,
        tlp: str | None = None,
        is_published: bool | None = None,
        is_revoked: bool | None = None,
        parent: int | None = None,
        ordering: str | None = None,
    ) -> dict[str, Any]:
        params = assign_params(
            limit=limit,
            offset=offset,
            search=search,
            stix_type=stix_type,
            classification=classification,
            confidence=confidence,
            tlp=tlp,
            is_published=is_published,
            is_revoked=is_revoked,
            parent=parent,
            ordering=ordering,
        )
        return self._http_request("GET", "/indicators/", params=params)

    def get_indicator(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/indicators/{indicator_id}/")

    def create_indicator(self, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", "/indicators/", json_data=body, ok_codes=(200, 201))

    def update_indicator(self, indicator_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("PATCH", f"/indicators/{indicator_id}/", json_data=body)

    def delete_indicator(self, indicator_id: int) -> None:
        self._http_request("DELETE", f"/indicators/{indicator_id}/", resp_type="response")

    # ── Item Types & Flags ─────────────────────────────────

    def list_item_types(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/item-types/")

    def list_item_flags(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/item-flags/")

    # ── Items (Evidence) ───────────────────────────────────

    def list_items(self, case_id: int | None = None) -> list[dict[str, Any]]:
        params: dict[str, Any] = {}
        if case_id is not None:
            params["case"] = case_id
        return self._http_request("GET", "/items/", params=params)

    def get_item(self, item_id: str) -> dict[str, Any]:
        return self._http_request("GET", f"/items/{item_id}/")

    def create_item(self, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", "/items/", json_data=body)

    # ── Attachments ──────────────────────────────────────

    def list_attachments(self, item_uuid: str | None = None) -> list[dict[str, Any]]:
        params: dict[str, Any] = {}
        if item_uuid:
            params["item"] = item_uuid
        return self._http_request("GET", "/attachments/", params=params)

    def get_attachment(self, attachment_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/attachments/{attachment_id}/")

    def delete_attachment(self, attachment_id: int) -> None:
        self._http_request("DELETE", f"/attachments/{attachment_id}/", resp_type="response")

    def chunked_upload_init(
        self, filename: str, size: int, case_id: int | None = None, item_uuid: str | None = None, category: str = "evidence"
    ) -> dict[str, Any]:
        body: dict[str, Any] = {"filename": filename, "size": size, "category": category}
        if case_id is not None:
            body["case"] = case_id
        if item_uuid:
            body["item"] = item_uuid
        return self._http_request("POST", "/attachments/chunked/init/", json_data=body)

    def chunked_upload_chunk(self, session_id: str, chunk_index: int, data: bytes) -> dict[str, Any]:
        headers = dict(self._headers) if self._headers else {}
        headers["Content-Type"] = "application/octet-stream"
        headers["X-Chunk-Index"] = str(chunk_index)
        return self._http_request(
            "POST",
            f"/attachments/chunked/{session_id}/upload/",
            data=data,
            headers=headers,
            resp_type="json",
        )

    def chunked_upload_complete(self, session_id: str) -> dict[str, Any]:
        return self._http_request("POST", f"/attachments/chunked/{session_id}/complete/")

    # ── Timeline ─────────────────────────────────────────

    def list_timeline(self, case_id: int) -> dict[str, Any] | list[dict[str, Any]]:
        return self._http_request("GET", f"/cases/{case_id}/timeline/")

    def create_timeline_event(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/timeline/", json_data=body)

    # ── Users ────────────────────────────────────────────

    def list_users(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/users/")

    # ── Case Indicators ──────────────────────────────────

    def list_case_indicators(self, case_id: int) -> list[dict[str, Any]]:
        return self._http_request("GET", f"/cases/{case_id}/indicators/")

    def add_case_indicator(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/indicators/", json_data=body)

    def remove_case_indicator(self, case_id: int, association_id: int) -> None:
        self._http_request("DELETE", f"/cases/{case_id}/indicators/{association_id}/", resp_type="response")

    # ── IOC operations ───────────────────────────────────

    def ioc_extract(self, text: str) -> dict[str, Any]:
        return self._http_request("POST", "/ioc/extract/", json_data={"text": text})

    def indicator_check(self, indicators: list[dict[str, Any]]) -> dict[str, Any]:
        return self._http_request("POST", "/indicators/check/", json_data={"indicators": indicators})

    def indicator_enrich(self, indicator_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/enrich/", json_data=body)

    def indicator_enrichments(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/indicators/{indicator_id}/enrichments/")

    def indicator_publish(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/publish/", json_data={})

    def indicator_unpublish(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/unpublish/", json_data={})

    def indicator_revoke(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/revoke/", json_data={})

    def indicator_unrevoke(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/unrevoke/", json_data={})

    def indicator_decompose(self, indicator_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/decompose/", json_data={})

    def indicator_add_tags(self, indicator_id: int, tags: list[str]) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{indicator_id}/add-tags/", json_data={"tags": tags})

    def indicator_correlated(self) -> dict[str, Any]:
        return self._http_request("GET", "/indicators/correlated/")

    def indicator_bulk(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/indicators/{path}/", json_data=body)

    # ── Case AI / reports ────────────────────────────────

    def case_generate_summary(self, case_id: int) -> str:
        return self._http_request("GET", f"/cases/{case_id}/generate-summary/", resp_type="text")

    def case_chat(self, case_id: int, message: str) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/chat/", json_data={"message": message})

    def case_update_report(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/update-report/", json_data=body)

    def case_can_report_list(self, case_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/cases/{case_id}/can-reports/")

    def case_can_report_generate(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/can-reports/generate/", json_data=body)

    def case_investigation_report_get(self, case_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/cases/{case_id}/investigation-report/")

    def case_investigation_report_generate(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/investigation-report/generate/", json_data=body)

    def case_investigation_report_finalize(self, case_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/investigation-report/finalize/", json_data={})

    def case_investigation_report_ready_for_qa(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/investigation-report/ready-for-qa/", json_data=body)

    # ── Case timeline / phase ────────────────────────────

    def case_timeline_change_phase(self, case_id: int, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/timeline/change-phase/", json_data=body)

    # ── Case todos ───────────────────────────────────────

    def case_todo_assign(self, case_id: int, todo_id: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/todo/{todo_id}/assign/", json_data=body)

    def case_todo_note(self, case_id: int, todo_id: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("PUT", f"/cases/{case_id}/todo/{todo_id}/note/", json_data=body)

    def case_todo_attach_runbook(self, case_id: int, todo_id: str, body: dict[str, Any]) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/todo/{todo_id}/attach-runbook/", json_data=body)

    def case_todo_detach_runbook(self, case_id: int, todo_id: str) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_id}/todo/{todo_id}/detach-runbook/", json_data={})

    # ── Case timers ──────────────────────────────────────

    def case_timer_list(self, case_pk: int) -> dict[str, Any]:
        return self._http_request("GET", f"/cases/{case_pk}/timers/")

    def case_timer_get(self, case_pk: int, timer_id: int) -> dict[str, Any]:
        return self._http_request("GET", f"/cases/{case_pk}/timers/{timer_id}/")

    def case_timer_complete(self, case_pk: int, timer_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_pk}/timers/{timer_id}/complete/", json_data={})

    def case_timer_reset(self, case_pk: int, timer_id: int) -> dict[str, Any]:
        return self._http_request("POST", f"/cases/{case_pk}/timers/{timer_id}/reset/", json_data={})

    # ── Convenience lookups ──────────────────────────────

    def case_get_by_number(self, case_number: str) -> dict[str, Any]:
        return self._http_request("GET", f"/case/{case_number}/")

    def item_resolve_short_id(self, short_id: str) -> dict[str, Any]:
        return self._http_request("GET", f"/item/{short_id}/")

    # ── Reference data ───────────────────────────────────

    def list_incident_categories(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/incident-categories/")

    def list_incident_phases(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/incident-phases/")

    def list_outcome_verdicts(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/outcome-verdicts/")

    def list_projects(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/projects/")

    def list_runbooks(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/runbooks/")

    def list_groups(self) -> list[dict[str, Any]]:
        return self._http_request("GET", "/groups/")


# ── Helpers ──────────────────────────────────────────────


def build_optional_body(args: dict[str, Any], fields: list[str]) -> dict[str, Any]:
    """Build a request body from args, including only non-None values."""
    body: dict[str, Any] = {}
    for field in fields:
        val = args.get(field)
        if val is not None:
            body[field] = val
    return body


# ── Command functions ────────────────────────────────────


def test_module(client: Client) -> str:
    client.list_cases(page_size=1)
    return "ok"


# Search


def search_command(client: Client, args: dict[str, Any]) -> CommandResults:
    query = args["query"]
    result = client.search(query)
    results = result.get("results", []) if isinstance(result, dict) else result
    headers = ["id", "type", "title", "snippet", "rank", "date"]
    return CommandResults(
        outputs_prefix="DFIRe.Search",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown(f'DFIRe Search Results for "{query}"', results, headers=headers, removeNull=True),
    )


# Case Types


def case_type_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    results = client.list_case_types()
    headers = ["id", "name"]
    return CommandResults(
        outputs_prefix="DFIRe.CaseType",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Case Types", results, headers=headers, removeNull=True),
    )


# Cases


def case_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    page_size = arg_to_number(args.get("limit", "50")) or 50
    page = arg_to_number(args.get("page"))
    status__in_list = argToList(args.get("status_in"))
    status__in = ",".join(status__in_list) if status__in_list else None
    result = client.list_cases(
        page_size=page_size,
        page=page,
        status=args.get("status"),
        status__in=status__in,
        severity=args.get("severity"),
        case_mode=args.get("case_mode"),
        lead_investigator=arg_to_number(args.get("lead_investigator")),
        created_at__gte=args.get("created_at_gte"),
        created_at__lte=args.get("created_at_lte"),
        ordering=args.get("ordering"),
    )
    cases = result.get("results", []) if isinstance(result, dict) else result
    headers = ["id", "case_number", "title", "status", "severity", "case_mode", "case_type_name", "created_at"]
    return CommandResults(
        outputs_prefix="DFIRe.Case",
        outputs_key_field="id",
        outputs=cases,
        readable_output=tableToMarkdown("DFIRe Cases", cases, headers=headers, removeNull=True),
    )


def case_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    result = client.get_case(case_id)
    headers = [
        "id",
        "case_number",
        "title",
        "status",
        "severity",
        "case_mode",
        "case_type_name",
        "description",
        "lead_investigator",
        "current_phase_name",
        "item_count",
        "indicator_count",
        "created_at",
        "closed_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Case",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("DFIRe Case", result, headers=headers, removeNull=True),
    )


def case_create_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body: dict[str, Any] = {
        "title": args["title"],
        "case_type": arg_to_number(args["case_type"], required=True),
        # Default to false so SOAR-created cases don't auto-create Slack channels;
        # users can opt in explicitly via the create_slack_channel arg.
        "create_slack_channel": argToBoolean(args.get("create_slack_channel", "false")),
    }
    for field in ("description", "notes", "severity", "case_mode", "external_id"):
        if args.get(field):
            body[field] = args[field]
    for int_field in ("lead_investigator", "project_id", "incident_category", "outcome_verdict"):
        val = arg_to_number(args.get(int_field))
        if val is not None:
            body[int_field] = val
    for list_field in ("investigators", "viewers", "investigator_ids", "viewer_ids"):
        ids = [arg_to_number(x) for x in argToList(args.get(list_field))]
        ids = [x for x in ids if x is not None]
        if ids:
            body[list_field] = ids
    if args.get("attributes"):
        body["attributes"] = safe_load_json(args["attributes"])

    result = client.create_case(body)
    return CommandResults(
        outputs_prefix="DFIRe.Case",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(
            "Created DFIRe Case", result, headers=["id", "case_number", "title", "status"], removeNull=True
        ),
    )


def case_update_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    body = build_optional_body(args, ["title", "description", "notes", "status", "severity", "case_mode", "external_id"])
    for int_field in ("lead_investigator", "incident_category", "outcome_verdict"):
        val = arg_to_number(args.get(int_field))
        if val is not None:
            body[int_field] = val
    for list_field in ("investigators", "viewers", "investigator_ids", "viewer_ids"):
        ids = [arg_to_number(x) for x in argToList(args.get(list_field))]
        ids = [x for x in ids if x is not None]
        if ids:
            body[list_field] = ids
    if args.get("attributes"):
        body["attributes"] = safe_load_json(args["attributes"])

    if not body:
        raise DemistoException("At least one field to update must be provided.")

    result = client.update_case(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.Case",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(
            "Updated DFIRe Case", result, headers=["id", "case_number", "title", "status"], removeNull=True
        ),
    )


def case_delete_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    client.delete_case(case_id)
    return CommandResults(readable_output=f"Case {case_id} deleted successfully.")


# Case Notes


def case_note_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    results = client.list_case_notes(case_id)
    headers = ["id", "case", "author_name", "note", "show_on_timeline", "created_at"]
    return CommandResults(
        outputs_prefix="DFIRe.CaseNote",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Case Notes", results, headers=headers, removeNull=True),
    )


def case_note_create_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body: dict[str, Any] = {
        "case": arg_to_number(args["case_id"], required=True),
        "note": args["note"],
    }
    show = args.get("show_on_timeline")
    if show is not None:
        body["show_on_timeline"] = argToBoolean(show)

    result = client.create_case_note(body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseNote",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(
            "Created Case Note", result, headers=["id", "case", "note", "author_name"], removeNull=True
        ),
    )


# Indicators


def indicator_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    limit = arg_to_number(args.get("limit", "50")) or 50
    offset = arg_to_number(args.get("offset", "0")) or 0
    is_published = argToBoolean(args["is_published"]) if args.get("is_published") else None
    is_revoked = argToBoolean(args["is_revoked"]) if args.get("is_revoked") else None

    result = client.list_indicators(
        limit=limit,
        offset=offset,
        search=args.get("search"),
        stix_type=args.get("stix_type"),
        classification=args.get("classification"),
        confidence=args.get("confidence"),
        tlp=args.get("tlp"),
        is_published=is_published,
        is_revoked=is_revoked,
        parent=arg_to_number(args.get("parent")),
        ordering=args.get("ordering"),
    )
    indicators = result.get("results", []) if isinstance(result, dict) else result
    headers = [
        "id",
        "value",
        "stix_type",
        "classification",
        "confidence",
        "tlp",
        "is_published",
        "is_revoked",
        "case_count",
        "first_seen",
        "created_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=indicators,
        readable_output=tableToMarkdown("DFIRe Indicators", indicators, headers=headers, removeNull=True),
    )


def indicator_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    result = client.get_indicator(indicator_id)
    headers = [
        "id",
        "value",
        "value_normalized",
        "stix_type",
        "classification",
        "confidence",
        "tlp",
        "tags",
        "public_notes",
        "is_published",
        "is_revoked",
        "parent",
        "case_count",
        "children_count",
        "first_seen",
        "last_seen",
        "created_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("DFIRe Indicator", result, headers=headers, removeNull=True),
    )


def indicator_create_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body: dict[str, Any] = {
        "value": args["value"],
        "stix_type": args["stix_type"],
    }
    for field in ("classification", "confidence", "tlp", "public_notes", "valid_until"):
        if args.get(field):
            body[field] = args[field]
    tags = argToList(args.get("tags"))
    if tags:
        body["tags"] = tags

    result = client.create_indicator(body)
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(
            "Created DFIRe Indicator", result, headers=["id", "value", "stix_type", "is_existing"], removeNull=True
        ),
    )


def indicator_update_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    body = build_optional_body(args, ["classification", "confidence", "tlp", "public_notes", "valid_until"])
    tags = argToList(args.get("tags"))
    if tags:
        body["tags"] = tags

    if not body:
        raise DemistoException("At least one field to update must be provided.")

    result = client.update_indicator(indicator_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(
            "Updated DFIRe Indicator", result, headers=["id", "value", "classification", "confidence", "tlp"], removeNull=True
        ),
    )


def indicator_delete_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    client.delete_indicator(indicator_id)
    return CommandResults(readable_output=f"Indicator {indicator_id} deleted successfully.")


# Item Types & Flags


def item_type_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    results = client.list_item_types()
    headers = ["id", "name", "icon"]
    return CommandResults(
        outputs_prefix="DFIRe.ItemType",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Item Types", results, headers=headers, removeNull=True),
    )


def item_flag_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    results = client.list_item_flags()
    headers = ["id", "name", "color", "description"]
    return CommandResults(
        outputs_prefix="DFIRe.ItemFlag",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Item Flags", results, headers=headers, removeNull=True),
    )


# Items (Evidence)


def item_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args.get("case_id"))
    results = client.list_items(case_id=case_id)
    headers = ["uuid", "name", "display_title", "item_type_name", "case", "location", "attachment_count", "created_at"]
    return CommandResults(
        outputs_prefix="DFIRe.Item",
        outputs_key_field="uuid",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Evidence Items", results, headers=headers, removeNull=True),
    )


def item_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    item_id = args.get("item_id")
    if not item_id:
        raise DemistoException("`item_id` is required.")
    result = client.get_item(item_id)
    headers = [
        "uuid",
        "name",
        "display_title",
        "item_type_name",
        "case",
        "location",
        "owner_details",
        "primary_user_details",
        "attachment_count",
        "created_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Item",
        outputs_key_field="uuid",
        outputs=result,
        readable_output=tableToMarkdown("DFIRe Evidence Item", result, headers=headers, removeNull=True),
    )


def item_create_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body: dict[str, Any] = {
        "case": arg_to_number(args["case_id"], required=True),
        "item_type": arg_to_number(args["item_type"], required=True),
        "location": args["location"],
    }
    if args.get("name"):
        body["name"] = args["name"]
    for int_field in ("owner_id", "primary_user_id", "collected_by", "parent_item"):
        val = args.get(int_field)
        if val is not None:
            body[int_field] = val if int_field == "parent_item" else arg_to_number(val)

    result = client.create_item(body)
    return CommandResults(
        outputs_prefix="DFIRe.Item",
        outputs_key_field="uuid",
        outputs=result,
        readable_output=tableToMarkdown(
            "Created Evidence Item", result, headers=["uuid", "name", "display_title", "case", "location"], removeNull=True
        ),
    )


# Attachments


def attachment_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    item_uuid = args.get("item_uuid")
    results = client.list_attachments(item_uuid=item_uuid)
    headers = [
        "id",
        "filename",
        "mime_type",
        "size",
        "category",
        "case",
        "item",
        "hash_sha256",
        "uploaded_by_name",
        "uploaded_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Attachment",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Attachments", results, headers=headers, removeNull=True),
    )


def attachment_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    attachment_id = arg_to_number(args["attachment_id"], required=True)
    assert attachment_id is not None
    result = client.get_attachment(attachment_id)
    headers = [
        "id",
        "filename",
        "mime_type",
        "size",
        "category",
        "description",
        "case",
        "item",
        "hash_sha256",
        "status",
        "storage_location",
        "uploaded_by_name",
        "uploaded_at",
    ]
    return CommandResults(
        outputs_prefix="DFIRe.Attachment",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("DFIRe Attachment", result, headers=headers, removeNull=True),
    )


def attachment_upload_command(client: Client, args: dict[str, Any]) -> CommandResults:
    entry_id = args["entry_id"]
    file_info = demisto.getFilePath(entry_id)
    file_path = file_info["path"]
    file_name = args.get("filename") or file_info.get("name", "upload")

    case_id = arg_to_number(args.get("case_id"))
    item_uuid = args.get("item_uuid")
    category = args.get("category", "general")

    file_size = os.path.getsize(file_path)

    # Step 1: Init session
    init_resp = client.chunked_upload_init(
        filename=file_name,
        size=file_size,
        case_id=case_id,
        item_uuid=item_uuid,
        category=category,
    )
    session_id = init_resp["session_id"]
    chunk_size = init_resp.get("chunk_size", 5 * 1024 * 1024)  # default 5MB

    # Step 2: Upload chunks
    chunk_index = 0
    with open(file_path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            try:
                client.chunked_upload_chunk(session_id, chunk_index, chunk)
            except Exception as e:
                raise DemistoException(
                    f"Failed to upload chunk {chunk_index} of '{file_name}' (session {session_id}): {e}"
                ) from e
            chunk_index += 1

    # Step 3: Complete
    result = client.chunked_upload_complete(session_id)

    return CommandResults(
        outputs_prefix="DFIRe.Attachment",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("Uploaded Attachment", result, removeNull=True),
    )


def attachment_delete_command(client: Client, args: dict[str, Any]) -> CommandResults:
    attachment_id = arg_to_number(args["attachment_id"], required=True)
    assert attachment_id is not None
    client.delete_attachment(attachment_id)
    return CommandResults(readable_output=f"Attachment {attachment_id} deleted successfully.")


# Timeline


def timeline_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    result = client.list_timeline(case_id)
    events = result.get("events", []) if isinstance(result, dict) else result
    headers = ["id", "event_type", "subject", "details", "event_datetime", "created_by_name"]
    return CommandResults(
        outputs_prefix="DFIRe.TimelineEvent",
        outputs_key_field="id",
        outputs=events,
        readable_output=tableToMarkdown("DFIRe Timeline Events", events, headers=headers, removeNull=True),
    )


def timeline_create_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    # The DFIRe API requires event_datetime; default to "now" if not supplied so
    # playbook authors don't have to compute a timestamp for the common case.
    event_datetime = args.get("event_datetime") or datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
    body: dict[str, Any] = {
        "subject": args["subject"],
        "event_datetime": event_datetime,
    }
    if args.get("details"):
        body["details"] = args["details"]

    result = client.create_timeline_event(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.TimelineEvent",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("Created Timeline Event", result, removeNull=True),
    )


# Users


def user_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    results = client.list_users()
    headers = ["id", "username", "full_name", "email", "is_active", "groups"]
    return CommandResults(
        outputs_prefix="DFIRe.User",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("DFIRe Users", results, headers=headers, removeNull=True),
    )


# Case Indicators


def case_indicator_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    results = client.list_case_indicators(case_id)
    headers = ["id", "indicator.id", "indicator.value", "indicator.stix_type", "context", "source", "case_count", "created_at"]

    # Flatten nested indicator fields for the readable table
    table_data = []
    for r in results:
        row = dict(r)
        ind = row.pop("indicator", {}) or {}
        row["indicator.id"] = ind.get("id")
        row["indicator.value"] = ind.get("value")
        row["indicator.stix_type"] = ind.get("stix_type")
        table_data.append(row)

    return CommandResults(
        outputs_prefix="DFIRe.CaseIndicator",
        outputs_key_field="id",
        outputs=results,
        readable_output=tableToMarkdown("Case Indicators", table_data, headers=headers, removeNull=True),
    )


def case_indicator_add_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    body: dict[str, Any] = {
        "value": args["value"],
        "stix_type": args["stix_type"],
    }
    for field in ("classification", "confidence", "tlp", "context", "source", "source_reference", "valid_until"):
        if args.get(field):
            body[field] = args[field]
    tags = argToList(args.get("tags"))
    if tags:
        body["tags"] = tags
    for bool_field in ("decompose", "publish"):
        val = args.get(bool_field)
        if val is not None:
            body[bool_field] = argToBoolean(val)

    result = client.add_case_indicator(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseIndicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown("Added Indicator to Case", result, removeNull=True),
    )


def case_indicator_remove_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    association_id = arg_to_number(args["association_id"], required=True)
    assert association_id is not None
    client.remove_case_indicator(case_id, association_id)
    return CommandResults(readable_output=f"Indicator association {association_id} removed from case {case_id}.")


# IOC operations


def ioc_extract_command(client: Client, args: dict[str, Any]) -> CommandResults:
    text = args["text"]
    result = client.ioc_extract(text)
    candidates = result.get("candidates", []) if isinstance(result, dict) else result
    headers = ["value", "stix_type", "context"]
    return CommandResults(
        outputs_prefix="DFIRe.IOCExtraction",
        outputs=result,
        readable_output=tableToMarkdown("Extracted IOC Candidates", candidates, headers=headers, removeNull=True),
    )


def indicator_check_command(client: Client, args: dict[str, Any]) -> CommandResults:
    raw_indicators = args.get("indicators")
    if raw_indicators:
        indicators = safe_load_json(raw_indicators)
        if not isinstance(indicators, list):
            raise DemistoException("`indicators` must be a JSON array of {value, stix_type} objects.")
    else:
        values = argToList(args.get("values"))
        stix_type = args.get("stix_type")
        if not values or not stix_type:
            raise DemistoException("Provide either `indicators` (JSON list) or both `values` and `stix_type`.")
        indicators = [{"value": v, "stix_type": stix_type} for v in values]

    result = client.indicator_check(indicators)
    rows = result.get("results", []) if isinstance(result, dict) else result
    return CommandResults(
        outputs_prefix="DFIRe.IndicatorCheck",
        outputs=result,
        readable_output=tableToMarkdown("Indicator Check Results", rows, removeNull=True),
    )


def indicator_enrich_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    body: dict[str, Any] = {}
    providers = argToList(args.get("providers"))
    if providers:
        body["providers"] = providers
    force = args.get("force")
    if force is not None:
        body["force"] = argToBoolean(force)

    result = client.indicator_enrich(indicator_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Enriched Indicator {indicator_id}", result, removeNull=True),
    )


def indicator_enrichment_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    result = client.indicator_enrichments(indicator_id)
    rows = result.get("enrichments", []) if isinstance(result, dict) else result
    return CommandResults(
        outputs_prefix="DFIRe.Enrichment",
        outputs=result,
        readable_output=tableToMarkdown(f"Enrichments for Indicator {indicator_id}", rows, removeNull=True),
    )


def _indicator_lifecycle(client_fn, args: dict[str, Any], action_label: str) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    result = client_fn(indicator_id)
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"{action_label} Indicator {indicator_id}", result, removeNull=True),
    )


def indicator_publish_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _indicator_lifecycle(client.indicator_publish, args, "Published")


def indicator_unpublish_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _indicator_lifecycle(client.indicator_unpublish, args, "Unpublished")


def indicator_revoke_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _indicator_lifecycle(client.indicator_revoke, args, "Revoked")


def indicator_unrevoke_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _indicator_lifecycle(client.indicator_unrevoke, args, "Unrevoked")


def indicator_decompose_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _indicator_lifecycle(client.indicator_decompose, args, "Decomposed")


def indicator_add_tags_command(client: Client, args: dict[str, Any]) -> CommandResults:
    indicator_id = arg_to_number(args["indicator_id"], required=True)
    assert indicator_id is not None
    tags = argToList(args["tags"])
    if not tags:
        raise DemistoException("`tags` argument is required.")
    result = client.indicator_add_tags(indicator_id, tags)
    return CommandResults(
        outputs_prefix="DFIRe.Indicator",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Tagged Indicator {indicator_id}", result, removeNull=True),
    )


def indicator_correlated_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    result = client.indicator_correlated()
    rows = result.get("results", []) if isinstance(result, dict) else result
    return CommandResults(
        outputs_prefix="DFIRe.IndicatorCorrelated",
        outputs=result,
        readable_output=tableToMarkdown("Correlated Indicators", rows, removeNull=True),
    )


def _build_bulk_body(args: dict[str, Any], extra_fields: dict[str, Any]) -> dict[str, Any]:
    ids = [arg_to_number(x) for x in argToList(args.get("indicator_ids"))]
    ids = [x for x in ids if x is not None]
    if not ids:
        raise DemistoException("`indicator_ids` argument is required.")
    body: dict[str, Any] = {"indicator_ids": ids}
    body.update({k: v for k, v in extra_fields.items() if v is not None})
    return body


def indicator_bulk_classify_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {"classification": args.get("classification")})
    if "classification" not in body:
        raise DemistoException("`classification` argument is required.")
    result = client.indicator_bulk("bulk-classify", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Classify Result", result, removeNull=True),
    )


def indicator_bulk_confidence_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {"confidence": args.get("confidence")})
    if "confidence" not in body:
        raise DemistoException("`confidence` argument is required.")
    result = client.indicator_bulk("bulk-confidence", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Confidence Result", result, removeNull=True),
    )


def indicator_bulk_tag_command(client: Client, args: dict[str, Any]) -> CommandResults:
    tags = argToList(args.get("tags"))
    if not tags:
        raise DemistoException("`tags` argument is required.")
    body = _build_bulk_body(args, {"tags": tags, "mode": args.get("mode")})
    result = client.indicator_bulk("bulk-tag", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Tag Result", result, removeNull=True),
    )


def indicator_bulk_tlp_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {"tlp": args.get("tlp")})
    if "tlp" not in body:
        raise DemistoException("`tlp` argument is required.")
    result = client.indicator_bulk("bulk-tlp", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk TLP Result", result, removeNull=True),
    )


def indicator_bulk_publish_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {})
    result = client.indicator_bulk("bulk-publish", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkPublishResponse",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Publish Result", result, removeNull=True),
    )


def indicator_bulk_revoke_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {})
    result = client.indicator_bulk("bulk-revoke", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Revoke Result", result, removeNull=True),
    )


def indicator_bulk_delete_command(client: Client, args: dict[str, Any]) -> CommandResults:
    body = _build_bulk_body(args, {})
    result = client.indicator_bulk("bulk-delete", body)
    return CommandResults(
        outputs_prefix="DFIRe.BulkResult",
        outputs=result,
        readable_output=tableToMarkdown("Bulk Delete Result", result, removeNull=True),
    )


# Case AI / reports


def case_generate_summary_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    summary_text = client.case_generate_summary(case_id)
    output = {"case_id": case_id, "summary": summary_text}
    return CommandResults(
        outputs_prefix="DFIRe.CaseSummary",
        outputs_key_field="case_id",
        outputs=output,
        readable_output=f"### Case {case_id} Summary\n\n```\n{summary_text}\n```",
    )


def case_chat_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    message = args["message"]
    result = client.case_chat(case_id, message)
    return CommandResults(
        outputs_prefix="DFIRe.CaseChat",
        outputs=result,
        readable_output=tableToMarkdown(f"Case {case_id} Chat Response", result, removeNull=True),
    )


def case_update_report_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    report_id = arg_to_number(args["report_id"], required=True)
    assert case_id is not None
    assert report_id is not None
    body = {"id": report_id, "report_text": args["report_text"]}
    result = client.case_update_report(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseReport",
        outputs=result,
        readable_output=tableToMarkdown(f"Case {case_id} Report {report_id} Update", result, removeNull=True),
    )


def case_can_report_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    result = client.case_can_report_list(case_id)
    rows = result.get("results", []) if isinstance(result, dict) else result
    return CommandResults(
        outputs_prefix="DFIRe.CANReport",
        outputs_key_field="id",
        outputs=rows if isinstance(rows, list) else result,
        readable_output=tableToMarkdown(f"CAN Reports for Case {case_id}", rows, removeNull=True),
    )


def case_can_report_generate_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    body = safe_load_json(args["body"]) if args.get("body") else {}
    result = client.case_can_report_generate(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CANReport",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Generated CAN Report for Case {case_id}", result, removeNull=True),
    )


def case_investigation_report_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    result = client.case_investigation_report_get(case_id)
    return CommandResults(
        outputs_prefix="DFIRe.InvestigationReport",
        outputs=result,
        readable_output=tableToMarkdown(f"Investigation Report for Case {case_id}", result, removeNull=True),
    )


def case_investigation_report_generate_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    section_id = arg_to_number(args["section_id"], required=True)
    assert case_id is not None
    assert section_id is not None
    body: dict[str, Any] = {"section_id": section_id}
    result = client.case_investigation_report_generate(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.InvestigationReport",
        outputs=result,
        readable_output=tableToMarkdown(
            f"Generated content for section {section_id} of Case {case_id} report", result, removeNull=True
        ),
    )


def case_investigation_report_finalize_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    result = client.case_investigation_report_finalize(case_id)
    return CommandResults(
        outputs_prefix="DFIRe.InvestigationReport",
        outputs=result,
        readable_output=tableToMarkdown(f"Finalized Investigation Report for Case {case_id}", result, removeNull=True),
    )


def case_investigation_report_ready_for_qa_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    section_id = arg_to_number(args["section_id"], required=True)
    assert case_id is not None
    assert section_id is not None
    result = client.case_investigation_report_ready_for_qa(case_id, {"section_id": section_id})
    return CommandResults(
        outputs_prefix="DFIRe.InvestigationReport",
        outputs=result,
        readable_output=tableToMarkdown(
            f"Section {section_id} of Case {case_id} report marked ready for QA", result, removeNull=True
        ),
    )


# Timeline phase


def case_timeline_change_phase_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    body: dict[str, Any] = {}
    phase = arg_to_number(args.get("phase_id"))
    if phase is not None:
        body["phase_id"] = phase
    if args.get("phase_name"):
        body["phase_name"] = args["phase_name"]
    if args.get("note"):
        body["note"] = args["note"]
    result = client.case_timeline_change_phase(case_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.TimelineEvent",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Phase change on Case {case_id}", result, removeNull=True),
    )


# Case todos


def _extract_case_todos(client: Client, case_id: int) -> list[dict[str, Any]]:
    """Fetch a case and return its `todo_checklist`. The DFIRe API does not expose a
    separate GET endpoint for todos; they live on the Case resource itself."""
    case = client.get_case(case_id)
    todos = case.get("todo_checklist") if isinstance(case, dict) else None
    if isinstance(todos, list):
        return todos
    if isinstance(todos, dict):
        # Some serializers nest the list inside a results/items key.
        for key in ("results", "items", "todos"):
            inner = todos.get(key)
            if isinstance(inner, list):
                return inner
    return []


def case_todo_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todos = _extract_case_todos(client, case_id)
    headers = ["id", "title", "status", "assignee_name", "runbook_slug", "created_at"]
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=todos,
        readable_output=tableToMarkdown(f"Todos for Case {case_id}", todos, headers=headers, removeNull=True),
    )


def case_todo_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todo_id = str(args["todo_id"])
    todos = _extract_case_todos(client, case_id)
    matches = [t for t in todos if str(t.get("id")) == todo_id]
    if not matches:
        raise DemistoException(f"No todo with id {todo_id} found on case {case_id}.")
    result = matches[0]
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Todo {todo_id} (Case {case_id})", result, removeNull=True),
    )


def case_todo_assign_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todo_id = str(args["todo_id"])
    body: dict[str, Any] = {}
    user_id = arg_to_number(args.get("user_id"))
    if user_id is not None:
        body["user_id"] = user_id
    result = client.case_todo_assign(case_id, todo_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Assigned todo {todo_id} on Case {case_id}", result, removeNull=True),
    )


def case_todo_note_set_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todo_id = str(args["todo_id"])
    body = {"note": args["note"]}
    result = client.case_todo_note(case_id, todo_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Updated note on todo {todo_id} (Case {case_id})", result, removeNull=True),
    )


def case_todo_attach_runbook_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todo_id = str(args["todo_id"])
    body = {"runbook_slug": args["runbook_slug"]}
    result = client.case_todo_attach_runbook(case_id, todo_id, body)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Attached runbook to todo {todo_id} (Case {case_id})", result, removeNull=True),
    )


def case_todo_detach_runbook_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_id = arg_to_number(args["case_id"], required=True)
    assert case_id is not None
    todo_id = str(args["todo_id"])
    result = client.case_todo_detach_runbook(case_id, todo_id)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTodo",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Detached runbook from todo {todo_id} (Case {case_id})", result, removeNull=True),
    )


# Case timers


def case_timer_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_pk = arg_to_number(args["case_id"], required=True)
    assert case_pk is not None
    result = client.case_timer_list(case_pk)
    rows = result.get("results", []) if isinstance(result, dict) else result
    headers = ["id", "name", "framework", "duration_hours", "start_time", "end_time"]
    return CommandResults(
        outputs_prefix="DFIRe.CaseTimer",
        outputs_key_field="id",
        outputs=rows if isinstance(rows, list) else result,
        readable_output=tableToMarkdown(f"Timers for Case {case_pk}", rows, headers=headers, removeNull=True),
    )


def case_timer_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_pk = arg_to_number(args["case_id"], required=True)
    timer_id = arg_to_number(args["timer_id"], required=True)
    assert case_pk is not None
    assert timer_id is not None
    result = client.case_timer_get(case_pk, timer_id)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTimer",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Timer {timer_id} (Case {case_pk})", result, removeNull=True),
    )


def case_timer_complete_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_pk = arg_to_number(args["case_id"], required=True)
    timer_id = arg_to_number(args["timer_id"], required=True)
    assert case_pk is not None
    assert timer_id is not None
    result = client.case_timer_complete(case_pk, timer_id)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTimer",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Completed timer {timer_id} on Case {case_pk}", result, removeNull=True),
    )


def case_timer_reset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_pk = arg_to_number(args["case_id"], required=True)
    timer_id = arg_to_number(args["timer_id"], required=True)
    assert case_pk is not None
    assert timer_id is not None
    result = client.case_timer_reset(case_pk, timer_id)
    return CommandResults(
        outputs_prefix="DFIRe.CaseTimer",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"Reset timer {timer_id} on Case {case_pk}", result, removeNull=True),
    )


# Convenience lookups


def case_get_by_number_command(client: Client, args: dict[str, Any]) -> CommandResults:
    case_number = args["case_number"]
    result = client.case_get_by_number(case_number)
    return CommandResults(
        outputs_prefix="DFIRe.Case",
        outputs_key_field="id",
        outputs=result,
        readable_output=tableToMarkdown(f"DFIRe Case {case_number}", result, removeNull=True),
    )


def item_resolve_short_id_command(client: Client, args: dict[str, Any]) -> CommandResults:
    short_id = args["short_id"]
    result = client.item_resolve_short_id(short_id)
    return CommandResults(
        outputs_prefix="DFIRe.Item",
        outputs_key_field="uuid",
        outputs=result,
        readable_output=tableToMarkdown(f"DFIRe Item Short-ID {short_id}", result, removeNull=True),
    )


# Reference data


def _list_reference(client_fn, prefix: str, label: str) -> CommandResults:
    result = client_fn()
    rows = result.get("results", []) if isinstance(result, dict) else result
    return CommandResults(
        outputs_prefix=prefix,
        outputs_key_field="id",
        outputs=rows if isinstance(rows, list) else result,
        readable_output=tableToMarkdown(label, rows, removeNull=True),
    )


def incident_category_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_incident_categories, "DFIRe.IncidentCategory", "DFIRe Incident Categories")


def incident_phase_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_incident_phases, "DFIRe.IncidentPhase", "DFIRe Incident Phases")


def outcome_verdict_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_outcome_verdicts, "DFIRe.OutcomeVerdict", "DFIRe Outcome Verdicts")


def project_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_projects, "DFIRe.Project", "DFIRe Projects")


def runbook_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_runbooks, "DFIRe.Runbook", "DFIRe Runbooks")


def group_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    return _list_reference(client.list_groups, "DFIRe.Group", "DFIRe Groups")


# ── Command dispatch ─────────────────────────────────────

COMMANDS: dict[str, Any] = {
    "dfire-search": search_command,
    "dfire-case-type-list": case_type_list_command,
    "dfire-case-list": case_list_command,
    "dfire-case-get": case_get_command,
    "dfire-case-create": case_create_command,
    "dfire-case-update": case_update_command,
    "dfire-case-delete": case_delete_command,
    "dfire-case-note-list": case_note_list_command,
    "dfire-case-note-create": case_note_create_command,
    "dfire-indicator-list": indicator_list_command,
    "dfire-indicator-get": indicator_get_command,
    "dfire-indicator-create": indicator_create_command,
    "dfire-indicator-update": indicator_update_command,
    "dfire-indicator-delete": indicator_delete_command,
    "dfire-item-type-list": item_type_list_command,
    "dfire-item-flag-list": item_flag_list_command,
    "dfire-item-list": item_list_command,
    "dfire-item-get": item_get_command,
    "dfire-item-create": item_create_command,
    "dfire-attachment-list": attachment_list_command,
    "dfire-attachment-get": attachment_get_command,
    "dfire-attachment-upload": attachment_upload_command,
    "dfire-attachment-delete": attachment_delete_command,
    "dfire-timeline-list": timeline_list_command,
    "dfire-timeline-create": timeline_create_command,
    "dfire-user-list": user_list_command,
    "dfire-case-indicator-list": case_indicator_list_command,
    "dfire-case-indicator-add": case_indicator_add_command,
    "dfire-case-indicator-remove": case_indicator_remove_command,
    # IOC operations
    "dfire-ioc-extract": ioc_extract_command,
    "dfire-indicator-check": indicator_check_command,
    "dfire-indicator-enrich": indicator_enrich_command,
    "dfire-indicator-enrichment-list": indicator_enrichment_list_command,
    "dfire-indicator-publish": indicator_publish_command,
    "dfire-indicator-unpublish": indicator_unpublish_command,
    "dfire-indicator-revoke": indicator_revoke_command,
    "dfire-indicator-unrevoke": indicator_unrevoke_command,
    "dfire-indicator-decompose": indicator_decompose_command,
    "dfire-indicator-add-tags": indicator_add_tags_command,
    "dfire-indicator-correlated-list": indicator_correlated_list_command,
    "dfire-indicator-bulk-classify": indicator_bulk_classify_command,
    "dfire-indicator-bulk-confidence": indicator_bulk_confidence_command,
    "dfire-indicator-bulk-tag": indicator_bulk_tag_command,
    "dfire-indicator-bulk-tlp": indicator_bulk_tlp_command,
    "dfire-indicator-bulk-publish": indicator_bulk_publish_command,
    "dfire-indicator-bulk-revoke": indicator_bulk_revoke_command,
    "dfire-indicator-bulk-delete": indicator_bulk_delete_command,
    # Case AI / reports
    "dfire-case-generate-summary": case_generate_summary_command,
    "dfire-case-chat": case_chat_command,
    "dfire-case-update-report": case_update_report_command,
    "dfire-case-can-report-list": case_can_report_list_command,
    "dfire-case-can-report-generate": case_can_report_generate_command,
    "dfire-case-investigation-report-get": case_investigation_report_get_command,
    "dfire-case-investigation-report-generate": case_investigation_report_generate_command,
    "dfire-case-investigation-report-finalize": case_investigation_report_finalize_command,
    "dfire-case-investigation-report-ready-for-qa": case_investigation_report_ready_for_qa_command,
    # Timeline phase
    "dfire-case-timeline-change-phase": case_timeline_change_phase_command,
    # Todos
    "dfire-case-todo-list": case_todo_list_command,
    "dfire-case-todo-get": case_todo_get_command,
    "dfire-case-todo-assign": case_todo_assign_command,
    "dfire-case-todo-note-set": case_todo_note_set_command,
    "dfire-case-todo-attach-runbook": case_todo_attach_runbook_command,
    "dfire-case-todo-detach-runbook": case_todo_detach_runbook_command,
    # Timers
    "dfire-case-timer-list": case_timer_list_command,
    "dfire-case-timer-get": case_timer_get_command,
    "dfire-case-timer-complete": case_timer_complete_command,
    "dfire-case-timer-reset": case_timer_reset_command,
    # Convenience lookups
    "dfire-case-get-by-number": case_get_by_number_command,
    "dfire-item-resolve-short-id": item_resolve_short_id_command,
    # Reference data
    "dfire-incident-category-list": incident_category_list_command,
    "dfire-incident-phase-list": incident_phase_list_command,
    "dfire-outcome-verdict-list": outcome_verdict_list_command,
    "dfire-project-list": project_list_command,
    "dfire-runbook-list": runbook_list_command,
    "dfire-group-list": group_list_command,
}


def main():
    params = demisto.params()
    base_url = urljoin(params.get("url", ""), "/api")
    credentials = params.get("apikey") or {}
    api_key = credentials.get("password", "")
    if not api_key:
        return_error("API Key is required. Configure it in the integration instance.")
    verify_certificate = not argToBoolean(params.get("insecure", False))
    proxy = argToBoolean(params.get("proxy", False))

    headers = {"Authorization": f"Bearer {api_key}"}

    client = Client(
        base_url=base_url,
        verify=verify_certificate,
        headers=headers,
        proxy=proxy,
    )

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

    try:
        if command == "test-module":
            return_results(test_module(client))
        elif command in COMMANDS:
            return_results(COMMANDS[command](client, demisto.args()))
        else:
            raise NotImplementedError(f"Command {command} is not implemented.")
    except DemistoException as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}\n\n{traceback.format_exc()}")


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()