Details
| ID | ThreatZone |
|---|---|
| Provider | Malwation |
| Category | Forensics & Malware Analysis |
| From Version | 6.9.0 |
| Docker Image | demisto/threatzone-sdk:1.1.1.12287331 |
| Supported Modules | Agentix XSIAM |
README
ThreatZone malware analysis sandboxing.
This integration was integrated and tested with ThreatZone.
Configure ThreatZone in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL (e.g. https://app.threat.zone) | The ThreatZone instance root or /public-api URL. The integration appends the suffix exactly once. | True |
| ThreatZone API Key | The API key generated for the ThreatZone workspace. | True |
| Source Reliability | The reliability of the source providing the intelligence data. | False |
| Trust any certificate (not secure) | Whether to trust any certificate (not secure) by disabling TLS certificate validation. | False |
| Use system proxy settings | Whether to use the system proxy settings for SDK requests. | 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.
tz-sandbox-upload-sample
Submits a sample to ThreatZone for sandbox analysis.
Base Command
tz-sandbox-upload-sample
Input
| Argument Name | Description | Required |
|---|---|---|
| entry_id | The entry ID of the file to submit. | Required |
| environment | The environment to run the submission in. Possible values are: w7_x64, w10_x64, w11_x64, linux-ubuntu_2204, macos-ventura, android9. Default is w7_x64. | Optional |
| private | Whether the submission is private. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| extension_check | Whether to enforce MIME-based extension correction before sandbox execution. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| auto | Whether to automatically select a sandbox environment based on the uploaded file type, ignoring the environment argument. Possible values are: true, false. Default is false. | Optional |
| modules | The legacy compatibility argument. ThreatZone v3.2 does not support per-submission module selection, so this value is validated but not forwarded. | Optional |
| analyze_config | The raw analyzeConfig JSON used to override or add sandbox metafields. | Optional |
| timeout | The duration of the submission analysis. If omitted, the current ThreatZone API-provided default is used. Possible values are: 60, 120, 180, 300. | Optional |
| work_path | The working path of the submission. If omitted, the current ThreatZone API-provided default is used. Possible values are: desktop, root, appdata, windows, temp. | Optional |
| mouse_simulation | Whether to enable mouse simulation. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| https_inspection | Whether to enable HTTPS inspection for encrypted traffic. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| internet_connection | Whether to enable internet connection. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| raw_logs | Whether to request raw logs. This legacy compatibility argument is not forwarded because ThreatZone v3.2 does not accept the metafield. Possible values are: true, false. | Optional |
| snapshot | Whether to enable Fast Bootup. If omitted, the current ThreatZone API-provided default is used. Possible values are: true, false. | Optional |
| entrypoint | The filename inside the archive to execute (for archives). | Optional |
| password | The password for password-protected archives. | Optional |
| configurations | The advanced execution configuration as a JSON object (for example preScript, startArguments, or networkConfig). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Sandbox.UUID | String | The UUID of the sample. |
| ThreatZone.Limits.E_Mail | String | The owner e-mail of current plan. |
| ThreatZone.Limits.API_Limit | String | The remaining/total API request limits of the current plan. |
| ThreatZone.Limits.Concurrent_Limit | String | The remaining/total concurrent analysis limits of the current plan. |
| ThreatZone.Limits.Daily_Submission_Limit | String | The remaining/total daily submission limits of the current plan. |
tz-static-upload-sample
Submits a sample to ThreatZone for static analysis.
Base Command
tz-static-upload-sample
Input
| Argument Name | Description | Required |
|---|---|---|
| entry_id | The entry ID of the file to submit. | Required |
| private | Whether the submission is visible only to members of your workspace. When false, the submission is visible to everyone. Possible values are: true, false. Default is true. | Optional |
| extension_check | Whether to enforce MIME-based extension correction before static scan. Possible values are: true, false. Default is false. | Optional |
| entrypoint | The filename inside the archive to analyze (for archives). | Optional |
| password | The password for password-protected archives. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Static.UUID | String | The UUID of the sample. |
| ThreatZone.Limits.E_Mail | String | The owner e-mail of current plan. |
| ThreatZone.Limits.API_Limit | String | The remaining/total API request limits of the current plan. |
| ThreatZone.Limits.Concurrent_Limit | String | The remaining/total concurrent analysis limits of the current plan. |
| ThreatZone.Limits.Daily_Submission_Limit | String | The remaining/total daily submission limits of the current plan. |
tz-cdr-upload-sample
Submits a sample to ThreatZone for CDR.
Base Command
tz-cdr-upload-sample
Input
| Argument Name | Description | Required |
|---|---|---|
| entry_id | The entry ID of the file to submit. | Required |
| private | Whether the submission is visible only to members of your workspace. When false, the submission is visible to everyone. Possible values are: true, false. Default is true. | Optional |
| extension_check | Whether to enforce MIME-based extension correction before sanitization. Possible values are: true, false. Default is true. | Optional |
| entrypoint | The filename inside the archive to sanitize (for archives). | Optional |
| password | The password for password-protected archives. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.CDR.UUID | String | The UUID of the sample. |
| ThreatZone.Limits.E_Mail | String | The owner e-mail of current plan. |
| ThreatZone.Limits.API_Limit | String | The remaining/total API request limits of the current plan. |
| ThreatZone.Limits.Concurrent_Limit | String | The remaining/total concurrent analysis limits of the current plan. |
| ThreatZone.Limits.Daily_Submission_Limit | String | The remaining/total daily submission limits of the current plan. |
tz-url-analysis
Submits a URL to ThreatZone for analysis.
Base Command
tz-url-analysis
Input
| Argument Name | Description | Required |
|---|---|---|
| url | The URL to analyze. | Required |
| private | Whether to mark the submission as workspace-private. Possible values are: true, false. Default is true. | Optional |
| safe_browsing | Whether to start an isolated safe-browsing session alongside URL analysis. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.URL.UUID | String | The UUID of the URL submission. |
| ThreatZone.Submission.URL.URL | String | The URL submitted for analysis. |
| ThreatZone.Limits.E_Mail | String | The owner e-mail of current plan. |
| ThreatZone.Limits.API_Limit | String | The remaining/total API request limits of the current plan. |
| ThreatZone.Limits.Concurrent_Limit | String | The remaining/total concurrent analysis limits of the current plan. |
| ThreatZone.Limits.Daily_Submission_Limit | String | The remaining/total daily submission limits of the current plan. |
tz-get-result
Retrieve the analysis result from ThreatZone.
Base Command
tz-get-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| details | Whether to include inline Indicators, IOCs, YARA matches, artifacts, and configuration data in the readable output. Possible values are: true, false. Default is false. | Optional |
| download_sanitized | Whether to download the sanitized file after a CDR analysis completes. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission | Unknown | The raw submission data returned by ThreatZone. |
| ThreatZone.Analysis.STATUS | Number | The status of the submission scanning process as an integer code (0=declined, 1=file received, 2=accepted, 3=running, 4=VM ready, 5=finished). |
| ThreatZone.Analysis.LEVEL | Number | The threat level of the scanned file as an integer code (0=not measured, 1=informative, 2=suspicious, 3=malicious). |
| ThreatZone.Analysis.INFO | String | The submission metadata such as file name/URL and privacy status. |
| ThreatZone.Analysis.REPORT | String | The analysis report of the submission. |
| ThreatZone.Analysis.URL | String | The result page url of the submission. |
| ThreatZone.Analysis.MD5 | String | The md5 hash of the submission. |
| ThreatZone.Analysis.SHA1 | String | The sha1 hash of the submission. |
| ThreatZone.Analysis.SHA256 | String | The sha256 hash of the submission. |
| ThreatZone.Analysis.UUID | String | The UUID of the submission. |
| ThreatZone.Analysis.SANITIZED | String | The url of the sanitized file. |
| ThreatZone.IOC.URL | List | The URL data extracted from IOC. |
| ThreatZone.IOC.IP | List | The IP data extracted from IOC. |
| ThreatZone.IOC.DOMAIN | List | The DOMAIN data extracted from IOC. |
| ThreatZone.IOC.EMAIL | List | The EMAIL data extracted from IOC. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Reliability | String | The reliability of the source providing the intelligence data. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | unknown | The vendor used to calculate the score. |
tz-get-indicator-result
Retrieves dynamic behaviour indicators for a submission from ThreatZone.
Base Command
tz-get-indicator-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| level | The indicator severity to filter by. Possible values are: benign, suspicious, malicious. | Optional |
| category | The indicator category to filter by. | Optional |
| pid | The process ID to filter by. | Optional |
| attack_code | The MITRE ATT&CK technique code to filter by. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Indicators.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Indicators.Data | Unknown | The dynamic behaviour indicators returned by ThreatZone. |
tz-get-ioc-result
Retrieves Indicators of Compromise for a submission from ThreatZone.
Base Command
tz-get-ioc-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| type | The IOC type to filter by. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.IOCs.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.IOCs.Data | Unknown | The Indicators of Compromise returned by ThreatZone. |
tz-get-yara-result
Retrieves YARA rules matched during analysis. Use tz-download-yara-rule for the generated rule file.
Base Command
tz-get-yara-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| category | The matched YARA rule category to filter by. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.YaraMatches.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.YaraMatches.Data | Unknown | The matched YARA rules returned by ThreatZone. |
tz-get-artifact-result
Retrieves analysis artifacts for a submission from ThreatZone.
Base Command
tz-get-artifact-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Artifacts.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Artifacts.Data | Unknown | The analysis artifacts returned by ThreatZone. |
tz-get-config-result
Retrieves configuration extractor results for a submission from ThreatZone.
Base Command
tz-get-config-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Config.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Config.Data | Unknown | The configuration extractor results returned by ThreatZone. |
tz-get-sanitized
Downloads a sanitized file from the ThreatZone API and uploads it to the War Room.
Base Command
tz-get-sanitized
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.Extension | String | The extension of the file sanitized by CDR. |
| InfoFile.Name | String | The name of the file sanitized by CDR. |
| InfoFile.Size | Number | The size of the file sanitized by CDR. |
| InfoFile.EntryID | String | The entry ID of the file sanitized by CDR. |
| InfoFile.Info | String | The info of the file sanitized by CDR. |
| InfoFile.MD5 | String | The MD5 hash of the file sanitized by CDR. |
| InfoFile.SHA1 | String | The SHA1 hash of the file sanitized by CDR. |
| InfoFile.SHA256 | String | The SHA256 hash of the file sanitized by CDR. |
| InfoFile.SHA512 | String | The SHA512 hash of the file sanitized by CDR. |
| InfoFile.SSDeep | String | The SSDeep hash of the file sanitized by CDR. |
tz-download-html-report
Downloads the HTML report for a submission and uploads it to the War Room.
Base Command
tz-download-html-report
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.Extension | String | The extension of the HTML report. |
| InfoFile.Name | String | The name of the downloaded HTML report. |
| InfoFile.Size | Number | The size of the downloaded HTML report. |
| InfoFile.EntryID | String | The entry ID of the downloaded HTML report. |
| InfoFile.Info | String | The info for the downloaded HTML report. |
| InfoFile.MD5 | String | The MD5 hash of the downloaded HTML report. |
| InfoFile.SHA1 | String | The SHA1 hash of the downloaded HTML report. |
| InfoFile.SHA256 | String | The SHA256 hash of the downloaded HTML report. |
| InfoFile.SHA512 | String | The SHA512 hash of the downloaded HTML report. |
| InfoFile.SSDeep | String | The SSDeep hash of the downloaded HTML report. |
tz-get-metafields
Retrieves available ThreatZone metafields, optionally filtered by scan type.
Base Command
tz-get-metafields
Input
| Argument Name | Description | Required |
|---|---|---|
| scan_type | The scan type to filter metafields. Possible values are: sandbox, static, cdr, url, open_in_browser. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Configuration.Metafields.ScanType | String | The scan type used to filter the metafields. |
| ThreatZone.Configuration.Metafields.Data | Unknown | The metafield definitions returned by ThreatZone. |
tz-get-environments
Retrieves available ThreatZone sandbox environments.
Base Command
tz-get-environments
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Configuration.Environments.Data | Unknown | The sandbox environment definitions returned by ThreatZone. |
tz-list-network-configs
Lists network configurations available to the current ThreatZone workspace.
Base Command
tz-list-network-configs
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Configuration.NetworkConfigurations.Data | Unknown | The network configuration summaries returned by ThreatZone. |
tz-open-in-browser
Creates a ThreatZone open-in-browser submission.
Base Command
tz-open-in-browser
Input
| Argument Name | Description | Required |
|---|---|---|
| url | The URL to open in an isolated browser environment. | Required |
| environment | The sandbox environment key for browser execution. | Optional |
| auto | Whether to let ThreatZone select the browser environment. Possible values are: true, false. Default is false. | Optional |
| metafields | The open-in-browser metafields as a JSON object. | Optional |
| private | Whether to mark the submission as workspace-private. Possible values are: true, false. Default is true. | Optional |
| configurations | The advanced execution configuration as a JSON object. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.OpenInBrowser.UUID | String | The UUID of the open-in-browser submission. |
| ThreatZone.Submission.OpenInBrowser.URL | String | The URL submitted to ThreatZone. |
tz-list-submissions
Lists ThreatZone submissions with optional filters.
Base Command
tz-list-submissions
Input
| Argument Name | Description | Required |
|---|---|---|
| page | The page number, starting at 1. Default is 1. | Optional |
| limit | The number of submissions per page (1-100). Default is 20. | Optional |
| level | The comma-separated threat levels. | Optional |
| type | The submission type. Possible values are: file, url. | Optional |
| sha256 | The SHA256 hash filter. | Optional |
| filename | The partial filename filter. | Optional |
| start_date | The ISO 8601 date to include submissions created on or after (for example, 2020-01-01T00:11:22Z). | Optional |
| end_date | The ISO 8601 date to include submissions created on or before (for example, 2020-01-01T00:11:22Z). | Optional |
| private | Whether to filter by privacy status. Possible values are: true, false. | Optional |
| tags | The comma-separated tag filters. | Optional |
| sort | The field used to sort results, such as createdAt. | Optional |
| order | The sort order. Possible values are: asc, desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.List | Unknown | The paginated ThreatZone submission results. |
tz-search-submissions-by-sha256
Searches ThreatZone submissions by SHA256 hash.
Base Command
tz-search-submissions-by-sha256
Input
| Argument Name | Description | Required |
|---|---|---|
| sha256 | The SHA256 hash to search for. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.SubmissionSearch.Data | Unknown | The matching submissions. |
tz-get-overview-summary
Retrieves aggregate analysis counts for a submission.
Base Command
tz-get-overview-summary
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.OverviewSummary.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.OverviewSummary.Data | Unknown | The aggregate analysis summary. |
tz-get-eml-analysis
Retrieves parsed EML analysis results.
Base Command
tz-get-eml-analysis
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.EMLAnalysis.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.EMLAnalysis.Data | Unknown | The parsed EML analysis data. |
tz-get-mitre-techniques
Retrieves MITRE ATT&CK techniques matched during analysis.
Base Command
tz-get-mitre-techniques
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.MITRE.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.MITRE.Data | Unknown | The matched MITRE ATT&CK techniques. |
tz-get-static-scan-result
Retrieves the static scan result for a submission.
Base Command
tz-get-static-scan-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.StaticScan.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.StaticScan.Data | Unknown | The static scan result. |
tz-get-cdr-result
Retrieves the CDR transformation result for a submission.
Base Command
tz-get-cdr-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.CDRResult.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.CDRResult.Data | Unknown | The CDR transformation result. |
tz-get-signature-check-result
Retrieves authenticode and signature-check results.
Base Command
tz-get-signature-check-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.SignatureCheck.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.SignatureCheck.Data | Unknown | The signature-check result. |
tz-get-processes
Retrieves processes captured during dynamic analysis.
Base Command
tz-get-processes
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Processes.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Processes.Data | Unknown | The captured processes. |
tz-get-process-tree
Retrieves the process tree captured during dynamic analysis.
Base Command
tz-get-process-tree
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.ProcessTree.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.ProcessTree.Data | Unknown | The captured process tree. |
tz-get-url-analysis-result
Retrieves the full URL analysis report.
Base Command
tz-get-url-analysis-result
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the URL or open-in-browser submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.URLAnalysis.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.URLAnalysis.Data | Unknown | The URL analysis report. |
tz-get-behaviours
Retrieves one bounded page of behaviour telemetry.
Base Command
tz-get-behaviours
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| type | The behaviour event type filter. | Optional |
| pid | The process ID filter. | Optional |
| operation | The operation-name filter. | Optional |
| process_name | The exact process-name filter. | Optional |
| page | The page number, starting at 1. Default is 1. | Optional |
| limit | The number of events to return (1-500). Default is 100. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Behaviours.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Behaviours.Data | Unknown | The behaviour events and pagination metadata. |
tz-get-syscalls
Retrieves one bounded page of syscall telemetry.
Base Command
tz-get-syscalls
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| page | The page number, starting at 1. Default is 1. | Optional |
| limit | The number of syscall lines to return (1-2000). Default is 500. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.Syscalls.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.Syscalls.Data | Unknown | The syscall lines and pagination metadata. |
tz-get-network-summary
Retrieves aggregate network activity counts.
Base Command
tz-get-network-summary
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.NetworkSummary.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.NetworkSummary.Data | Unknown | The network activity summary. |
tz-get-dns-queries
Retrieves a bounded window of DNS queries.
Base Command
tz-get-dns-queries
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| limit | The maximum items to return (0-1000). | Optional |
| skip | The number of items to skip (0-1000). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.DNSQueries.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.DNSQueries.Data | Unknown | The captured DNS queries. |
tz-get-http-requests
Retrieves a bounded window of HTTP request hosts.
Base Command
tz-get-http-requests
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| limit | The maximum items to return (0-1000). | Optional |
| skip | The number of items to skip (0-1000). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.HTTPRequests.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.HTTPRequests.Data | Unknown | The captured HTTP request hosts. |
tz-get-tcp-connections
Retrieves a bounded window of TCP connections.
Base Command
tz-get-tcp-connections
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| limit | The maximum items to return (0-1000). | Optional |
| skip | The number of items to skip (0-1000). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.TCPConnections.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.TCPConnections.Data | Unknown | The captured TCP connections. |
tz-get-udp-connections
Retrieves a bounded window of UDP connections.
Base Command
tz-get-udp-connections
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| limit | The maximum items to return (0-1000). | Optional |
| skip | The number of items to skip (0-1000). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.UDPConnections.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.UDPConnections.Data | Unknown | The captured UDP connections. |
tz-get-network-threats
Retrieves a bounded window of Suricata network threats.
Base Command
tz-get-network-threats
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| limit | The maximum items to return (0-1000). | Optional |
| skip | The number of items to skip (0-1000). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.NetworkThreats.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.NetworkThreats.Data | Unknown | The detected network threats. |
tz-download-static-scan-strings
Downloads the static scan strings JSON to the War Room.
Base Command
tz-download-static-scan-strings
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-download-sample
Downloads the original submitted sample to the War Room.
Base Command
tz-download-sample
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-download-artifact
Downloads an extracted artifact to the War Room.
Base Command
tz-download-artifact
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| artifact_id | The artifact ID returned by tz-get-artifact-result. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-download-pcap
Downloads the network capture to the War Room.
Base Command
tz-download-pcap
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-download-yara-rule
Polls for and downloads the generated YARA rule file to the War Room.
Base Command
tz-download-yara-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| timeout | The maximum seconds to poll for generated YARA rule readiness (1-3600). Default is 120. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-download-url-screenshot
Downloads the URL analysis screenshot to the War Room.
Base Command
tz-download-url-screenshot
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the URL analysis submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-list-media-files
Lists screenshots and videos available for a submission.
Base Command
tz-list-media-files
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Submission.MediaFiles.UUID | String | The UUID of the submission. |
| ThreatZone.Submission.MediaFiles.Data | Unknown | The available media file metadata. |
tz-download-media-file
Downloads a submission media file to the War Room.
Base Command
tz-download-media-file
Input
| Argument Name | Description | Required |
|---|---|---|
| uuid | The UUID of the submission. | Required |
| file_id | The media file ID returned by tz-list-media-files. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | String | The entry ID of the downloaded file. |
| InfoFile.Name | String | The name of the downloaded file. |
tz-check-limits
Check the plan limits from ThreatZone API.
Base Command
tz-check-limits
Input
| Argument Name | Description | Required |
|---|---|---|
| detailed | Whether to include plan file limits, enabled modules, and account metadata. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThreatZone.Limits.E_Mail | String | The owner e-mail of current plan. |
| ThreatZone.Limits.API_Limit | String | The remaining/total API request limits of the current plan. |
| ThreatZone.Limits.Concurrent_Limit | String | The remaining/total concurrent analysis limits of the current plan. |
| ThreatZone.Limits.Daily_Submission_Limit | String | The remaining/total daily submission limits of the current plan. |
| ThreatZone.Plan.File_Size_Limit_MiB | Number | The maximum upload size for the workspace plan (MiB). |
| ThreatZone.Plan.Allowed_Extensions | List | The list of permitted file extensions for uploads. |
| ThreatZone.Plan.Modules | List | The enabled ThreatZone modules for the workspace. |
| ThreatZone.Metadata.Full_Name | String | The full name of the authenticated user. |
| ThreatZone.Metadata.Workspace | String | The workspace identifier or name associated with the account. |
| ThreatZone.Metadata.Plan_Name | String | The name of the active ThreatZone plan. |
| ThreatZone.Metadata.Plan_Status | String | The status of the active ThreatZone plan. |
Configuration parameters
url— Server URL (e.g. https://app.threat.zone) (required)apikey— (required)integrationReliability— Source Reliabilityinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (44)
-
tz-cdr-upload-sampleSubmits a sample to ThreatZone for CDR.
-
tz-check-limitsCheck the plan limits from ThreatZone API.
-
tz-download-artifactDownloads an extracted artifact to the War Room.
-
tz-download-html-reportDownloads the HTML report for a submission and uploads it to the War Room.
-
tz-download-media-fileDownloads a submission media file to the War Room.
-
tz-download-pcapDownloads the network capture to the War Room.
-
tz-download-sampleDownloads the original submitted sample to the War Room.
-
tz-download-static-scan-stringsDownloads the static scan strings JSON to the War Room.
-
tz-download-url-screenshotDownloads the URL analysis screenshot to the War Room.
-
tz-download-yara-rulePolls for and downloads the generated YARA rule file to the War Room.
-
tz-get-artifact-resultRetrieves analysis artifacts for a submission from ThreatZone.
-
tz-get-behavioursRetrieves one bounded page of behaviour telemetry.
-
tz-get-cdr-resultRetrieves the CDR transformation result for a submission.
-
tz-get-config-resultRetrieves configuration extractor results for a submission from ThreatZone.
-
tz-get-dns-queriesRetrieves a bounded window of DNS queries.
-
tz-get-eml-analysisRetrieves parsed EML analysis results.
-
tz-get-environmentsRetrieves available ThreatZone sandbox environments.
-
tz-get-http-requestsRetrieves a bounded window of HTTP request hosts.
-
tz-get-indicator-resultRetrieves dynamic behaviour indicators for a submission from ThreatZone.
-
tz-get-ioc-resultRetrieves Indicators of Compromise for a submission from ThreatZone.
-
tz-get-metafieldsRetrieves available ThreatZone metafields, optionally filtered by scan type.
-
tz-get-mitre-techniquesRetrieves MITRE ATT&CK techniques matched during analysis.
-
tz-get-network-summaryRetrieves aggregate network activity counts.
-
tz-get-network-threatsRetrieves a bounded window of Suricata network threats.
-
tz-get-overview-summaryRetrieves aggregate analysis counts for a submission.
-
tz-get-process-treeRetrieves the process tree captured during dynamic analysis.
-
tz-get-processesRetrieves processes captured during dynamic analysis.
-
tz-get-resultRetrieve the analysis result from ThreatZone.
-
tz-get-sanitizedDownloads a sanitized file from the ThreatZone API and uploads it to the War Room.
-
tz-get-signature-check-resultRetrieves authenticode and signature-check results.
-
tz-get-static-scan-resultRetrieves the static scan result for a submission.
-
tz-get-syscallsRetrieves one bounded page of syscall telemetry.
-
tz-get-tcp-connectionsRetrieves a bounded window of TCP connections.
-
tz-get-udp-connectionsRetrieves a bounded window of UDP connections.
-
tz-get-url-analysis-resultRetrieves the full URL analysis report.
-
tz-get-yara-resultRetrieves YARA rules matched during analysis. Use tz-download-yara-rule for the generated rule file.
-
tz-list-media-filesLists screenshots and videos available for a submission.
-
tz-list-network-configsLists network configurations available to the current ThreatZone workspace.
-
tz-list-submissionsLists ThreatZone submissions with optional filters.
-
tz-open-in-browserCreates a ThreatZone open-in-browser submission.
-
tz-sandbox-upload-sampleSubmits a sample to ThreatZone for sandbox analysis.
-
tz-search-submissions-by-sha256Searches ThreatZone submissions by SHA256 hash.
-
tz-static-upload-sampleSubmits a sample to ThreatZone for static analysis.
-
tz-url-analysisSubmits a URL to ThreatZone for analysis.
"""ThreatZone integration for Cortex XSOAR / XSIAM. API interaction is delegated to the official `threatzone` Python SDK. """ import json import re import time import traceback from collections.abc import Callable, Iterator from contextlib import contextmanager from pathlib import Path from typing import Any, BinaryIO, cast import demistomock as demisto # noqa: F401 import httpx from CommonServerPython import * # noqa: F401,F403 from ContentClientApiModule import * # noqa: F401,F403 from threatzone import ( AnalysisTimeoutError, APIError, AuthenticationError, BadRequestError, DownloadResponse, NotFoundError, PaymentRequiredError, PermissionDeniedError, RateLimitError, ReportUnavailableError, ThreatZoneError, YaraRulePendingError, ) from threatzone import ThreatZone as ThreatZoneSDK from threatzone.types.config import MetafieldOption INTEGRATION_NAME = "ThreatZone" PUBLIC_API_SUFFIX = "/public-api" SDK_REQUEST_TIMEOUT_SECONDS = 60.0 REPORT_FINDINGS_PAGE_SIZE = 100 MAX_REPORT_FINDINGS_PAGES = 100 YARA_POLL_INTERVAL_SECONDS = 5.0 MIN_YARA_POLL_INTERVAL_SECONDS = 1.0 MAX_YARA_POLL_ATTEMPTS = 3600 # Old integration used integer codes for level/status. Preserved for backward # compatibility with playbooks that branch on these numbers. LEVEL_LABEL_TO_INT = {"unknown": 0, "benign": 1, "suspicious": 2, "malicious": 3} LEVEL_INT_TO_LABEL = { 0: "Not Measured", 1: "Informative", 2: "Suspicious", 3: "Malicious", } STATUS_LABEL_TO_INT = { "not_started": 1, "accepted": 2, "in_progress": 3, "clean_up": 4, "completed": 5, "error": 0, } STATUS_INT_TO_READABLE = { 0: "Declined", 1: "File received", 2: "Submission is accepted", 3: "Submission is running", 4: "Submission VM is ready", 5: "Submission is finished", } REPORT_TYPE_LABEL = { "dynamic": "dynamic", "static": "static", "cdr": "cdr", "url_analysis": "urlAnalysis", "open_in_browser": "openInBrowser", } def normalize_sdk_base_url(base_url: str) -> str: normalized_base_url = base_url.strip().rstrip("/") if not normalized_base_url: raise DemistoException("Server URL is required.") # noqa: F405 if normalized_base_url.endswith(PUBLIC_API_SUFFIX): return normalized_base_url return normalized_base_url + PUBLIC_API_SUFFIX def extract_api_key(api_key_param: Any) -> str: """Return an API key from either a type-9 credential or a legacy string value.""" if isinstance(api_key_param, dict): return str(api_key_param.get("password") or "") return str(api_key_param or "") class Client(ContentClient): # noqa: F405 - ContentClient from ContentClientApiModule """Thin wrapper over the ThreatZone SDK.""" def __init__(self, base_url: str, api_key: str, verify: bool, proxy: bool, reliability: str | None = None) -> None: self.reliability = reliability sdk_base_url = normalize_sdk_base_url(base_url) super().__init__( # noqa: F405 base_url=sdk_base_url, verify=verify, proxy=proxy, headers={"Authorization": f"Bearer {api_key}"}, timeout=SDK_REQUEST_TIMEOUT_SECONDS, client_name=INTEGRATION_NAME, is_multithreaded=False, ) http_client = httpx.Client( verify=verify, trust_env=bool(proxy), timeout=httpx.Timeout(SDK_REQUEST_TIMEOUT_SECONDS), follow_redirects=True, ) self.sdk = ThreatZoneSDK( api_key=api_key, base_url=sdk_base_url, verify_ssl=verify, http_client=http_client, ) self._http_client = http_client def close(self) -> None: if self._closed: return try: self.sdk.close() finally: try: self._http_client.close() finally: super().close() class _OriginalNameFile: """Proxy a file handle while exposing its original War Room filename.""" def __init__(self, file_handle: BinaryIO, original_name: str) -> None: self._file_handle = file_handle self.name = original_name def __getattr__(self, attribute: str) -> Any: return getattr(self._file_handle, attribute) @contextmanager def open_file_for_upload(file_path: Path, original_name: str) -> Iterator[BinaryIO]: """Open a local War Room file for streaming upload under its original name.""" with file_path.open("rb") as file_handle: yield cast(BinaryIO, _OriginalNameFile(file_handle, original_name)) def translate_score(score: int | None) -> int: if score is None or isinstance(score, bool): return Common.DBotScore.NONE # noqa: F405 try: score_int = int(score) except (TypeError, ValueError): return Common.DBotScore.NONE # noqa: F405 if score_int <= 0: return Common.DBotScore.NONE # noqa: F405 if score_int == 1: return Common.DBotScore.GOOD # noqa: F405 if score_int == 2: return Common.DBotScore.SUSPICIOUS # noqa: F405 return Common.DBotScore.BAD # noqa: F405 def get_reputation_reliability(reliability: str | None) -> str | None: mapping = { "A+ - 3rd party enrichment": DBotScoreReliability.A_PLUS, # noqa: F405 "A - Completely reliable": DBotScoreReliability.A, # noqa: F405 "B - Usually reliable": DBotScoreReliability.B, # noqa: F405 "C - Fairly reliable": DBotScoreReliability.C, # noqa: F405 "D - Not usually reliable": DBotScoreReliability.D, # noqa: F405 "E - Unreliable": DBotScoreReliability.E, # noqa: F405 "F - Reliability cannot be judged": DBotScoreReliability.F, # noqa: F405 } return mapping.get(reliability or "") def build_dbot_score( indicator: str, level_int: int | None, indicator_type: str, reliability: str | None, ) -> Common.DBotScore: # noqa: F405 type_map = { "file": DBotScoreType.FILE, # noqa: F405 "ip": DBotScoreType.IP, # noqa: F405 "domain": DBotScoreType.DOMAIN, # noqa: F405 "url": DBotScoreType.URL, # noqa: F405 "email": DBotScoreType.EMAIL, # noqa: F405 } return Common.DBotScore( # noqa: F405 indicator=indicator, indicator_type=type_map.get(indicator_type, DBotScoreType.CUSTOM), # noqa: F405 integration_name=INTEGRATION_NAME, score=translate_score(level_int), reliability=get_reputation_reliability(reliability), ) def parse_int_argument(arg_value: str | None, argument_name: str) -> int | None: if arg_value is None or arg_value == "": return None try: return int(arg_value) except (TypeError, ValueError) as exc: raise DemistoException(f"{argument_name} argument must be an integer.") from exc # noqa: F405 def parse_bounded_int_argument( arg_value: str | None, argument_name: str, *, minimum: int, maximum: int, default: int | None = None, ) -> int | None: """Parse an optional integer and enforce the API-supported range.""" value = default if arg_value in (None, "") else parse_int_argument(arg_value, argument_name) if value is None: return None if not minimum <= value <= maximum: raise DemistoException( # noqa: F405 f"{argument_name} argument must be between {minimum} and {maximum}." ) return value def parse_json_object_argument(arg_value: str | None, argument_name: str) -> dict[str, Any] | None: """Parse a JSON-object command argument.""" if not arg_value: return None try: parsed = json.loads(arg_value) except (TypeError, ValueError) as exc: raise DemistoException(f"{argument_name} argument must be a valid JSON object.") from exc # noqa: F405 if not isinstance(parsed, dict): raise DemistoException(f"{argument_name} argument must be a JSON object.") # noqa: F405 return parsed def parse_csv_list_argument(arg_value: str | None) -> list[str] | None: """Parse a comma-separated command argument, omitting empty values.""" # argToList does the split/strip; filter empties + `or None` keep the historical contract # the SDK filter kwargs rely on (None = omit filter, not an empty/blank filter value). return [value for value in argToList(arg_value) if value] or None # noqa: F405 def resolve_private_flag(private_arg: str | None, default: bool = True) -> bool: if private_arg is None: return default return argToBoolean(private_arg) # noqa: F405 def parse_modules_argument(modules_arg: str | None) -> list[str] | None: if not modules_arg: return None try: parsed = json.loads(modules_arg) except ValueError: parsed = None if isinstance(parsed, list): modules = [str(m).strip() for m in parsed if str(m).strip()] elif isinstance(parsed, str): modules = [parsed.strip()] elif parsed is None: modules = [m.strip() for m in modules_arg.split(",") if m.strip()] else: raise DemistoException("modules argument must be a JSON array or comma-separated string.") # noqa: F405 return modules or None def parse_analyze_config_argument(analyze_arg: str | None) -> list[dict[str, Any]]: if not analyze_arg: return [] try: parsed = json.loads(analyze_arg) except ValueError as exc: raise DemistoException("Invalid JSON provided for analyze_config argument.") from exc # noqa: F405 if isinstance(parsed, dict): parsed = [parsed] if not isinstance(parsed, list): raise DemistoException("analyze_config argument must be a JSON object or array of objects.") # noqa: F405 validated: list[dict[str, Any]] = [] for entry in parsed: if not isinstance(entry, dict): raise DemistoException("analyze_config entries must be JSON objects.") # noqa: F405 metafield_id = entry.get("metafieldId") if not metafield_id: raise DemistoException("analyze_config entries must include 'metafieldId'.") # noqa: F405 if "value" not in entry: raise DemistoException("analyze_config entries must include 'value'.") # noqa: F405 validated.append({"metafieldId": metafield_id, "value": entry["value"]}) return validated def sandbox_api_defaults(client: Client) -> dict[str, Any]: """Return active, accessible sandbox defaults advertised by ThreatZone.""" try: definitions = cast(list[MetafieldOption], client.sdk.get_metafields("sandbox")) except ThreatZoneError as exc: demisto.debug(f"Could not retrieve ThreatZone sandbox metafield defaults: {exc}") return {} defaults: dict[str, Any] = {} for definition in definitions: if not definition.active or not definition.accessible: continue if definition.options: default_option = next( (option for option in definition.options if option.value == definition.default), None, ) if default_option is not None and not default_option.accessible: continue defaults[definition.key] = definition.default return defaults def metafields_from_legacy_args(args: dict[str, Any], api_defaults: dict[str, Any] | None = None) -> dict[str, Any]: """Map historical sandbox arguments to SDK `metafields` dict. The legacy v2 integration accepted dedicated YAML arguments (timeout, work_path, mouse_simulation, etc.) and serialized them into an `analyzeConfig` envelope. The SDK now accepts a flat dict. """ fields = {key: value for key, value in (api_defaults or {}).items() if key not in {"private", "raw_logs"}} if (timeout := parse_int_argument(args.get("timeout"), "timeout")) is not None: fields["timeout"] = timeout if work_path := args.get("work_path"): fields["work_path"] = work_path # Threat.Zone v3.2 rejects the legacy `raw_logs` metafield for dynamic # reports even when its value is false. Keep accepting the XSOAR argument # for command compatibility, but do not forward the unsupported key. for arg_name in ( "mouse_simulation", "https_inspection", "internet_connection", "snapshot", ): if args.get(arg_name) is not None: fields[arg_name] = argToBoolean(args[arg_name]) # noqa: F405 if args.get("extension_check") is not None: fields["dynamic_mimetype_check"] = argToBoolean(args["extension_check"]) # noqa: F405 if parse_modules_argument(args.get("modules")) is not None: demisto.debug( "The legacy modules argument is ignored because ThreatZone v3.2 " "does not expose a supported submission field for module selection." ) user_overrides = parse_analyze_config_argument(args.get("analyze_config")) for entry in user_overrides: fields[str(entry["metafieldId"])] = entry["value"] return fields def submission_to_dict(submission: Any) -> dict[str, Any]: """Render any SDK pydantic model into a JSON-compatible dict (camelCase keys).""" if submission is None: return {} if hasattr(submission, "model_dump"): return submission.model_dump(by_alias=True, exclude_none=True, mode="json") if isinstance(submission, dict): return submission if isinstance(submission, list): return {"items": [submission_to_dict(item) for item in submission]} return {"value": submission} def serialize_sdk_data(value: Any) -> Any: """Render SDK models and collections into JSON-compatible values.""" if hasattr(value, "model_dump"): return value.model_dump(by_alias=True, exclude_none=True, mode="json") if isinstance(value, dict): return {key: serialize_sdk_data(item) for key, item in value.items()} if isinstance(value, list | tuple): return [serialize_sdk_data(item) for item in value] return value def models_to_dicts(items: Any) -> list[dict[str, Any]]: if items is None: return [] if hasattr(items, "items") and not isinstance(items, dict): # `IndicatorsResponse`, `IoCsResponse`, etc. return [submission_to_dict(item) for item in items.items] if isinstance(items, list): return [submission_to_dict(item) for item in items] return [submission_to_dict(items)] def get_all_report_items(fetch_page: Callable[..., Any], uuid: str, **filters: Any) -> list[Any]: """Fetch every page from an SDK report endpoint that exposes an item total.""" items: list[Any] = [] for page in range(1, MAX_REPORT_FINDINGS_PAGES + 1): response = fetch_page(uuid, page=page, limit=REPORT_FINDINGS_PAGE_SIZE, **filters) page_items = list(response.items) items.extend(page_items) if not page_items or len(items) >= response.total: return items raise DemistoException( # noqa: F405 f"Report pagination exceeded the supported maximum of {MAX_REPORT_FINDINGS_PAGES} pages." ) def parse_file_size_mib(file_size: Any) -> int | float | None: if isinstance(file_size, bool) or file_size is None: return None if isinstance(file_size, int | float): numeric_size = float(file_size) else: match = re.search(r"\d+(?:\.\d+)?", str(file_size)) if not match: return None numeric_size = float(match.group(0)) return int(numeric_size) if numeric_size.is_integer() else numeric_size def submission_level_int(level: str | None) -> int | None: if level is None: return None return LEVEL_LABEL_TO_INT.get(level) def report_status_int(status: str | None) -> int | None: if status is None: return None return STATUS_LABEL_TO_INT.get(status) def legacy_report_to_dict(report: Any) -> dict[str, Any]: report_dict = submission_to_dict(report) status_int = report_status_int(getattr(report, "status", None)) if status_int is not None: report_dict["status"] = status_int return report_dict def select_primary_report(submission: Any) -> Any: """Pick the most relevant report for legacy summary output. Preference order matches the legacy code: dynamic, static, cdr, url_analysis, open_in_browser. """ if not submission or not getattr(submission, "reports", None): return None by_type = {report.type: report for report in submission.reports} for preferred in ("dynamic", "static", "cdr", "url_analysis", "open_in_browser"): if preferred in by_type: return by_type[preferred] return submission.reports[0] def test_module(client: Client) -> str: try: client.sdk.get_user_info() except AuthenticationError: return "Authorization Error: make sure API Key is correctly set" except PermissionDeniedError as exc: return f"Authorization Error: {exc}" return "ok" def threatzone_check_limits(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 user_info = client.sdk.get_user_info() details = user_info.user_info limits_count = details.limits_count plan = user_info.plan submission_limits = plan.submission_limits limits = { "E_Mail": details.email, "Daily_Submission_Limit": f"{limits_count.daily_submission_count}/{submission_limits.daily_limit}", "Concurrent_Limit": f"{limits_count.concurrent_submission_count}/{submission_limits.concurrent_limit}", "API_Limit": f"{limits_count.api_request_count}/{submission_limits.api_limit}", } results: list[CommandResults] = [ # noqa: F405 CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Limits", outputs_key_field="E_Mail", outputs=limits, readable_output=tableToMarkdown("LIMITS", limits, removeNull=True), # noqa: F405 ) ] if argToBoolean(args.get("detailed", "false")): # noqa: F405 plan_details = { "File_Size_Limit_MiB": parse_file_size_mib(plan.file_limits.file_size), "Allowed_Extensions": plan.file_limits.extensions, "Modules": [module.module_name for module in user_info.modules], } results.append( CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Plan", outputs=plan_details, readable_output=tableToMarkdown("PLAN DETAILS", plan_details, removeNull=True), # noqa: F405 ) ) metadata = { "Full_Name": details.full_name, "Workspace": details.workspace.name, "Plan_Name": plan.plan_name, } filtered_metadata = {k: v for k, v in metadata.items() if v not in (None, "", [])} if filtered_metadata: results.append( CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Metadata", outputs=filtered_metadata, readable_output=tableToMarkdown("ACCOUNT METADATA", filtered_metadata, removeNull=True), # noqa: F405 ) ) return results def _verify_plan_capacity(client: Client, *, requires_concurrent: bool) -> None: """Raise DemistoException when the plan has no remaining quota for an upload.""" info = client.sdk.get_user_info() counts = info.user_info.limits_count limits = info.plan.submission_limits if counts.api_request_count >= limits.api_limit > 0: raise DemistoException( # noqa: F405 f"API request limit ({limits.api_limit}) exceeded. Upgrade your plan or contact us." ) if counts.daily_submission_count >= limits.daily_limit > 0: raise DemistoException( # noqa: F405 f"Daily submission limit ({limits.daily_limit}) exceeded. Upgrade your plan or contact us." ) if requires_concurrent and counts.concurrent_submission_count >= limits.concurrent_limit > 0: raise DemistoException( # noqa: F405 f"Concurrent analysis limit ({limits.concurrent_limit}) exceeded. Wait for in-flight analyses to finish." ) def _file_from_entry(args: dict[str, Any]) -> tuple[Path, str]: entry_id = args.get("entry_id") if not entry_id: raise DemistoException("entry_id argument is required.") # noqa: F405 try: file_info = demisto.getFilePath(entry_id) except Exception as exc: raise DemistoException(f"Failed to retrieve file for entry ID {entry_id}.") from exc # noqa: F405 return Path(file_info["path"]), file_info["name"] def _build_limits_payload(client: Client) -> dict[str, Any]: """Build the current plan limits payload returned with upload results.""" info = client.sdk.get_user_info() counts = info.user_info.limits_count limits = info.plan.submission_limits return { "E_Mail": info.user_info.email, "Daily_Submission_Limit": f"{counts.daily_submission_count}/{limits.daily_limit}", "Concurrent_Limit": f"{counts.concurrent_submission_count}/{limits.concurrent_limit}", "API_Limit": f"{counts.api_request_count}/{limits.api_limit}", } def _submission_result(scan_prefix: str, payload: dict[str, Any], readable: str, limits: dict[str, Any]) -> list[CommandResults]: # noqa: F405 filtered = {k: v for k, v in payload.items() if v not in (None, "", [])} return [ CommandResults( # noqa: F405 outputs_prefix=f"ThreatZone.Submission.{scan_prefix}", outputs_key_field="UUID", outputs=filtered, readable_output=readable, ), CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Limits", outputs_key_field="E_Mail", outputs=limits, ), ] def threatzone_sandbox_upload_sample(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 _verify_plan_capacity(client, requires_concurrent=True) file_path, original_name = _file_from_entry(args) auto_select_environment = argToBoolean(args.get("auto", "false")) # noqa: F405 environment = None if auto_select_environment else args.get("environment") api_defaults = sandbox_api_defaults(client) private = resolve_private_flag(args.get("private"), bool(api_defaults.get("private", True))) entrypoint = args.get("entrypoint") password = args.get("password") metafields = metafields_from_legacy_args(args, api_defaults) configurations = parse_json_object_argument(args.get("configurations"), "configurations") with open_file_for_upload(file_path, original_name) as upload_file: submission = client.sdk.create_sandbox_submission( upload_file, environment=environment, auto_select_environment=auto_select_environment, metafields=metafields or None, private=private, entrypoint=entrypoint, password=password, configurations=configurations, ) message = submission.message submission_uuid = submission.uuid payload = { "Message": message, "UUID": submission_uuid, "FileName": original_name, } readable = tableToMarkdown("SAMPLE UPLOADED", payload) # noqa: F405 return _submission_result("Sandbox", payload, readable, _build_limits_payload(client)) def threatzone_static_or_cdr_upload(client: Client, args: dict[str, Any], scan_type: str) -> list[CommandResults]: # noqa: F405 if scan_type not in ("static", "cdr"): raise DemistoException(f"Unsupported scan_type '{scan_type}'.") # noqa: F405 _verify_plan_capacity(client, requires_concurrent=True) file_path, original_name = _file_from_entry(args) private = resolve_private_flag(args.get("private")) entrypoint = args.get("entrypoint") password = args.get("password") extension_check = args.get("extension_check") dynamic_mimetype_check = argToBoolean(extension_check) if extension_check is not None else None # noqa: F405 with open_file_for_upload(file_path, original_name) as upload_file: if scan_type == "static": submission = client.sdk.create_static_submission( upload_file, private=private, entrypoint=entrypoint, password=password, dynamic_mimetype_check=dynamic_mimetype_check, ) prefix = "Static" else: submission = client.sdk.create_cdr_submission( upload_file, private=private, entrypoint=entrypoint, password=password, dynamic_mimetype_check=dynamic_mimetype_check, ) prefix = "CDR" payload = { "Message": submission.message, "UUID": submission.uuid, "FileName": original_name, } readable = tableToMarkdown("SAMPLE UPLOADED", payload) # noqa: F405 return _submission_result(prefix, payload, readable, _build_limits_payload(client)) def threatzone_submit_url_analysis(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 _verify_plan_capacity(client, requires_concurrent=False) url_value = args.get("url") if not url_value: raise DemistoException("url argument is required.") # noqa: F405 private = resolve_private_flag(args.get("private")) safe_browsing = argToBoolean(args.get("safe_browsing", "false")) # noqa: F405 submission = client.sdk.create_url_submission(url_value, private=private, safe_browsing=safe_browsing) payload = { "Message": submission.message, "UUID": submission.uuid, "URL": url_value, } readable = tableToMarkdown("URL SUBMITTED", payload) # noqa: F405 return _submission_result("URL", payload, readable, _build_limits_payload(client)) def _build_indicator_object(submission: Any, level_int: int | None, reliability: str | None) -> Any: hashes = getattr(submission, "hashes", None) sha256 = getattr(hashes, "sha256", None) if hashes else None md5 = getattr(hashes, "md5", None) if hashes else None sha1 = getattr(hashes, "sha1", None) if hashes else None url_value = getattr(submission, "url", None) if sha256: dbot = build_dbot_score(sha256, level_int, "file", reliability) return Common.File(dbot, md5=md5, sha1=sha1, sha256=sha256) # noqa: F405 if url_value: dbot = build_dbot_score(url_value, level_int, "url", reliability) return Common.URL(url=url_value, dbot_score=dbot) # noqa: F405 return None def threatzone_get_result(client: Client, args: dict[str, Any]) -> list[CommandResults | dict]: # noqa: F405 uuid = args.get("uuid") if not uuid: raise DemistoException("uuid argument is required.") # noqa: F405 submission = client.sdk.get_submission(uuid) submission_dict = submission_to_dict(submission) primary_report = select_primary_report(submission) level_int = submission_level_int(submission.level) level_label = LEVEL_INT_TO_LABEL.get(level_int) if level_int is not None else "Unknown" status_label = primary_report.status if primary_report else None status_int = report_status_int(status_label) if status_int == 0: raise DemistoException( # noqa: F405 "Submission is declined by the scanner. The file may be corrupted or the analyzer encountered an unrecoverable error." ) status_readable = STATUS_INT_TO_READABLE.get(status_int, status_label or "Unknown") if status_int is not None else "Unknown" legacy_report = legacy_report_to_dict(primary_report) report_type_label = REPORT_TYPE_LABEL.get(primary_report.type, primary_report.type) if primary_report else None analysis_type_label = "URL Analysis" if report_type_label == "urlAnalysis" else (report_type_label or "") hashes = submission.hashes md5 = hashes.md5 if hashes else None sha1 = hashes.sha1 if hashes else None sha256 = hashes.sha256 if hashes else None analyzed_url = submission.url file_name = submission.filename or (submission.file.name if submission.file else None) private_flag = submission.private summary_info: dict[str, Any] = {"private": private_flag} if file_name: summary_info["file_name"] = file_name if analyzed_url: summary_info["url"] = analyzed_url summary_output: dict[str, Any] = { "TYPE": analysis_type_label, "STATUS": status_readable, "MD5": md5, "SHA1": sha1, "SHA256": sha256, "LEVEL": level_int, "LEVEL_LABEL": level_label, "INFO": summary_info, "UUID": submission.uuid, "REPORT": dict(legacy_report), } submission_dict["Summary"] = summary_output download_sanitized = argToBoolean(args.get("download_sanitized", "false")) # noqa: F405 include_details = argToBoolean(args.get("details", "false")) # noqa: F405 warnings: list[str] = [] if status_int is not None and status_int != 5: warnings.append(f"Submission status is '{status_readable}'; some sections may not yet be available.") indicator_obj = _build_indicator_object(submission, level_int, client.reliability) sanitized_file: dict | None = None if primary_report and primary_report.type == "cdr" and status_int == 5 and download_sanitized: try: sanitized_file = threatzone_get_sanitized_file(client, {"uuid": submission.uuid}) except (ThreatZoneError, DemistoException) as exc: # noqa: F405 warnings.append(f"sanitized-download: {exc}") readable_summary: dict[str, Any] = { "ANALYSIS TYPE": analysis_type_label, "STATUS": status_readable, "THREAT_LEVEL": level_label, "PRIVATE": private_flag, "UUID": submission.uuid, } if file_name: readable_summary["FILE_NAME"] = file_name if analyzed_url: readable_summary["URL"] = analyzed_url if md5: readable_summary["MD5"] = md5 if sha1: readable_summary["SHA1"] = sha1 if sha256: readable_summary["SHA256"] = sha256 readable_output = tableToMarkdown("Submission Result", readable_summary, removeNull=True) # noqa: F405 detail_extras: list[CommandResults] = [] # noqa: F405 ioc_items: list[Any] = [] ioc_fetch_attempted = False if include_details: try: indicators_data = models_to_dicts(get_all_report_items(client.sdk.get_indicators, submission.uuid)) detail_extras.append( _section_result( submission.uuid, "ThreatZone.Submission.Indicators", "Dynamic Indicators", indicators_data, ) ) except ReportUnavailableError as exc: warnings.append(f"indicators-unavailable: {exc}") ioc_fetch_attempted = True try: ioc_items = get_all_report_items(client.sdk.get_iocs, submission.uuid) iocs_data = models_to_dicts(ioc_items) detail_extras.append( _section_result( submission.uuid, "ThreatZone.Submission.IOCs", "Indicators of Compromise", iocs_data, ) ) except ReportUnavailableError as exc: warnings.append(f"iocs-unavailable: {exc}") try: yara_data = models_to_dicts(get_all_report_items(client.sdk.get_yara_rules, submission.uuid)) detail_extras.append( _section_result( submission.uuid, "ThreatZone.Submission.YaraMatches", "Matched YARA Rules", yara_data, ) ) except ReportUnavailableError as exc: warnings.append(f"yara-unavailable: {exc}") try: artifacts_data = models_to_dicts(client.sdk.get_artifacts(submission.uuid)) detail_extras.append( _section_result( submission.uuid, "ThreatZone.Submission.Artifacts", "Analysis Artifacts", artifacts_data, ) ) except ReportUnavailableError as exc: warnings.append(f"artifacts-unavailable: {exc}") try: configs_data = models_to_dicts(client.sdk.get_extracted_configs(submission.uuid)) detail_extras.append( _section_result( submission.uuid, "ThreatZone.Submission.Config", "Configuration Extractor Results", configs_data, ) ) except ReportUnavailableError as exc: warnings.append(f"config-unavailable: {exc}") if warnings: warning_lines = "\n".join(f"- {warning}" for warning in warnings) readable_output = f"{readable_output}\n\n### Additional Data Notes\n{warning_lines}" legacy_analysis: dict[str, Any] = { "TYPE": report_type_label, "STATUS": status_int if status_int is not None else -1, "MD5": md5, "SHA1": sha1, "SHA256": sha256, "LEVEL": level_int, "INFO": dict(summary_info), "UUID": submission.uuid, "REPORT": dict(legacy_report), "URL": analyzed_url, "SANITIZED": sanitized_file.get("EntryID") if isinstance(sanitized_file, dict) else None, } legacy_iocs: dict[str, list[Any]] = {"URL": [], "DOMAIN": [], "EMAIL": [], "IP": []} if not ioc_fetch_attempted: try: ioc_items = get_all_report_items(client.sdk.get_iocs, submission.uuid) except (ReportUnavailableError, APIError) as exc: demisto.debug(f"Could not retrieve ThreatZone IOCs for legacy output: {exc}") for item in ioc_items: bucket = item.type.upper() if bucket in legacy_iocs: legacy_iocs[bucket].append(item.value) command_results: list[CommandResults | dict] = [ # noqa: F405 CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Submission", outputs_key_field="uuid", outputs=submission_dict, raw_response=submission_dict, readable_output=readable_output, indicator=indicator_obj, ), CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Analysis", outputs_key_field="UUID", outputs=legacy_analysis, ), CommandResults( # noqa: F405 outputs_prefix="ThreatZone.IOC", outputs=legacy_iocs, ), ] command_results.extend(detail_extras) if sanitized_file: command_results.append(sanitized_file) return command_results def _section_result(uuid: str, prefix: str, title: str, data: list[dict[str, Any]]) -> CommandResults: # noqa: F405 readable = tableToMarkdown(title, data, removeNull=True) if data else f"{title}\nNo data returned." # noqa: F405 outputs: dict[str, Any] | None = {"UUID": uuid, "Data": data} if data else None return CommandResults( # noqa: F405 outputs_prefix=prefix, outputs_key_field="UUID" if outputs else None, outputs=outputs, readable_output=readable, ) def _require_uuid(args: dict[str, Any]) -> str: uuid = args.get("uuid") if not uuid: raise DemistoException("uuid argument is required.") # noqa: F405 return uuid def threatzone_get_indicator_result(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) filters = { "level": args.get("level"), "category": args.get("category"), "pid": parse_int_argument(args.get("pid"), "pid"), "attack_code": args.get("attack_code"), } data = models_to_dicts( get_all_report_items( client.sdk.get_indicators, uuid, **{key: value for key, value in filters.items() if value is not None}, ) ) return [_section_result(uuid, "ThreatZone.Submission.Indicators", "Dynamic Indicators", data)] def threatzone_get_ioc_result(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) filters = {"type": args.get("type")} if args.get("type") else {} data = models_to_dicts(get_all_report_items(client.sdk.get_iocs, uuid, **filters)) return [_section_result(uuid, "ThreatZone.Submission.IOCs", "Indicators of Compromise", data)] def threatzone_get_yara_result(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) filters = {"category": args.get("category")} if args.get("category") else {} data = models_to_dicts(get_all_report_items(client.sdk.get_yara_rules, uuid, **filters)) return [_section_result(uuid, "ThreatZone.Submission.YaraMatches", "Matched YARA Rules", data)] def threatzone_get_artifact_result(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) data = models_to_dicts(client.sdk.get_artifacts(uuid)) return [_section_result(uuid, "ThreatZone.Submission.Artifacts", "Analysis Artifacts", data)] def threatzone_get_config_result(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) data = models_to_dicts(client.sdk.get_extracted_configs(uuid)) return [ _section_result( uuid, "ThreatZone.Submission.Config", "Configuration Extractor Results", data, ) ] def _structured_result( prefix: str, title: str, data: Any, *, uuid: str | None = None, scan_type: str | None = None, ) -> CommandResults: # noqa: F405 """Build a predictable Data envelope for SDK-native command results.""" serialized_data = serialize_sdk_data(data) outputs: dict[str, Any] = {"Data": serialized_data} if uuid is not None: outputs["UUID"] = uuid if scan_type is not None: outputs["ScanType"] = scan_type readable = ( tableToMarkdown(title, serialized_data, removeNull=True) # noqa: F405 if serialized_data else f"{title}\nNo data returned." ) return CommandResults( # noqa: F405 outputs_prefix=prefix, outputs_key_field="UUID" if uuid is not None else None, outputs=outputs, readable_output=readable, ) def threatzone_get_metafields(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 scan_type = args.get("scan_type") allowed_scan_types = {"sandbox", "static", "cdr", "url", "open_in_browser"} if scan_type and scan_type not in allowed_scan_types: raise DemistoException( # noqa: F405 "scan_type must be one of sandbox, static, cdr, url, or open_in_browser." ) data = client.sdk.get_metafields(scan_type) if scan_type else client.sdk.get_metafields() return [ _structured_result( "ThreatZone.Configuration.Metafields", "ThreatZone Metafields", data, scan_type=scan_type, ) ] def threatzone_get_environments(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 return [ _structured_result( "ThreatZone.Configuration.Environments", "ThreatZone Environments", client.sdk.get_environments(), ) ] def threatzone_list_network_configs(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 return [ _structured_result( "ThreatZone.Configuration.NetworkConfigurations", "ThreatZone Network Configurations", client.sdk.list_network_configs(), ) ] def threatzone_open_in_browser(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 _verify_plan_capacity(client, requires_concurrent=True) url_value = args.get("url") if not url_value: raise DemistoException("url argument is required.") # noqa: F405 auto_select_environment = argToBoolean(args.get("auto", "false")) # noqa: F405 environment = None if auto_select_environment else args.get("environment") submission = client.sdk.create_open_in_browser_submission( url_value, environment=environment, auto_select_environment=auto_select_environment, metafields=parse_json_object_argument(args.get("metafields"), "metafields"), private=resolve_private_flag(args.get("private")), configurations=parse_json_object_argument(args.get("configurations"), "configurations"), ) payload = {"Message": submission.message, "UUID": submission.uuid, "URL": url_value} readable = tableToMarkdown("OPEN IN BROWSER SUBMITTED", payload) # noqa: F405 return _submission_result("OpenInBrowser", payload, readable, _build_limits_payload(client)) def threatzone_list_submissions(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 page = cast( int, parse_bounded_int_argument(args.get("page"), "page", minimum=1, maximum=2_147_483_647, default=1), ) limit = cast( int, parse_bounded_int_argument(args.get("limit"), "limit", minimum=1, maximum=100, default=20), ) private = argToBoolean(args["private"]) if args.get("private") is not None else None # noqa: F405 response = client.sdk.list_submissions( page=page, limit=limit, level=parse_csv_list_argument(args.get("level")), type=args.get("type"), sha256=args.get("sha256"), filename=args.get("filename"), start_date=args.get("start_date"), end_date=args.get("end_date"), private=private, tags=parse_csv_list_argument(args.get("tags")), sort=args.get("sort"), order=args.get("order"), ) serialized = serialize_sdk_data(response) return [ CommandResults( # noqa: F405 outputs_prefix="ThreatZone.Submission.List", outputs=serialized, readable_output=tableToMarkdown("ThreatZone Submissions", serialized.get("items", []), removeNull=True), # noqa: F405 ) ] def threatzone_search_submissions(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 sha256 = args.get("sha256") if not sha256: raise DemistoException("sha256 argument is required.") # noqa: F405 return [ _structured_result( "ThreatZone.SubmissionSearch", "ThreatZone Submission Search", client.sdk.search_by_sha256(sha256), ) ] def threatzone_get_uuid_section( client: Client, args: dict[str, Any], sdk_method: str, section: str, title: str, ) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) data = getattr(client.sdk, sdk_method)(uuid) return [_structured_result(f"ThreatZone.Submission.{section}", title, data, uuid=uuid)] def threatzone_get_behaviours(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) page = cast( int, parse_bounded_int_argument(args.get("page"), "page", minimum=1, maximum=2_147_483_647, default=1), ) limit = cast( int, parse_bounded_int_argument(args.get("limit"), "limit", minimum=1, maximum=500, default=100), ) data = client.sdk.get_behaviours( uuid, type=args.get("type"), pid=parse_int_argument(args.get("pid"), "pid"), operation=args.get("operation"), process_name=args.get("process_name"), page=page, limit=limit, ) return [_structured_result("ThreatZone.Submission.Behaviours", "ThreatZone Behaviours", data, uuid=uuid)] def threatzone_get_syscalls(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) page = cast( int, parse_bounded_int_argument(args.get("page"), "page", minimum=1, maximum=2_147_483_647, default=1), ) limit = cast( int, parse_bounded_int_argument(args.get("limit"), "limit", minimum=1, maximum=2000, default=500), ) data = client.sdk.get_syscalls(uuid, page=page, limit=limit) return [_structured_result("ThreatZone.Submission.Syscalls", "ThreatZone Syscalls", data, uuid=uuid)] def threatzone_get_network_data( client: Client, args: dict[str, Any], sdk_method: str, section: str, title: str, ) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) limit = parse_bounded_int_argument(args.get("limit"), "limit", minimum=0, maximum=1000) skip = parse_bounded_int_argument(args.get("skip"), "skip", minimum=0, maximum=1000) window = {"limit": limit, "skip": skip} data = getattr(client.sdk, sdk_method)(uuid, **{key: value for key, value in window.items() if value is not None}) return [_structured_result(f"ThreatZone.Submission.{section}", title, data, uuid=uuid)] def _save_download(download: DownloadResponse, fallback_filename: str) -> dict[str, Any]: try: download_filename = Path(download.filename.replace("\\", "/")).name if download.filename else "" filename = fallback_filename if download_filename in ("", "download") else download_filename saved_path = download.save(filename) finally: download.close() return file_result_existing_file(str(saved_path), filename) # noqa: F405 def _safe_media_filename(filename: str, fallback_filename: str) -> str: """Reject unsafe API-provided media names instead of writing outside the War Room.""" if not filename: return fallback_filename if filename in {".", ".."} or "\x00" in filename or "/" in filename or "\\" in filename: raise DemistoException("ThreatZone returned an unsafe media filename.") # noqa: F405 return filename def threatzone_download_sdk_file( client: Client, args: dict[str, Any], sdk_method: str, fallback_pattern: str, *, id_argument: str | None = None, ) -> dict[str, Any]: uuid = _require_uuid(args) if id_argument: resource_id = args.get(id_argument) if not resource_id: raise DemistoException(f"{id_argument} argument is required.") # noqa: F405 download = getattr(client.sdk, sdk_method)(uuid, resource_id) else: download = getattr(client.sdk, sdk_method)(uuid) return _save_download(download, fallback_pattern.format(uuid=uuid)) def threatzone_download_yara_rule(client: Client, args: dict[str, Any]) -> dict[str, Any]: uuid = _require_uuid(args) timeout = cast( int, parse_bounded_int_argument(args.get("timeout"), "timeout", minimum=1, maximum=3600, default=120), ) started_at = time.monotonic() last_pending_error: YaraRulePendingError | None = None max_attempts = min(timeout, MAX_YARA_POLL_ATTEMPTS) for _ in range(max_attempts): try: download = client.sdk.download_yara_rule(uuid) return _save_download(download, f"{uuid}.yar") except YaraRulePendingError as exc: last_pending_error = exc elapsed = time.monotonic() - started_at retry_after = ( max(MIN_YARA_POLL_INTERVAL_SECONDS, exc.retry_after) if exc.retry_after is not None else YARA_POLL_INTERVAL_SECONDS ) if elapsed >= timeout or retry_after > timeout - elapsed: raise DemistoException( # noqa: F405 f"Timed out after {timeout} seconds waiting for the generated YARA rule." ) from exc demisto.executeCommand("Sleep", {"seconds": str(retry_after)}) raise DemistoException( # noqa: F405 f"Timed out after {timeout} seconds waiting for the generated YARA rule." ) from last_pending_error def threatzone_download_url_screenshot(client: Client, args: dict[str, Any]) -> dict[str, Any]: uuid = _require_uuid(args) return fileResult(f"threatzone-url-screenshot-{uuid}.png", client.sdk.get_screenshot(uuid)) # noqa: F405 def threatzone_list_media_files(client: Client, args: dict[str, Any]) -> list[CommandResults]: # noqa: F405 uuid = _require_uuid(args) return [ _structured_result( "ThreatZone.Submission.MediaFiles", "ThreatZone Media Files", client.sdk.list_media_files(uuid), uuid=uuid, ) ] def threatzone_download_media_file(client: Client, args: dict[str, Any]) -> dict[str, Any]: uuid = _require_uuid(args) file_id = args.get("file_id") if not file_id: raise DemistoException("file_id argument is required.") # noqa: F405 media_files = client.sdk.list_media_files(uuid) media_file = next((item for item in media_files if item.id == file_id), None) if media_file is None: raise DemistoException(f"Media file '{file_id}' was not found for submission '{uuid}'.") # noqa: F405 filename = _safe_media_filename(media_file.name, f"threatzone-media-{uuid}-{file_id}") return fileResult(filename, client.sdk.get_media_file(uuid, file_id)) # noqa: F405 def threatzone_get_sanitized_file(client: Client, args: dict[str, Any]) -> dict[str, Any]: uuid = _require_uuid(args) download = client.sdk.download_cdr_result(uuid) return _save_download(download, f"sanitized-{uuid}.zip") def threatzone_get_html_report_file(client: Client, args: dict[str, Any]) -> dict[str, Any]: uuid = _require_uuid(args) download = client.sdk.download_html_report(uuid) return _save_download(download, f"threatzone-report-{uuid}.html") def _format_sdk_exception(exc: Exception) -> str: if isinstance(exc, AuthenticationError): return f"Authorization error: {exc}. Verify the API key is correct and active." if isinstance(exc, PermissionDeniedError): return f"Permission denied: {exc}" if isinstance(exc, PaymentRequiredError): return f"Plan limit reached: {exc}" if isinstance(exc, NotFoundError): return f"Resource not found: {exc}" if isinstance(exc, BadRequestError): return f"Bad request: {exc}" if isinstance(exc, RateLimitError): return f"Rate limited: {exc}" if isinstance(exc, ReportUnavailableError): return f"Report not yet available: {exc}" if isinstance(exc, YaraRulePendingError): return f"YARA rule still being generated: {exc}" if isinstance(exc, AnalysisTimeoutError): return f"Analysis timed out: {exc}" if isinstance(exc, APIError): return f"ThreatZone API error: {exc}" return str(exc) CommandHandler = Callable[[Client, dict[str, Any]], Any] def _uuid_section_handler(sdk_method: str, section: str, title: str) -> CommandHandler: return lambda client, args: threatzone_get_uuid_section(client, args, sdk_method, section, title) def _network_handler(sdk_method: str, section: str, title: str) -> CommandHandler: return lambda client, args: threatzone_get_network_data(client, args, sdk_method, section, title) COMMAND_HANDLERS: dict[str, CommandHandler] = { "test-module": lambda client, args: test_module(client), "tz-check-limits": threatzone_check_limits, "tz-sandbox-upload-sample": threatzone_sandbox_upload_sample, "tz-static-upload-sample": lambda client, args: threatzone_static_or_cdr_upload(client, args, "static"), "tz-cdr-upload-sample": lambda client, args: threatzone_static_or_cdr_upload(client, args, "cdr"), "tz-url-analysis": threatzone_submit_url_analysis, "tz-open-in-browser": threatzone_open_in_browser, "tz-list-submissions": threatzone_list_submissions, "tz-search-submissions-by-sha256": threatzone_search_submissions, "tz-get-metafields": threatzone_get_metafields, "tz-get-environments": threatzone_get_environments, "tz-list-network-configs": threatzone_list_network_configs, "tz-get-result": threatzone_get_result, "tz-get-indicator-result": threatzone_get_indicator_result, "tz-get-ioc-result": threatzone_get_ioc_result, "tz-get-yara-result": threatzone_get_yara_result, "tz-get-artifact-result": threatzone_get_artifact_result, "tz-get-config-result": threatzone_get_config_result, "tz-get-overview-summary": _uuid_section_handler("get_overview_summary", "OverviewSummary", "ThreatZone Overview Summary"), "tz-get-eml-analysis": _uuid_section_handler("get_eml_analysis", "EMLAnalysis", "ThreatZone EML Analysis"), "tz-get-mitre-techniques": _uuid_section_handler("get_mitre_techniques", "MITRE", "ThreatZone MITRE ATT&CK Techniques"), "tz-get-static-scan-result": _uuid_section_handler("get_static_scan_results", "StaticScan", "ThreatZone Static Scan"), "tz-get-cdr-result": _uuid_section_handler("get_cdr_results", "CDRResult", "ThreatZone CDR Result"), "tz-get-signature-check-result": _uuid_section_handler( "get_signature_check_results", "SignatureCheck", "ThreatZone Signature Check" ), "tz-get-processes": _uuid_section_handler("get_processes", "Processes", "ThreatZone Processes"), "tz-get-process-tree": _uuid_section_handler("get_process_tree", "ProcessTree", "ThreatZone Process Tree"), "tz-get-behaviours": threatzone_get_behaviours, "tz-get-syscalls": threatzone_get_syscalls, "tz-get-url-analysis-result": _uuid_section_handler("get_url_analysis", "URLAnalysis", "ThreatZone URL Analysis"), "tz-get-network-summary": _uuid_section_handler("get_network_summary", "NetworkSummary", "ThreatZone Network Summary"), "tz-get-dns-queries": _network_handler("get_dns_queries", "DNSQueries", "ThreatZone DNS Queries"), "tz-get-http-requests": _network_handler("get_http_requests", "HTTPRequests", "ThreatZone HTTP Requests"), "tz-get-tcp-connections": _network_handler("get_tcp_connections", "TCPConnections", "ThreatZone TCP Connections"), "tz-get-udp-connections": _network_handler("get_udp_connections", "UDPConnections", "ThreatZone UDP Connections"), "tz-get-network-threats": _network_handler("get_network_threats", "NetworkThreats", "ThreatZone Network Threats"), "tz-get-sanitized": threatzone_get_sanitized_file, "tz-download-html-report": threatzone_get_html_report_file, "tz-download-static-scan-strings": lambda client, args: ( threatzone_download_sdk_file(client, args, "get_static_scan_strings", "{uuid}_strings.json") ), "tz-download-sample": lambda client, args: threatzone_download_sdk_file(client, args, "download_sample", "sample-{uuid}"), "tz-download-artifact": lambda client, args: threatzone_download_sdk_file( client, args, "download_artifact", "artifact-{uuid}", id_argument="artifact_id" ), "tz-download-pcap": lambda client, args: threatzone_download_sdk_file( client, args, "download_pcap", "threatzone-{uuid}.pcap" ), "tz-download-yara-rule": threatzone_download_yara_rule, "tz-download-url-screenshot": threatzone_download_url_screenshot, "tz-list-media-files": threatzone_list_media_files, "tz-download-media-file": threatzone_download_media_file, } def main() -> None: params = demisto.params() base_url = str(params.get("url") or "") verify = not params.get("insecure", False) proxy = params.get("proxy", False) api_key = extract_api_key(params.get("apikey")) reliability = params.get("integrationReliability") handle_proxy() # noqa: F405 - propagates HTTPS_PROXY env vars when proxy enabled command = demisto.command() demisto.debug(f"Command being called is {command}") args = demisto.args() client: Client | None = None try: client = Client(base_url=base_url, api_key=api_key, verify=verify, proxy=proxy, reliability=reliability) handler = COMMAND_HANDLERS.get(command) if handler is None: raise DemistoException(f"Command '{command}' is not implemented.") # noqa: F405 return_results(handler(client, args)) # noqa: F405 except Exception as exc: demisto.error(traceback.format_exc()) error_message = _format_sdk_exception(exc) if isinstance(exc, ThreatZoneError) else str(exc) return_error(f"Failed to execute {command} command.\nError:\n{error_message}", error=exc) # noqa: F405 finally: if client is not None: try: client.close() except Exception: # pragma: no cover - cleanup best-effort pass if __name__ in ("__main__", "__builtin__", "builtins"): main()