HelloWorldV2
Use the Hello World v2 integration to learn how to build and configure Cortex integrations.
Utilities · HelloWorld
Details
| ID | HelloWorldV2 |
|---|---|
| Provider | Open Source |
| Category | Utilities |
| From Version | 6.8.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | Agentix XSIAM Exposure Management |
README
Overview
This is the Hello World v2 integration for getting started.
Configure Hello World v2 in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL (e.g., https://api.dummy-example.com) | Default is https://api.dummy-example.com. | True |
| API Key | True | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Score threshold for IP reputation command | The minimum HelloWorld score required to mark an IP as malicious (0-100). Default is 65. | False |
| Source Reliability | Reliability of the source providing the intelligence data. Possible values are: A+ - 3rd party enrichment, A - Completely reliable, B - Usually reliable, C - Fairly reliable, D - Not usually reliable, E - Unreliable, F - Reliability cannot be judged. Default is C - Fairly reliable. | False |
| First fetch time | The time from which to start fetching alerts. Supports relative time (e.g., “3 hours ago”) or ISO 8601 format (e.g., “2025-12-01T00:00:00Z”). Default is 3 days. | False |
| Severity of alerts to fetch | Possible values are: low, medium, high, critical. Default is high. | False |
| Fetch incidents | Fetch HelloWorld alerts as incidents in Cortex XSOAR. Supported in Cortex XSOAR only. | False |
| Incident type | False | |
| Maximum number of incidents per fetch | Default is 10. Supported in Cortex XSOAR only. | False |
| Fetch events | Fetch HelloWorld alerts as events in Cortex XSIAM. Supported in Cortex XSIAM only. | False |
| Maximum number of events per fetch | Default is 1000. Supported in Cortex XSIAM only. | False |
| Fetch assets and vulnerabilities | 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.
helloworld-say-hello
Prints hello to a specified name.
Base Command
helloworld-say-hello
Input
| Argument Name | Description | Required |
|---|---|---|
| name | The name of the person you want to say hello to. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| HelloWorld.Hello.name | String | The greeting message returned by the command. |
Command example
!helloworld-say-hello name="Dbot"
Context Example
{
"HelloWorld": {
"Hello": {
"name": "Dbot"
}
}
}
Human Readable Output
Hello Dbot
helloworld-alert-list
Lists example alerts as they would appear in a fetch operation.
Base Command
helloworld-alert-list
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | Filter the fetch by alert ID. If not specified, all alert IDs will be retrieved. | Optional |
| limit | How many alerts to fetch. Default is 10. | Optional |
| severity | The severity by which to filter the alerts. Possible values are: low, medium, high, critical. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| HelloWorld.alert.id | Number | The ID of the alert. |
| HelloWorld.alert.name | String | The name of the alert. |
| HelloWorld.alert.severity | String | The severity of the alert. |
| HelloWorld.alert.date | Date | The date of the alert occurrence. |
| HelloWorld.alert.status | String | The status of the alert. |
Command example
!helloworld-alert-list limit="3" severity="low"
Context Example
{
"HelloWorld": {
"Alert": [
{
"date": "2023-09-14T11:30:39.882955",
"id": 1,
"name": "XSOAR Test Alert #1",
"severity": "low",
"status": "Testing"
},
{
"date": "2023-09-14T11:30:39.882955",
"id": 2,
"name": "XSOAR Test Alert #2",
"severity": "low",
"status": "Testing"
},
{
"date": "2023-09-14T11:30:39.882955",
"id": 3,
"name": "XSOAR Test Alert #3",
"severity": "low",
"status": "Testing"
}
]
}
}
Human Readable Output
Items List (Sample Data)
date id name severity status 2023-09-14T11:30:39.882955 1 XSOAR Test Alert #1 low Testing 2023-09-14T11:30:39.882955 2 XSOAR Test Alert #2 low Testing 2023-09-14T11:30:39.882955 3 XSOAR Test Alert #3 low Testing
Command example
!helloworld-alert-list alert_id=2
Context Example
{
"HelloWorld": {
"Alert": {
"date": "2023-09-14T11:30:39.882955",
"id": 2,
"name": "XSOAR Test Alert #2",
"severity": "low",
"status": "Testing"
}
}
}
Human Readable Output
Items List (Sample Data)
date id name severity status 2023-09-14T11:30:39.882955 2 XSOAR Test Alert #2 low Testing
helloworld-alert-note-create
Create a note in the API.
Base Command
helloworld-alert-note-create
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | The alert ID to add the note to. | Required |
| note_text | The text to add to the note. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| HelloWorld.Note.status | String | The note creation status. |
| HelloWorld.Note.msg | String | The message from the note creation response. |
Command example
!helloworld-alert-note-create alert_id=2 note_text=test
Context Example
{
"HelloWorld": {
"Note": {
"msg": "Note was created for alert #2 successfully with comment: test",
"status": "success"
}
}
}
Human Readable Output
Note was created successfully.
ip
The returned IP information and reputation.
Base Command
ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | A comma-separated list of IPs. | Required |
| threshold | The score threshold used to determine if an IP is malicious. If not provided, the default threshold from the instance configuration is used. Default is 65. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| HelloWorld.IP.asn | String | The autonomous system name (ASN) for the IP address. |
| HelloWorld.IP.asn_cidr | String | The network routing prefix in CIDR notation associated with the ASN. |
| HelloWorld.IP.asn_country_code | String | The two letter ISO country code associated with the ASN. |
| HelloWorld.IP.asn_date | Date | The date the ASN was assigned. |
| HelloWorld.IP.asn_description | String | The ASN description. |
| HelloWorld.IP.asn_registry | String | The registry the ASN belongs to. |
| HelloWorld.IP.entities | String | Entities associated to the IP. |
| HelloWorld.IP.ip | String | The actual IP address. |
| HelloWorld.IP.network.cidr | String | The network CIDR for the IP address. |
| HelloWorld.IP.network.country | String | The country of the IP address. |
| HelloWorld.IP.network.end_address | String | The last IP address of the CIDR. |
| HelloWorld.IP.network.events.action | String | The specific action recorded for the network (for example, registration or modification). |
| HelloWorld.IP.network.events.actor | Unknown | The actor (identifier or entity name) that performed the recorded action on the network. |
| HelloWorld.IP.network.events.timestamp | String | The date and time the event occurred. |
| HelloWorld.IP.network.handle | String | The unique registry identifier assigned to the network block. |
| HelloWorld.IP.network.ip_version | String | The IP address version. |
| HelloWorld.IP.network.links | String | Links associated to the IP address. |
| HelloWorld.IP.network.name | String | The name of the network. |
| HelloWorld.IP.network.notices.description | String | The description of the notice. |
| HelloWorld.IP.network.notices.links | Unknown | A list of URLs providing additional information or documentation related to the network notice. |
| HelloWorld.IP.network.notices.title | String | The title of a specific notice related to the network. |
| HelloWorld.IP.network.parent_handle | String | The unique registry identifier of the parent network from which this block was allocated. |
| HelloWorld.IP.network.raw | Unknown | Additional raw data for the network. |
| HelloWorld.IP.network.remarks | Unknown | Additional remarks for the network. |
| HelloWorld.IP.network.start_address | String | The first IP address of the CIDR. |
| HelloWorld.IP.network.status | String | The network status. |
| HelloWorld.IP.network.type | String | The type of the network. |
| HelloWorld.IP.query | String | The IP address that was queried. |
| HelloWorld.IP.raw | Unknown | Additional raw data for the IP address. |
| HelloWorld.IP.score | Number | The reputation score from HelloWorld for this IP (0 to 100, where higher is worse). |
| IP.Address | String | The IP address. |
| IP.Malicious.Vendor | String | The vendor reporting the IP address as malicious. |
| IP.Malicious.Description | String | A description explaining why the IP address was reported as malicious. |
| IP.ASN | String | The autonomous system name for the IP address. |
| IP.Relationships.EntityA | string | The source of the relationship. |
| IP.Relationships.EntityB | string | The destination of the relationship. |
| IP.Relationships.Relationship | string | The name of the relationship. |
| IP.Relationships.EntityAType | string | The relationship source type. |
| IP.Relationships.EntityBType | string | The relationship destination type. |
Command example
!ip ip="8.8.8.8"
Context Example
{
"DBotScore": {
"Indicator": "8.8.8.8",
"Reliability": "C - Fairly reliable",
"Score": 3,
"Type": "ip",
"Vendor": "HelloWorld"
},
"HelloWorld": {
"IP": {
"id": "x.x.x.x",
"ip": "8.8.8.8",
"links": {
"self": "https://www.virustotal.com/api/v3/ip_addresses/x.x.x.x"
},
"type": "ip_address"
}
},
"IP": {
"Address": "8.8.8.8",
"Malicious": {
"Description": "Hello World returned reputation -4",
"Vendor": "HelloWorld"
},
"Relationships": [
{
"EntityA": "8.8.8.8",
"EntityAType": "IP",
"EntityB": "h",
"EntityBType": "URL",
"Relationship": "related-to"
},
{
"EntityA": "8.8.8.8",
"EntityAType": "IP",
"EntityB": "x",
"EntityBType": "URL",
"Relationship": "related-to"
}
]
}
}
Human Readable Output
IP (Sample Data)
id ip links type x.x.x.x 8.8.8.8 self: https://www.virustotal.com/api/v3/ip_addresses/x.x.x.x ip_address Attributes
as_owner asn continent country jarm last_analysis_stats last_modification_date network regional_internet_registry reputation tags total_votes whois_date EMERALD-ONION 396507 NA US :jarm: harmless: 72
malicious: 5
suspicious: 2
timeout: 0
undetected: 81613300914 :cidr: ARIN -4 harmless: 0
malicious: 11611870274
helloworld-get-events
Retrieves alerts from the HelloWorld API. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
Base Command
helloworld-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| severity | The severity by which to filter the alerts. Possible values are: low, medium, high, critical. | Required |
| start_time | The time from which to start fetching alerts. Supports relative time (e.g., “3 hours ago”) or ISO 8601 format (e.g., “2025-12-01T00:00:00Z”). | Optional |
| limit | Maximum number of alerts to retrieve. Default is 10. | Optional |
| should_push_events | Whether to push events to Cortex XSIAM (for Cortex XSIAM tenants only). Possible values are: true, false. Default is false. | Optional |
Context Output
There is no context output for this command.
Command example
!helloworld-get-events severity="low" limit=3
Human Readable Output
HelloWorld Events
id severity user action date status 1 low userB@test.com Testing 2023-09-14T11:30:39.882955 Error 2 low userA@test.com Testing 2023-09-14T11:30:39.883955 Success 3 low userB@test.com Testing 2023-09-14T11:30:39.884955 Error
helloworld-job-submit
Submits a job to the HelloWorld API and polls for completion. Used for asynchronous APIs and long-running operations.
Base Command
helloworld-job-submit
Input
| Argument Name | Description | Required |
|---|---|---|
| interval_in_seconds | Interval in seconds between each poll. Default is 30. | Optional |
| timeout_in_seconds | Timeout in seconds until polling stops. Default is 600. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| HelloWorld.Job.id | String | The ID of the submitted job. |
| HelloWorld.Job.status | String | The current status of the job. |
| HelloWorld.Job.type | String | The type of job submitted. |
| HelloWorld.Job.msg | String | Message from the completed job. |
Command example
#### Context Example
```json
{
"HelloWorld": {
"Job": {
"id": "abc-123",
"msg": "The configuration has successfully been updated.",
"status": "complete",
"type": "HelloWorldRefreshConfig"
}
}
}
Human Readable Output
HelloWorld Job abc-123 - Complete
id msg abc-123 The configuration has successfully been updated.
helloworld-get-assets
Retrieves resources and assets in the HelloWorld environment.
Base Command
helloworld-get-assets
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | Maximum number of assets to retrieve. Default is 10. | Optional |
Context Output
There is no context output for this command.
Command example
!helloworld-get-assets limit=3
Human Readable Output
HelloWorld Assets
id name type status created 1 Server-01 server active 2024-01-15T10:00:00 2 Database-01 database active 2024-01-16T11:30:00 3 Storage-01 storage active 2024-01-17T09:15:00
helloworld-get-vulnerabilities
Retrieves vulnerabilities found in the HelloWorld environment.
Base Command
helloworld-get-vulnerabilities
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | Maximum number of vulnerabilities to retrieve. Default is 10. | Optional |
Context Output
There is no context output for this command.
Command example
!helloworld-get-vulnerabilities limit=3
Human Readable Output
HelloWorld Vulnerabilities
id cve_id severity description published 1 CVE-MOCK-0001 critical Remote code execution vulnerability 2026-01-10T08:00:00 2 CVE-MOCK-0002 high SQL injection vulnerability 2026-01-12T14:30:00 3 CVE-MOCK-0003 medium Cross-site scripting vulnerability 2026-01-14T16:45:00
Developer Guide
This section documents the key architectural patterns and code features in Hello World v2 and provides guidelines for building robust Cortex integrations.
Key Patterns and Features
| Feature | Description |
|---|---|
| Robust User Input Validation | Type-safe configuration parameter and command argument validation with user-friendly error messages. |
| Modern API Client | Uses the ContentClient, which provides enhanced reliability, observability, and developer experience features. |
| Polling / Scheduled Commands | Commands that can schedule the future execution of other commands; suitable for periodically checking the status of a long-running external process or asynchronous task. |
| Centralized Execution Configuration | A centralized object for commands, configuration params, command arguments, and fetch last run state to minimize redundant system calls. |
| Dual Fetch Incidents / Events Support | A Unified flow for fetching Cortex XSOAR incidents and Cortex XSIAM events. |
| Fetch Assets and Vulnerabilities Support | A flow for fetching a current snapshot of an environment’s resources and vulnerabilities in Cortex XSIAM. |
| Structured Logging | A consistent, prefix-based Python f-string format that captures specific variable context, ensuring messages are easily searchable and facilitate efficient debugging of the execution flow. |
How to Build an Integration
Below is a step-by-step guide on how to build an integration that implements a basic automation command and fetch flow.
- Import the Required Modules
- Define a Configuration Parameters Validation Model
- Create an API Client Class
- Implement Standard Automation Command Pattern
- Implement Standard Fetch Flow Pattern
- Setup Execution Configuration
- Define the Main Function
1. Import the Required Modules
At the top of the integration code file, import the required modules. This can include built-in Python modules such as enum, typing, and asyncio, as well as Content-related imports such as CommonServerPython and ContentClientApiModule.
# Use enumerations to group related constants or define all possible values of configuration parameter or command argument
# For example, severity values: critical, high, moderate, low, unknown
from enum import Enum
# Use `typing` and/or `collections.abc` for defining attribute types in validation models and for type hinting.
from typing import Any
from collections.abc import Awaitable
# Import `CommonServerPython` and `demisto` class, which contain many useful helper and utility functions
from CommonServerPython import *
import demistomock as demisto
# Optionally use `CommonServerUserPython` (for custom integrations) to override constants, functions, and classes defined in `CommonServerPython`
from CommonServerUserPython import *
# Import `ContentClientApiModule` to use the new `ContentClient` class, which contains improved error handling, thread safety, and authentication handling.
from ContentClientApiModule import *
# Add any other required imports depending on your code
from datetime import datetime, UTC, timedelta
2. Define a Configuration Parameters Validation Model
Define the schema that corresponds to the configuration parameters in the integration YML file.
Use Pydantic classes that inherit from ContentBaseModel (or its subclasses like BaseParams) for robust input validation with user-friendly error messages.
You can use an AI agent to automatically generate models from the configuration parameters defined in the integration YML file.
class Credentials(ContentBaseModel):
"""Credentials model for API authentication."""
username: str
password: SecretStr
class MyIntegrationParams(BaseParams):
"""Integration parameters with validation.
Attributes:
url: API base URL (trailing slash removed automatically).
credentials: Username and password for API Authentication.
max_fetch: Maximum incidents per fetch.
"""
# `proxy` and `insecure` are already defined in `BaseParams`
url: AnyUrl
credentials: Credentials
# Ensure attribute name matches the param `name` field value in the YML
# To follow Python "snake_case" format, use an `alias` value for mapping to the `camelCase` param name in the YML
is_fetch: bool | None = Field(default=False, alias="isFetch") # corresponds to "Fetch incidents" checkbox
first_fetch: str = "1 week"
max_fetch: int = 50
@property
def first_fetch_datetime(self) -> datetime:
"""Cast first fetch to a datetime object."""
return arg_to_datetime(self.first_fetch) or (datetime.now(tz=UTC) - timedelta(weeks=1))
@validator('url', reuse=True)
def clean_url(cls, v) -> str:
"""Remove trailing slash from URL."""
return v.rstrip('/')
@validator('max_fetch', reuse=True)
def validate_max_fetch(cls, v) -> int:
"""Check that max fetch is not above the permitted value."""
max_fetch = arg_to_number(v)
if max_fetch > 1000:
raise ValueError("The maximum number of incidents per fetch must not be greater than 1000.")
return max_fetch
3. Create an API Client Class
Create a MyIntegrationClient class that inherits from ContentClient to leverage built-in retry logic, rate limit handling, authentication, and thread safety.
For authentication, define a custom AuthHandler if needed or use any of the included ones in ContentClientApiModule such as APIKeyAuthHandler, BearerTokenAuthHandler, or BasicAuthHandler.
# Example client class that inherits from ContentClient and adds two integration-specific methods
class MyIntegrationClient(ContentClient):
def __init__(self, params: HelloWorldParams):
"""Initialize client with ContentClient capabilities.
Args:
params (MyIntegrationParams): Validated integration configuration parameters.
"""
credentials: Credentials = params.credentials
super().__init__(
base_url=params.url,
verify=params.verify,
proxy=params.proxy,
auth_handler=BasicAuthHandler(username=credentials.username, password=credentials.password),
client_name="MyIntegrationClient",
diagnostic_mode=is_debug_mode(), # enable if commands are run with `debug-mode=true`
)
def get_item_by_id(self, item_id: int) -> dict[str, Any]:
"""Get an item in MyIntegration by its ID.
Args:
item_id (int): Item ID.
Returns:
dict[str, Any]: Item dictionary.
"""
endpoint = f"api/items/{item_id}"
return self.get(endpoint) # JSON response bodies are decoded by default
def get_items_list(self, limit: int, start_time: str | None = None) -> list[dict]:
"""Get a list of items in MyIntegration up to the limit.
Args:
limit (int): Maximum number of items to return.
start_time (str | None): Optional start time in ISO 8601 format.
Returns:
list[dict]: List of items.
"""
endpoint = "api/items"
query_params = assign_params(limit=limit, start_time=start_time) # use `assign_params` to remove empty values
return self.get(endpoint, params=query_params)
4. Implement Standard Automation Command Pattern
Define an arguments validation model and a command function for each command. Define additional (helper) functions if needed.
You can use an AI agent to automatically generate models from the command arguments defined in the integration YML file.
The code snippet below demonstrates how to implement a basic automation command.
See the following references for example implementations of more complex commands:
| Type | Example |
|---|---|
| Polling / scheduled command | helloworld-job-poll command in Hello World v2 |
| Generic reputation command | ip command in Hello World v2 |
# Basic automation command example implementation
class MyIntegrationItemListArgs(BaseArgs):
"""Arguments for `my-integration-item-list` command.
Attributes:
item_id: Optional item ID to retrieve.
limit: Maximum number of items to retrieve (default: 10).
"""
item_id: int | None = None
limit: int = 10
def my_integration_item_list_command(client: MyIntegrationClient, args: MyIntegrationItemListArgs) -> CommandResults:
"""Run `my-integration-item-list` command logic.
Args:
client (MyIntegrationClient): An initialized API client instance.
args (MyIntegrationItemListArgs): Validated command arguments.
Returns:
CommandResults: Command results containing context and human-readable outputs.
"""
if args.item_id:
items = client.get_item_by_id(item_id=args.item_id)
else:
items = client.get_items_list(limit=args.limit)
return CommandResults(
outputs_prefix="MyIntegration.Item", # Context output prefix
outputs_key_field="id", # Objects under the defined prefix will be deduplicated according to this field value
outputs=items, # The items to return to the context output
readable_output=tableToMarkDown("My Integration Items", items), # Human-readable entry to return to the war room
)
5. Implement Standard Fetch Flow Pattern
The code snippet below demonstrates how to implement a basic fetch-incidents flow.
See the following references for example implementations of more complex fetch flows:
| Type | Example |
|---|---|
| Unified Cortex XSOAR fetch incidents and Cortex XSIAM fetch events | fetch-incidents and fetch-events commands, respectively, in Hello World v2 |
| Cortex XSIAM fetch assets flow | fetch-assets command in Hello World v2 |
| Fetch indicators | fetch-indicators command in Hello World Feed |
# Basic fetch-incidents example implementation
class MyIntegrationLastRun(BaseLastRun):
"""State management for fetch-incidents.
Attributes:
start_time: ISO 8601 timestamp of the last fetched item.
last_item_ids: List of item IDs from the last fetch time to prevent duplicates.
"""
start_time: str | None = None
last_item_ids: list[int] = []
def fetch_incidents(
client: MyIntegrationClient,
last_run: MyIntegrationLastRun,
max_fetch: int,
first_fetch_datetime: datetime,
) -> MyIntegrationLastRun:
"""Fetch new items as incidents.
Args:
client (MyIntegrationClient): An initialized API client instance.
last_run (MyIntegrationLastRun): Last run state from previous fetch invocation.
max_fetch (int): Maximum number of incidents to fetch.
first_fetch_datetime (datetime): Date from which to start fetching incidents.
"""
default_batch_limit: int = 100
start_time: str = last_run.start_time or first_fetch_datetime.isoformat()
last_item_ids: list[int] = last_run.last_item_ids
unique_items: list[dict] = []
while len(unique_items) < max_fetch:
# Send requests in batches to avoid exceeding the API's maximum `limit` value
remaining_count = max_fetch - len(unique_items)
batch_limit = min(default_batch_limit, remaining_count)
items = client.get_items_list(limit=batch_limit, start_time=start_time)
# Deduplication logic
for item in items:
if item["id"] in last_item_ids:
continue
unique_items.append(item)
if unique_items:
start_time = unique_items[-1]["time"]
last_item_ids = [item["id"] for item in unique_items if item["time"] == start_time]
# Formatting and incident creation logic
incidents = format_as_incidents(unique_items)
demisto.createIncidents(incidents) # create incidents
return MyIntegrationLastRun(start_time=start_time, last_item_ids=last_item_ids)
6. Setup Execution Configuration
Inherits from the BaseExecutionConfig class in BaseContentApiModule to centralize command execution context, prevent redundant system calls (via the demisto class), and provide type-safe access to configuration parameters, command arguments, and fetch last run state.
class MyIntegrationExecutionConfig(BaseExecutionConfig):
@property
def params(self) -> MyIntegrationItemListArgs:
return MyIntegrationItemListArgs(**self._raw_params)
@property
def item_list_args(self) -> MyIntegrationItemListArgs:
"""Get validated arguments for `my-integration-item-list` command."""
return MyIntegrationItemListArgs.get(**self._raw_args)
@property
def last_run(self) -> MyIntegrationLastRun:
"""Get validated last run object for `fetch-incidents` flow."""
return MyIntegrationLastRun(**self._raw_last_run)
7. Define the Main Function
Define and implement a main() function, which would serve as the entrypoint into the integration logic.
This function should initialize the Execution Configuration and API Client classes and route to the implemented command functions.
def main():
execution = ExecutionConfig()
command: str = execution.command
client = None
try:
params: MyIntegrationParams = execution.params
client = MyIntegrationClient(params)
# Ensure the integration implements connection / configuration testing logic
if command == "test-module":
return_results(my_integration_test_module(client, params))
# Route to automation command
elif command == "my-integration-item-list":
args = execution.item_list_args
return_results(my_integration_item_list_command(client, args))
elif command == "fetch-incidents":
last_run = execution.last_run
next_run = fetch_incidents(client, last_run, max_fetch=params.max_fetch, first_fetch_datetime=params.first_fetch_datetime)
next_run.set()
else:
raise NotImplementedError(f"Command {command} is not implemented")
# Log exceptions and return errors
except Exception as e:
demisto.error(f"[Main] Failed to execute {command=}: {str(e)}. {traceback.format_exc()}")
return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")
finally:
demisto.debug(f"[Main] Generating diagnostic report after executing {command=}.")
if client:
client.log_optional_diagnostic_report()
if __name__ in ("__main__", "__builtin__", "builtins"):
main()
AI Agent Prompts
Uplift an Existing Integration
Update the logic in MyIntegration.py to follow the style of HelloWorldV2.py.
REFERENCE DOCUMENTATION
- The ContentClientApiModule README.md file
- The "Developer Guide" section in the HelloWorldV2 README.md file
- The HelloWorldV2.py code file
FOCUS AREAS
Focus on implementing key features <including fetch if relevant> and avoid common pitfalls, as documented.
REQUIREMENTS
- Maintain backward compatibility with existing YML configuration parameters and command arguments (Do not add any new commands and keep existing ones)
- Preserve all integration-specific logic (authentication methods, API quirks, data transformations)
- Include consistent debug and diagnostic logging that captures specific variable context
- Pass type checking with Pydantic validation models
- Follow the region-based organization from HelloWorldV2
Make sure to update the unit test to match the new code in MyIntegration.py in the style of HelloWorldV2_test.py. Mock API responses and helper functions where needed.
Update the API Module dependencies and Docker image in MyIntegration.yml to match HelloWorldV2.yml
Create a New Integration
Create a new integration called MyIntegration available in the <xsoar|marketplacev2|platform> marketplaces with supported modules <...> and support level <xsoar|partner|community|...> inside my <new|existing> MyPack pack.
REFERENCE DOCUMENTATION
- The ContentClientApiModule README.md file
- The "Developer Guide" section in the HelloWorldV2 README.md file
- The HelloWorldV2.py code file
FOCUS AREAS
Focus on implementing key features <including fetch if relevant> and avoid common pitfalls, as documented.
API DETAILS
- Base URL: <https://api.example.com>
- Authentication: <OAuth2|Basic|API Key|Custom>
- Rate Limits: <X requests per minute/hour>
CONFIGURATION PARAMETERS
For each parameter, define in YML and include in the parameter validation model:
1. **Required** `url` - "Server URL" (type: 0, default: https://api.example.com)
2. **Required** `credentials` - "Client ID/Client Secret" (type: 9)
3. **Optional** `proxy` - "Use system proxy settings" (type: 8, inherited from BaseParams)
4. **Optional** `insecure` - "Trust any certificate" (type: 8, inherited from BaseParams)
5. **Optional** `<param_name>` - "<Display Name>" (type: <0-16>, default: <value>)
6. Add more parameters as needed with clear descriptions
COMMANDS TO IMPLEMENT
For each command, define: YML configuration, argument validation model, command function, and client method.
1. **test-module**: Validate connectivity and authentication
- No arguments
- Returns: "ok" on success
2. **<integration-name>-<command-name>**: <Brief description>
- Arguments:
- **<Required|Optional>** `<arg_name>`: <description> (Python type: <str|int|bool>, default: <value>)
- Context Outputs:
- `<IntegrationName>.<CommandPrefix>.<key>` : <description> (YML type: <string|number|boolean|object>)
- API Endpoint: <GET|POST|PUT|DELETE> /v1/<endpoint>
- Request <Params|Body>: <list parameters>
- Returns: CommandResults with readable markdown table and outputs_prefix="<IntegrationName>.<CommandPrefix>"
3. <Add more commands following the same pattern
REQUIREMENTS
- Ensure matching configuration parameters and command arguments in both the YML and code
- Pass type checking with Pydantic validation models
- Follow the region-based organization from HelloWorldV2
- Include consistent debug and diagnostic logging that captures specific variable context
Make sure to write parameterized unit tests in the style of HelloWorldV2_test.py that check edge cases and cover at least 80% of the code. Mock API responses and helper functions where needed.
Update the API Module dependencies and Docker image in MyIntegration.yml to match HelloWorldV2.yml
Pitfalls to Avoid
Avoid the following where possible to adhere to best practices:
- Do not create an API Client that inherits from
BaseClientinCommonServerPython; inherit fromContentClientinContentClientApiModuleinstead, which is backwards compatible. - Do not call
demisto.params()directly; define aparamsproperty under theExecutionConfigclass and useexecution.paramsinstead. - Do not forget to define an alias for YML configuration parameter or command argument mapping if the field name in the YML is in the “camelCase” format in the validation models. For example:
snake_case_name: str = Field(alias="camelCaseName"). - Do not skip defining a custom Pydantic
@validatorfunction for specific field(s) under the validation model for complex validation logic. - Do not forget to call
client.log_optional_diagnostic_report()infinallyblock of themain()function. - Do not forget to accommodate all tenant types if the integration supports multiple marketplaces. For example
send_events_to_xsiamis not supported on Cortex XSOAR tenants.
Additional Resources
- Python Integration Development Guide and Code Standards
- Reputation Commands and DBotScore Documentation
- Pydantic 1.10 Documentation
- Python Type Hints
<~PLATFORM>
License Requirements
The following configuration parameters require the Cortex XSIAM license:
- Fetch events
The following configuration parameters require Cortex XSIAM with the Exposure Management add-on:
- Fetch assets and vulnerabilities
The following configuration parameters require one of these licenses: Cortex XSIAM or Agentix:
- Fetch incidents
</~PLATFORM>
Configuration parameters
url— Server URL (e.g., https://api.dummy-example.com) (required)credentials— (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingseventFetchInterval— Events Fetch Intervalthreshold_ip— Score threshold for IP reputation commandintegrationReliability— Source Reliabilityfirst_fetch— First fetch timeseverity— Severity of alerts to fetchisFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch IntervalincidentType— Incident typemax_incidents_fetch— Maximum number of incidents per fetchisFetchEvents— Fetch eventsmax_events_fetch— Maximum number of events per fetchisFetchAssets— Fetch assets and vulnerabilitiesassetsFetchInterval— Assets and vulnerabilities fetch interval
Commands (9)
-
helloworld-alert-listLists example alerts as they would appear in a fetch operation.
-
helloworld-alert-note-createCreate a note in the API.
-
helloworld-get-assetsRetrieves resources and assets in the HelloWorld environment.
-
helloworld-get-eventsRetrieves alerts from the HelloWorld API. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
-
helloworld-get-vulnerabilitiesRetrieves vulnerabilities found in the HelloWorld environment.
-
helloworld-job-pollPolls a submitted job for completion status. This is called automatically by helloworld-job-submit.
-
helloworld-job-submitSubmits a job to the HelloWorld API and polls for completion. Used for asynchronous APIs and long-running operations.
-
helloworld-say-helloPrints hello to a specified name.
-
ipThe returned IP information and reputation.
"""HelloWorldV2 Integration - Unit Tests file This file contains the Unit Tests for the HelloWorldV2 Integration based on pytest. Cortex contribution requirements mandate that every integration should have a proper set of unit tests to automatically verify that the integration is behaving as expected during CI/CD pipeline. Test Execution -------------- Unit tests can be checked in multiple ways: - Using the command `demisto-sdk lint`. The command will build a dedicated Docker instance for your integration locally and use the Docker instance to execute your tests in a dedicated Docker instance. - Using the command `demisto-sdk pre-commit pytest` (Ensure all relevant files are staged). - From the command line using `pytest -v` or `pytest -vv` - From PyCharm Example with demisto-sdk (from the content root directory): demisto-sdk lint -i Packs/HelloWorld/Integrations/HelloWorldV2 Coverage -------- There should be at least one unit test per command function. In each unit test, the target command function is executed with specific parameters and the output of the command function is checked against an expected output. Unit tests should be self contained and should not interact with external resources like (API, devices, ...). To isolate the code from external resources you need to mock the API of the external resource using pytest-mock: https://github.com/pytest-dev/pytest-mock/ In the following code we configure requests-mock (a mock of Python requests) before each test to simulate the API calls to the HelloWorld API. This way we can have full control of the API behavior and focus only on testing the logic inside the integration code. We recommend to use outputs from the API calls and use them to compare the results when possible. See the ``test_data`` directory that contains the data we use for comparison, in order to reduce the complexity of the unit tests and avoiding to manually mock all the fields. NOTE: we do not have to import or build a requests-mock instance explicitly. requests-mock library uses a pytest specific mechanism to provide a requests_mock instance to any function with an argument named requests_mock. More Details ------------ More information about Unit Tests in Cortex XSOAR: https://xsoar.pan.dev/docs/integrations/unit-testing """ import json import pytest from pytest_mock import MockerFixture from unittest.mock import AsyncMock from CommonServerPython import DemistoException, Common, arg_to_datetime from HelloWorldV2 import ( BASE_CONTEXT_OUTPUT_PREFIX, Credentials, DUMMY_VALID_API_KEY, FetchAssetsStages, HelloWorldClient, HelloWorldParams, HelloWorldLastRun, HelloWorldAssetsLastRun, HelloWorldSeverity, ContentClient, ) @pytest.fixture(autouse=True) def mock_support_multithreading(mocker: MockerFixture): """Mock support_multithreading to prevent demistomock attribute errors. This fixture automatically runs before each test to mock the support_multithreading function which is called during ContentClient initialization. Without this mock, tests fail with: AttributeError: module 'demistomock' has no attribute '_Demisto__do' """ mocker.patch("ContentClientApiModule.support_multithreading") def util_load_json(path: str): """Load JSON test data from file. Args: path (str): Path to JSON file relative to test_data directory. Returns: dict | list: Parsed JSON data. """ with open(path, encoding="utf-8") as f: return json.loads(f.read()) # region General - Credentials & Params Tests class TestCredentials: """Tests for valid Credentials model initialization.""" @pytest.mark.parametrize( "password", [ pytest.param("secret123", id="Simple password"), pytest.param("P@ssw0rd!", id="Complex password"), pytest.param("very_long_password_with_many_characters_123456789", id="Long password"), ], ) def test_credentials_valid_passwords(self, password): """ Given: - A valid password string. When: - Initializing Credentials model. Then: - Assert model is created successfully. - Assert password is stored as SecretStr. """ from HelloWorldV2 import Credentials creds = Credentials(password=password) assert creds.password.get_secret_value() == password def test_credentials_missing_password(self): """ Given: - No password provided. When: - Initializing Credentials model. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import Credentials with pytest.raises(DemistoException, match="password"): Credentials() # type: ignore[call-arg] class TestHelloWorldParams: """Tests for valid HelloWorldParams model initialization.""" @pytest.mark.parametrize( "url,expected_clean_url", [ pytest.param("https://api.example.com", "https://api.example.com", id="No trailing slash"), pytest.param("https://api.example.com/", "https://api.example.com", id="Single trailing slash"), pytest.param("https://api.example.com///", "https://api.example.com", id="Multiple trailing slashes"), ], ) def test_params_url_cleaning(self, url, expected_clean_url): """ Given: - A URL with or without trailing slashes. When: - Initializing HelloWorldParams. Then: - Assert URL is cleaned (trailing slashes removed). """ from HelloWorldV2 import HelloWorldParams params = HelloWorldParams( url=url, # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] ) assert str(params.url) == expected_clean_url @pytest.mark.parametrize( "max_fetch, expected", [ pytest.param(10, 10, id="Within limit"), pytest.param(10000, 10000, id="At limit"), pytest.param(200000, 100000, id="Exceeds limit capped"), ], ) def test_params_max_events_fetch_capping(self, mocker: MockerFixture, max_fetch: int, expected: int): """ Given: - A max_fetch value that may exceed the cap. When: - Initializing HelloWorldParams. Then: - Assert max_fetch is capped at 50 for non-event systems. """ from HelloWorldV2 import HelloWorldParams mocker.patch("HelloWorldV2.CAN_SEND_EVENTS", True) # Assume running on a Cortex XSIAM tenant params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] isFetchEvents=True, max_events_fetch=max_fetch, ) assert params.max_fetch == expected @pytest.mark.parametrize( "max_fetch, expected", [ pytest.param(10, 10, id="Within limit"), pytest.param(200, 200, id="At limit"), pytest.param(1000, 200, id="Exceeds limit capped"), ], ) def test_params_max_incidents_fetch_capping(self, mocker: MockerFixture, max_fetch: int, expected: int): """ Given: - A max_fetch value that may exceed the cap. When: - Initializing HelloWorldParams. Then: - Assert max_fetch is capped at 50 for non-event systems. """ from HelloWorldV2 import HelloWorldParams mocker.patch("HelloWorldV2.CAN_SEND_EVENTS", False) # Assume running on a Cortex XSOAR tenant params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] isFetch=True, max_incidents_fetch=max_fetch, ) assert params.max_fetch == expected def test_params_valid_severity(self): """ Given: - Valid HelloWorldSeverity enum values. When: - Initializing HelloWorldParams with each severity. Then: - Assert severity is set correctly. """ from HelloWorldV2 import HelloWorldParams, HelloWorldSeverity for severity in [HelloWorldSeverity.LOW, HelloWorldSeverity.MEDIUM, HelloWorldSeverity.HIGH, HelloWorldSeverity.CRITICAL]: params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] severity=severity, ) assert params.severity == severity def test_params_default_values(self): """ Given: - Minimal required parameters. When: - Initializing HelloWorldParams. Then: - Assert default values are set correctly. """ from HelloWorldV2 import HelloWorldParams, HelloWorldSeverity params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] ) assert params.insecure is False assert params.proxy is False assert params.max_fetch == 10 assert params.severity == HelloWorldSeverity.HIGH assert params.threshold_ip == 65 @pytest.mark.parametrize( "first_fetch,expected_contains", [ pytest.param("3 days", "T", id="Relative time - 3 days"), pytest.param("1 week", "T", id="Relative time - 1 week"), pytest.param("2026-01-15T00:00:00Z", "2026-01-15T00:00:00", id="Absolute ISO 8601 time"), ], ) def test_params_first_fetch_time_conversion(self, first_fetch, expected_contains): """ Given: - Different first_fetch values (relative and absolute time). When: - Accessing first_fetch_time property. Then: - Assert property returns ISO 8601 timestamp string. - Assert the timestamp contains expected components. """ params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] first_fetch=first_fetch, ) first_fetch_time = params.first_fetch_time assert isinstance(first_fetch_time, str) assert expected_contains in first_fetch_time # Verify it's a valid ISO 8601 format by parsing it parsed = arg_to_datetime(first_fetch_time) assert parsed is not None def test_params_missing_url(self): """ Given: - No URL provided. When: - Initializing HelloWorldParams. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloWorldParams with pytest.raises(DemistoException, match="url"): HelloWorldParams(credentials={"password": "secret"}) # type: ignore[call-arg] def test_params_missing_credentials(self): """ Given: - No credentials provided. When: - Initializing HelloWorldParams. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloWorldParams with pytest.raises(DemistoException, match="credentials"): HelloWorldParams(url="https://api.example.com") # type: ignore[call-arg,arg-type] def test_params_invalid_url(self): """ Given: - An invalid URL format. When: - Initializing HelloWorldParams. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloWorldParams with pytest.raises(DemistoException, match="url"): HelloWorldParams( url="not-a-valid-url", # type: ignore[arg-type] credentials={"password": "secret"}, # type: ignore[arg-type] ) # endregion # region test-module def test_module_authentication_error(mocker: MockerFixture): """ Given: - Client that raises ContentClientAuthenticationError. When: - Running test_module. Then: - Assert appropriate error message is returned. """ from HelloWorldV2 import test_module, ContentClientAuthenticationError # Mock demisto functions mocker.patch("HelloWorldV2.demisto.error") # Create mock client that raises authentication error mocker.patch.object(HelloWorldClient, "say_hello", side_effect=ContentClientAuthenticationError("Invalid API key")) # Create params params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password="wrong-key"), is_fetch=False, ) with pytest.raises(DemistoException, match="Invalid Credentials. Please verify your API key."): client = HelloWorldClient(params) test_module(client, params) # endregion # region helloworld-say-hello class TestHelloworldSayHelloArgs: @pytest.mark.parametrize( "name", [ pytest.param("John", id="Simple name"), pytest.param("John Doe", id="Name with space"), pytest.param("José García", id="Name with accents"), ], ) def test_say_hello_args_valid_names(self, name): """ Given: - A valid name string. When: - Initializing HelloworldSayHelloArgs. Then: - Assert model is created successfully. - Assert name is stored correctly. """ from HelloWorldV2 import HelloworldSayHelloArgs args = HelloworldSayHelloArgs(name=name) assert args.name == name def test_say_hello_args_missing_name(self): """ Given: - No name provided. When: - Initializing HelloworldSayHelloArgs. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloworldSayHelloArgs with pytest.raises(DemistoException, match="name"): HelloworldSayHelloArgs() # type: ignore[call-arg] @pytest.mark.parametrize( "name", [ pytest.param("World", id="Simple name"), pytest.param("John Doe", id="Name with space"), pytest.param("José García", id="Name with accents"), ], ) def test_say_hello_command(mocker: MockerFixture, name: str): """ Given: - Valid client and args with different name values. When: - Running say_hello_command. Then: - Assert client.say_hello is called once with the provided name. - Assert CommandResults is returned with correct readable_output and outputs. """ from HelloWorldV2 import say_hello_command, HelloworldSayHelloArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock client.say_hello expected_response = f"Hello {name}" mock_say_hello = mocker.patch.object(client, "say_hello", return_value=expected_response) # Create args args = HelloworldSayHelloArgs(name=name) # Execute command result = say_hello_command(client, args) # Assertions assert mock_say_hello.call_count == 1 assert mock_say_hello.call_args.kwargs == {"name": name} # Verify CommandResults assert result.readable_output == f"## {expected_response}" assert result.outputs == {"name": name} assert result.outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.Hello" # endregion # region fetch-incidents / fetch-events def test_create_events(mocker: MockerFixture): """ Given: - List of alert dictionaries. When: - Running create_events. Then: - Assert format_as_events is called once with correct parameters. - Assert send_events_to_xsiam is called once with formatted events. """ from HelloWorldV2 import create_events, format_as_events, EventsDatasetConfigs # Load mock alert data mock_alerts = util_load_json("test_data/alert_events.json") expected_formatted_events = format_as_events(mock_alerts, time_field="date") mock_send_events = mocker.patch("HelloWorldV2.send_events_to_xsiam") # Execute function create_events(mock_alerts) assert mock_send_events.call_count == 1 assert mock_send_events.call_args.kwargs == { "events": expected_formatted_events, "vendor": EventsDatasetConfigs.VENDOR.value, "product": EventsDatasetConfigs.PRODUCT.value, "client_class": ContentClient, } def test_create_incidents(mocker: MockerFixture): """ Given: - List of alert dictionaries. When: - Running create_incidents. Then: - Assert format_as_incidents is called once with correct parameters. - Assert demisto.incidents is called once with formatted incidents. """ from HelloWorldV2 import create_incidents, format_as_incidents # Load mock alert data mock_alerts = util_load_json("test_data/alert_events.json") # Mock formatted incidents expected_formatted_incidents = format_as_incidents( mock_alerts, id_field="id", occurred_field="date", severity_field="severity" ) # Mock helper functions mock_demisto_incidents = mocker.patch("HelloWorldV2.demisto.incidents") # Execute function create_incidents(mock_alerts) # Assertions assert mock_demisto_incidents.call_count == 1 assert mock_demisto_incidents.call_args.args[0] == expected_formatted_incidents def test_format_as_incidents(): """ Given: - List of alert dictionaries. When: - Running format_as_incidents. Then: - Assert alerts are correctly formatted as XSOAR incidents. - Assert incident fields are properly mapped. """ from HelloWorldV2 import format_as_incidents # Create mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "critical", "date": "2026-01-15T00:00:01Z", "user": "admin@example.com", "action": "Alert", "status": "Error", }, ] # Execute function incidents = format_as_incidents( mock_alerts, id_field="id", occurred_field="date", severity_field="severity", ) # Assertions assert len(incidents) == 2 assert incidents[0]["name"] == "XSOAR Test Alert #1" assert incidents[0]["occurred"] == "2026-01-15T00:00:00Z" assert incidents[0]["type"] == "Hello World Alert" assert incidents[0]["severity"] == 3 # High severity assert "rawJSON" in incidents[0] assert incidents[1]["name"] == "XSOAR Test Alert #2" assert incidents[1]["severity"] == 4 # Critical severity def test_format_as_events(): """ Given: - List of alert dictionaries. When: - Running format_as_events. Then: - Assert alerts are correctly formatted as XSIAM events. - Assert event fields are properly mapped. """ from HelloWorldV2 import format_as_events, EventsDatasetConfigs # Create mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "critical", "date": "2026-01-15T00:00:01Z", "user": "admin@example.com", "action": "Alert", "status": "Error", }, ] # Execute function events = format_as_events(mock_alerts, time_field="date") # Assertions assert len(events) == 2 assert events[0]["id"] == 1 assert events[0][EventsDatasetConfigs.TIME_KEY.value] == "2026-01-15T00:00:00Z" assert events[0][EventsDatasetConfigs.SOURCE_LOG_TYPE_KEY.value] == "Alert" assert events[1]["id"] == 2 assert events[1][EventsDatasetConfigs.TIME_KEY.value] == "2026-01-15T00:00:01Z" assert events[1][EventsDatasetConfigs.SOURCE_LOG_TYPE_KEY.value] == "Alert" @pytest.mark.asyncio async def test_get_alert_list(mocker: MockerFixture): """ Given: - Client, start_time, limit, severity, should_push, and last_alert_ids. When: - Running get_alert_list async function. Then: - Assert client.get_alert_list is called with correct parameters. - Assert alerts are fetched and deduplicated. - Assert function returns list of alerts. """ from HelloWorldV2 import get_alert_list # Create params and client params = HelloWorldParams( url="https://api.example.com", # type: ignore[arg-type] credentials=Credentials(password=DUMMY_VALID_API_KEY), # type: ignore[arg-type] ) client = HelloWorldClient(params) # Mock client.get_alert_list to return alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "critical", "date": "2026-01-15T00:00:01Z", "user": "admin@example.com", "action": "Alert", "status": "Error", }, ] mock_get_alert_list = mocker.patch.object(client, "get_alert_list", return_value=mock_alerts) # Execute async function start_time = "2026-01-15T00:00:00Z" limit = 10 severity = HelloWorldSeverity.HIGH should_push = False last_alert_ids = [] alerts = await get_alert_list( client=client, start_time=start_time, severity=severity, limit=limit, should_push=should_push, last_alert_ids=last_alert_ids, ) # Assertions assert len(alerts) == 2 assert alerts[0]["id"] == 1 assert alerts[1]["id"] == 2 # Verify client.get_alert_list was called with correct parameters mock_get_alert_list.assert_called_once_with( limit=limit, start_time=start_time, severity=severity, ) class TestHelloWorldLastRun: """Tests for HelloWorldLastRun model.""" @pytest.mark.parametrize( "start_time,last_alert_ids", [ pytest.param(None, [], id="Initial state - no data"), pytest.param("2026-01-15T00:00:00Z", [], id="With start_time only"), pytest.param("2026-01-15T00:00:00Z", [1, 2, 3], id="With start_time and alert IDs"), pytest.param(None, [5, 6], id="With alert IDs only"), ], ) def test_last_run_valid_states(self, start_time, last_alert_ids): """ Given: - Valid state parameters for HelloWorldLastRun. When: - Initializing HelloWorldLastRun. Then: - Assert model is created successfully with correct values. """ from HelloWorldV2 import HelloWorldLastRun last_run = HelloWorldLastRun(start_time=start_time, last_alert_ids=last_alert_ids) assert last_run.start_time == start_time assert last_run.last_alert_ids == last_alert_ids def test_last_run_default_values(self): """ Given: - No parameters provided. When: - Initializing HelloWorldLastRun. Then: - Assert default values are set correctly. """ from HelloWorldV2 import HelloWorldLastRun last_run = HelloWorldLastRun() assert last_run.start_time is None assert last_run.last_alert_ids == [] @pytest.mark.parametrize( "last_run,mock_alerts,expected_start_time,expected_alert_ids", [ pytest.param( HelloWorldLastRun(start_time=None, last_alert_ids=[]), [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ], "2026-01-15T00:00:01Z", [2], id="First fetch - no duplicates", ), pytest.param( HelloWorldLastRun(start_time="2026-01-15T00:00:00Z", last_alert_ids=[1, 2]), [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 3, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ], "2026-01-15T00:00:01Z", [3], id="Subsequent fetch - with duplicates filtered", ), pytest.param( HelloWorldLastRun(start_time="2026-01-15T00:00:00Z", last_alert_ids=[]), [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 3, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ], "2026-01-15T00:00:01Z", [1, 2, 3], id="Multiple alerts at same timestamp", ), ], ) def test_fetch_alerts_basic_flow( mocker: MockerFixture, last_run: HelloWorldLastRun, mock_alerts: list[dict], expected_start_time: str, expected_alert_ids: list[int], ): """ Given: - Valid client and last_run state. - Mock alerts from get_alert_list. When: - Running fetch_alerts. Then: - Assert get_alert_list is called with correct parameters. - Assert next_run state is correctly updated. - Assert latest alert IDs are tracked for deduplication. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock get_alert_list as AsyncMock mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts first_fetch_time = "2026-01-15T00:00:00Z" next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time=first_fetch_time, should_push=False, ) # Verify next_run state assert next_run.start_time == expected_start_time assert next_run.last_alert_ids == expected_alert_ids @pytest.mark.parametrize( "is_xsiam,expected_create_function", [ pytest.param(True, "create_events", id="XSIAM - create events"), pytest.param(False, "create_incidents", id="XSOAR - create incidents"), ], ) def test_fetch_alerts_xsiam_vs_xsoar(mocker: MockerFixture, is_xsiam: bool, expected_create_function: str): """ Given: - Valid client and last_run. When: - Running fetch_alerts with should_push=True on Cortex XSIAM and Cortex XSOAR tenants. Then: - Assert create_events is called for XSIAM. - Assert create_incidents is called for XSOAR. """ from HelloWorldV2 import fetch_alerts # Mock system capabilities mocker.patch("HelloWorldV2.CAN_SEND_EVENTS", is_xsiam) # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list to return alerts mocker.patch.object(client, "get_alert_list", return_value=mock_alerts) # Mock create functions mock_create_events = mocker.patch("HelloWorldV2.create_events") mock_create_incidents = mocker.patch("HelloWorldV2.create_incidents") # Execute fetch_alerts last_run = HelloWorldLastRun() fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-15T00:00:00Z", should_push=True, ) # Verify correct create function was called if expected_create_function == "create_events": assert mock_create_events.call_count >= 1 assert mock_create_incidents.call_count == 0 else: assert mock_create_incidents.call_count >= 1 assert mock_create_events.call_count == 0 def test_fetch_alerts_no_new_alerts(mocker: MockerFixture): """ Given: - Valid client and last_run. - get_alert_list returns empty list (no new alerts). When: - Running fetch_alerts. Then: - Assert last_run state is returned unchanged. - Assert no incidents/events are created. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock get_alert_list to return empty list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=[])) # Mock create functions mock_create_events = mocker.patch("HelloWorldV2.create_events") mock_create_incidents = mocker.patch("HelloWorldV2.create_incidents") # Execute fetch_alerts last_run = HelloWorldLastRun(start_time="2026-01-15T00:00:00Z", last_alert_ids=[1, 2]) next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-14T00:00:00Z", should_push=True, ) # Verify last_run is returned unchanged assert next_run.start_time == last_run.start_time assert next_run.last_alert_ids == last_run.last_alert_ids # Verify no create functions were called assert mock_create_events.call_count == 0 assert mock_create_incidents.call_count == 0 def test_fetch_alerts_deduplication(mocker: MockerFixture): """ Given: - Valid client and last_run with last_alert_ids. - get_alert_list returns alerts including duplicates. When: - Running fetch_alerts. Then: - Assert duplicate alerts are filtered out. - Assert only new alerts are processed. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts with some duplicates mock_alerts = [ { "id": 3, "severity": "high", "date": "2026-01-15T00:00:02Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 4, "severity": "high", "date": "2026-01-15T00:00:03Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts with last_alert_ids containing [1, 2] last_run = HelloWorldLastRun(start_time="2026-01-15T00:00:00Z", last_alert_ids=[1, 2]) next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-14T00:00:00Z", should_push=False, ) # Verify next_run contains new alert IDs assert next_run.start_time == "2026-01-15T00:00:03Z" assert next_run.last_alert_ids == [4] def test_fetch_alerts_first_fetch_uses_first_fetch_time(mocker: MockerFixture): """ Given: - Valid client and empty last_run (first fetch). - first_fetch_time parameter provided. When: - Running fetch_alerts. Then: - Assert first_fetch_time is used when last_run.start_time is None. - Assert get_alert_list is called with first_fetch_time. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mock_get_alert_list = mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts with empty last_run first_fetch_time = "2026-01-14T00:00:00Z" last_run = HelloWorldLastRun() fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time=first_fetch_time, should_push=False, ) # Verify get_alert_list was called with first_fetch_time # Note: asyncio.run is mocked, so we check the mock was called assert mock_get_alert_list.call_count == 1 @pytest.mark.parametrize( "severity", [ pytest.param(HelloWorldSeverity.LOW, id="Low severity"), pytest.param(HelloWorldSeverity.MEDIUM, id="Medium severity"), pytest.param(HelloWorldSeverity.HIGH, id="High severity"), pytest.param(HelloWorldSeverity.CRITICAL, id="Critical severity"), ], ) def test_fetch_alerts_different_severities(mocker: MockerFixture, severity: HelloWorldSeverity): """ Given: - Valid client and last_run. - Different severity levels. When: - Running fetch_alerts. Then: - Assert get_alert_list is called with correct severity. - Assert alerts are fetched for each severity level. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 1, "severity": severity.value, "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mock_get_alert_list = mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts last_run = HelloWorldLastRun() fetch_alerts( client, last_run, severity=severity, max_fetch=10, first_fetch_time="2026-01-15T00:00:00Z", should_push=False, ) # Verify get_alert_list was called assert mock_get_alert_list.call_count == 1 @pytest.mark.parametrize( "max_fetch,expected_limit", [ pytest.param(1, 1, id="Fetch 1 alert"), pytest.param(10, 10, id="Fetch 10 alerts"), pytest.param(100, 100, id="Fetch 100 alerts"), pytest.param(1000, 1000, id="Fetch 1000 alerts"), ], ) def test_fetch_alerts_max_fetch_limits(mocker: MockerFixture, max_fetch: int, expected_limit: int): """ Given: - Valid client and last_run. - Different max_fetch values. When: - Running fetch_alerts. Then: - Assert get_alert_list is called with correct limit. - Assert no more than max_fetch alerts are processed. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Generate mock alerts up to max_fetch mock_alerts = [ { "id": i, "severity": "high", "date": f"2026-01-15T00:00:{i:02d}Z", "user": "test@example.com", "action": "Testing", "status": "Success", } for i in range(1, min(max_fetch + 1, 101)) # Cap at 100 for test performance ] # Mock get_alert_list mock_get_alert_list = mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts last_run = HelloWorldLastRun() next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=max_fetch, first_fetch_time="2026-01-15T00:00:00Z", should_push=False, ) # Verify get_alert_list was called assert mock_get_alert_list.call_count == 1 # Verify next_run has correct number of alert IDs assert len(next_run.last_alert_ids) >= 1 def test_fetch_alerts_no_push(mocker: MockerFixture): """ Given: - Valid client and last_run. - should_push=False. When: - Running fetch_alerts. Then: - Assert get_alert_list is called. - Assert create_events/create_incidents are NOT called. - Assert next_run state is still updated correctly. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Mock create functions mock_create_events = mocker.patch("HelloWorldV2.create_events") mock_create_incidents = mocker.patch("HelloWorldV2.create_incidents") # Execute fetch_alerts with should_push=False last_run = HelloWorldLastRun() next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-15T00:00:00Z", should_push=False, ) # Verify create functions were NOT called assert mock_create_events.call_count == 0 assert mock_create_incidents.call_count == 0 # Verify next_run is still updated assert next_run.start_time == "2026-01-15T00:00:00Z" assert next_run.last_alert_ids == [1] def test_fetch_alerts_multiple_alerts_same_timestamp(mocker: MockerFixture): """ Given: - Valid client and last_run. - Multiple alerts with the same timestamp. When: - Running fetch_alerts. Then: - Assert all alert IDs at the latest timestamp are tracked. - Assert next_run.last_alert_ids contains all IDs from latest timestamp. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts - multiple at same timestamp same_timestamp = "2026-01-15T00:00:05Z" mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 3, "severity": "high", "date": same_timestamp, "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 4, "severity": "high", "date": same_timestamp, "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 5, "severity": "high", "date": same_timestamp, "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts last_run = HelloWorldLastRun() next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-15T00:00:00Z", should_push=False, ) # Verify all IDs at the latest timestamp are tracked assert next_run.start_time == same_timestamp assert set(next_run.last_alert_ids) == {3, 4, 5} assert len(next_run.last_alert_ids) == 3 def test_fetch_alerts_uses_last_run_start_time(mocker: MockerFixture): """ Given: - Valid client and last_run with existing start_time. - first_fetch_time parameter provided. When: - Running fetch_alerts. Then: - Assert last_run.start_time is used instead of first_fetch_time. - Assert get_alert_list is called with last_run.start_time. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 10, "severity": "high", "date": "2026-01-16T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mock_get_alert_list = mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts with existing last_run last_run_start_time = "2026-01-15T12:00:00Z" last_run = HelloWorldLastRun(start_time=last_run_start_time, last_alert_ids=[8, 9]) first_fetch_time = "2026-01-14T00:00:00Z" # Should be ignored fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time=first_fetch_time, should_push=False, ) # Verify get_alert_list was called (asyncio.run is mocked) assert mock_get_alert_list.call_count == 1 def test_fetch_alerts_empty_last_alert_ids(mocker: MockerFixture): """ Given: - Valid client and last_run with empty last_alert_ids. When: - Running fetch_alerts. Then: - Assert all alerts are processed (no deduplication). - Assert next_run.last_alert_ids is populated. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alerts mock_alerts = [ { "id": 1, "severity": "high", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, { "id": 2, "severity": "high", "date": "2026-01-15T00:00:01Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts last_run = HelloWorldLastRun(start_time="2026-01-15T00:00:00Z", last_alert_ids=[]) next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.HIGH, max_fetch=10, first_fetch_time="2026-01-14T00:00:00Z", should_push=False, ) # Verify next_run has alert IDs assert next_run.last_alert_ids == [2] def test_fetch_alerts_single_alert(mocker: MockerFixture): """ Given: - Valid client and last_run. - get_alert_list returns single alert. When: - Running fetch_alerts. Then: - Assert next_run state is updated with single alert. - Assert last_alert_ids contains only that alert ID. """ from HelloWorldV2 import fetch_alerts # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock single alert mock_alerts = [ { "id": 42, "severity": "critical", "date": "2026-01-15T00:00:00Z", "user": "test@example.com", "action": "Testing", "status": "Success", }, ] # Mock get_alert_list mocker.patch("HelloWorldV2.get_alert_list", new=AsyncMock(return_value=mock_alerts)) # Execute fetch_alerts last_run = HelloWorldLastRun() next_run = fetch_alerts( client, last_run, severity=HelloWorldSeverity.CRITICAL, max_fetch=1, first_fetch_time="2026-01-15T00:00:00Z", should_push=False, ) # Verify next_run assert next_run.start_time == "2026-01-15T00:00:00Z" assert next_run.last_alert_ids == [42] # endregion # region fetch-assets class TestHelloWorldAssetsLastRun: """Tests for HelloWorldAssetsLastRun model.""" @pytest.mark.parametrize( "stage,id_offset,cumulative_count,snapshot_id,next_trigger", [ pytest.param("assets", 0, 0, None, None, id="Initial state - assets stage"), pytest.param("vulnerabilities", 100, 50, "1234567890", "1", id="Vulnerabilities stage with data"), pytest.param("assets", 500, 250, "9876543210", None, id="Assets stage mid-fetch"), ], ) def test_assets_last_run_valid_states(self, stage, id_offset, cumulative_count, snapshot_id, next_trigger): """ Given: - Valid state parameters for HelloWorldAssetsLastRun. When: - Initializing HelloWorldAssetsLastRun. Then: - Assert model is created successfully with correct values. """ last_run = HelloWorldAssetsLastRun( stage=stage, id_offset=id_offset, cumulative_count=cumulative_count, snapshot_id=snapshot_id, nextTrigger=next_trigger, ) assert last_run.stage == FetchAssetsStages(stage) assert last_run.id_offset == id_offset assert last_run.cumulative_count == cumulative_count assert last_run.snapshot_id == snapshot_id assert last_run.next_trigger_in_seconds == next_trigger def test_assets_last_run_default_values(self): """ Given: - No parameters provided. When: - Initializing HelloWorldAssetsLastRun. Then: - Assert default values are set correctly. """ last_run = HelloWorldAssetsLastRun() assert last_run.stage == FetchAssetsStages.ASSETS assert last_run.id_offset == 0 assert last_run.cumulative_count == 0 assert last_run.snapshot_id is None assert last_run.next_trigger_in_seconds is None assert last_run.trigger_type == 1 @pytest.mark.parametrize( "assets_last_run,mock_assets_response,mock_vulns_response,expected_stage,expected_offset,expected_cumulative,expected_trigger", [ pytest.param( HelloWorldAssetsLastRun(stage=FetchAssetsStages.ASSETS, id_offset=0, cumulative_count=0, snapshot_id=None), { "has_more": False, "data": [{"id": 1, "name": "Server-01", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"}], }, None, "vulnerabilities", 0, 0, "1", id="First assets batch - no more data, move to vulnerabilities", ), pytest.param( HelloWorldAssetsLastRun(stage=FetchAssetsStages.ASSETS, id_offset=0, cumulative_count=0, snapshot_id=None), { "has_more": True, "data": [ {"id": i, "name": f"Server-{i:02d}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(1, 11) ], }, None, "assets", 10, 10, "1", id="First assets batch - has more data, continue assets", ), pytest.param( HelloWorldAssetsLastRun( stage=FetchAssetsStages.ASSETS, id_offset=1000, cumulative_count=1000, snapshot_id="1234567890" ), { "has_more": True, "data": [ {"id": i, "name": f"Server-{i:02d}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(1001, 1011) ], }, None, "assets", 1010, 1010, "1", id="Mid-fetch assets - has more data, continue pagination", ), pytest.param( HelloWorldAssetsLastRun(stage=FetchAssetsStages.VULNS, id_offset=0, cumulative_count=0, snapshot_id="1234567890"), None, { "has_more": False, "data": [ { "id": 1, "cve_id": "CVE-MOCK-0001", "severity": "critical", "description": "Test vuln", "published": "2026-01-15T00:00:00", } ], }, "assets", 0, 0, None, id="Vulnerabilities batch - no more data, complete cycle back to assets", ), pytest.param( HelloWorldAssetsLastRun(stage=FetchAssetsStages.VULNS, id_offset=0, cumulative_count=0, snapshot_id="1234567890"), None, { "has_more": True, "data": [ { "id": i, "cve_id": f"CVE-MOCK-{i:04d}", "severity": "high", "description": "Test", "published": "2026-01-15T00:00:00", } for i in range(1, 11) ], }, "vulnerabilities", 10, 10, "1", id="Vulnerabilities batch - has more data, continue vulnerabilities", ), ], ) def test_fetch_assets_stage_transitions( mocker: MockerFixture, assets_last_run: HelloWorldAssetsLastRun, mock_assets_response: dict | None, mock_vulns_response: dict | None, expected_stage: str, expected_offset: int, expected_cumulative: int, expected_trigger: str | None, ): """ Given: - Valid client and last_run state at different stages. - Mock API responses with varying has_more flags. When: - Running fetch_assets. Then: - Assert correct client method is called based on stage. - Assert send_data_to_xsiam is called with correct parameters. - Assert next_run state is correctly updated based on has_more flag. - Assert stage transitions happen correctly. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock client methods mock_get_assets = mocker.patch.object(client, "get_assets", return_value=mock_assets_response) mock_get_vulnerabilities = mocker.patch.object(client, "get_vulnerabilities", return_value=mock_vulns_response) # Mock send_data_to_xsiam mock_send_data = mocker.patch("HelloWorldV2.send_data_to_xsiam") # Mock generate_unix_timestamp to return consistent value mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="9999999999") # Execute fetch_assets next_run = fetch_assets(client, assets_last_run, should_push=True) # Assertions - verify correct method was called based on stage if assets_last_run.stage is FetchAssetsStages.ASSETS: assert mock_get_assets.call_count == 1 assert mock_get_assets.call_args.kwargs == {"limit": 1000, "id_offset": assets_last_run.id_offset} assert mock_get_vulnerabilities.call_count == 0 response = mock_assets_response or {} else: assert mock_get_vulnerabilities.call_count == 1 assert mock_get_vulnerabilities.call_args.kwargs == {"limit": 1000, "id_offset": assets_last_run.id_offset} assert mock_get_assets.call_count == 0 response = mock_vulns_response or {} # Verify send_data_to_xsiam was called assert mock_send_data.call_count == 1 call_kwargs = mock_send_data.call_args.kwargs # Verify data sent assert call_kwargs["data"] == response["data"] # Verify items_count and should_update_health_module based on has_more has_more = response["has_more"] batch_count = len(response["data"]) if has_more: assert call_kwargs["items_count"] == 1 assert call_kwargs["should_update_health_module"] is False else: expected_items_count = assets_last_run.cumulative_count + batch_count assert call_kwargs["items_count"] == expected_items_count assert call_kwargs["should_update_health_module"] is True # Verify next_run state assert next_run.stage == FetchAssetsStages(expected_stage) assert next_run.id_offset == expected_offset assert next_run.cumulative_count == expected_cumulative assert next_run.next_trigger_in_seconds == expected_trigger @pytest.mark.parametrize( "stage,has_more", [ pytest.param(FetchAssetsStages.ASSETS, True, id="Assets stage - has more - retain snapshot"), pytest.param(FetchAssetsStages.ASSETS, False, id="Assets stage - no more - new snapshot"), pytest.param(FetchAssetsStages.VULNS, True, id="Vulnerabilities stage - has more - retain snapshot"), pytest.param(FetchAssetsStages.VULNS, False, id="Vulnerabilities stage - no more - new snapshot"), ], ) def test_fetch_assets_snapshot_id_management(mocker: MockerFixture, stage: FetchAssetsStages, has_more: bool): """ Given: - Valid client and last_run with existing snapshot_id. - Mock API responses with varying has_more flags. When: - Running fetch_assets. Then: - Assert snapshot_id is retained when has_more=True. - Assert new snapshot_id is generated when has_more=False. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run with existing snapshot_id original_snapshot_id = "1234567890" last_run = HelloWorldAssetsLastRun(stage=stage, id_offset=0, cumulative_count=0, snapshot_id=original_snapshot_id) # Mock response mock_response = { "has_more": has_more, "data": [{"id": 1, "name": "Test", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"}], } # Mock client methods if stage is FetchAssetsStages.ASSETS: mocker.patch.object(client, "get_assets", return_value=mock_response) else: mocker.patch.object(client, "get_vulnerabilities", return_value=mock_response) # Mock send_data_to_xsiam mocker.patch("HelloWorldV2.send_data_to_xsiam") # Mock generate_unix_timestamp to return new value new_snapshot_id = "9999999999" mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value=new_snapshot_id) # Execute fetch_assets next_run = fetch_assets(client, last_run, should_push=True) # Verify snapshot_id behavior assert next_run.snapshot_id == original_snapshot_id if has_more else new_snapshot_id def test_fetch_assets_empty_response(mocker: MockerFixture): """ Given: - Valid client and last_run. - Mock API response with empty data array. When: - Running fetch_assets. Then: - Assert function handles empty data gracefully. - Assert send_data_to_xsiam is called with empty array. - Assert next_run state is updated correctly. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run last_run = HelloWorldAssetsLastRun(stage="assets", id_offset=0, cumulative_count=0) # Mock response with empty data mock_response = {"has_more": False, "data": []} # Mock client methods mocker.patch.object(client, "get_assets", return_value=mock_response) # Mock send_data_to_xsiam mock_send_data = mocker.patch("HelloWorldV2.send_data_to_xsiam") # Mock generate_unix_timestamp mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="1234567890") # Execute fetch_assets next_run = fetch_assets(client, last_run, should_push=True) # Assertions assert mock_send_data.call_count == 1 assert mock_send_data.call_args.kwargs["data"] == [] assert mock_send_data.call_args.kwargs["items_count"] == 0 assert next_run.stage.value == "vulnerabilities" assert next_run.id_offset == 0 def test_fetch_assets_no_push(mocker: MockerFixture): """ Given: - Valid client and last_run. - should_push=False. When: - Running fetch_assets. Then: - Assert client method is called. - Assert send_data_to_xsiam is NOT called. - Assert next_run state is still updated correctly. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run last_run = HelloWorldAssetsLastRun(stage="assets", id_offset=0, cumulative_count=0) # Mock response mock_response = util_load_json("test_data/assets.json") # Mock client methods mock_get_assets = mocker.patch.object(client, "get_assets", return_value=mock_response) # Mock send_data_to_xsiam mock_send_data = mocker.patch("HelloWorldV2.send_data_to_xsiam") # Mock generate_unix_timestamp mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="1234567890") # Execute fetch_assets with should_push=False next_run = fetch_assets(client, last_run, should_push=False) # Assertions assert mock_get_assets.call_count == 1 assert mock_send_data.call_count == 0 # Should NOT be called assert next_run.stage.value == "vulnerabilities" @pytest.mark.parametrize( "initial_offset,batch_size,has_more,expected_next_offset", [ pytest.param(0, 10, True, 10, id="First batch - has more"), pytest.param(100, 50, True, 150, id="Mid-fetch - has more"), pytest.param(5000, 100, True, 5100, id="Large offset - has more"), pytest.param(0, 10, False, 0, id="First batch - no more (reset)"), pytest.param(1000, 50, False, 0, id="Mid-fetch - no more (reset)"), ], ) def test_fetch_assets_offset_calculation( mocker: MockerFixture, initial_offset: int, batch_size: int, has_more: bool, expected_next_offset: int ): """ Given: - Valid client and last_run with various offset values. - Mock API responses with different batch sizes and has_more flags. When: - Running fetch_assets. Then: - Assert offset is correctly incremented when has_more=True. - Assert offset is reset to 0 when has_more=False. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run last_run = HelloWorldAssetsLastRun( stage="assets", id_offset=initial_offset, cumulative_count=initial_offset, snapshot_id="1234567890" ) # Mock response with specified batch size mock_data = [ {"id": i, "name": f"Asset-{i}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(initial_offset + 1, initial_offset + batch_size + 1) ] mock_response = {"has_more": has_more, "data": mock_data} # Mock client methods mocker.patch.object(client, "get_assets", return_value=mock_response) mocker.patch("HelloWorldV2.send_data_to_xsiam") mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="9999999999") # Execute fetch_assets next_run = fetch_assets(client, last_run, should_push=True) # Verify offset assert next_run.id_offset == expected_next_offset def test_fetch_assets_cumulative_count_tracking(mocker: MockerFixture): """ Given: - Valid client and last_run with existing cumulative_count. - Mock API response with has_more=True. When: - Running fetch_assets. Then: - Assert cumulative_count is correctly incremented. - Assert cumulative_count is reset when has_more=False. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Test case 1: has_more=True, cumulative should increment last_run = HelloWorldAssetsLastRun(stage="assets", id_offset=100, cumulative_count=100, snapshot_id="1234567890") mock_response = { "has_more": True, "data": [ {"id": i, "name": f"Asset-{i}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(101, 111) ], } mocker.patch.object(client, "get_assets", return_value=mock_response) mocker.patch("HelloWorldV2.send_data_to_xsiam") mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="9999999999") next_run = fetch_assets(client, last_run, should_push=True) # Verify cumulative_count incremented assert next_run.cumulative_count == 110 # Test case 2: has_more=False, cumulative should reset last_run2 = HelloWorldAssetsLastRun(stage="assets", id_offset=200, cumulative_count=200, snapshot_id="1234567890") mock_response2 = { "has_more": False, "data": [ {"id": i, "name": f"Asset-{i}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(201, 206) ], } mocker.patch.object(client, "get_assets", return_value=mock_response2) next_run2 = fetch_assets(client, last_run2, should_push=True) # Verify cumulative_count reset assert next_run2.cumulative_count == 0 def test_fetch_assets_vendor_product_dataset_selection(mocker: MockerFixture): """ Given: - Valid client and last_run at different stages. When: - Running fetch_assets. Then: - Assert correct vendor/product is used for assets stage. - Assert correct vendor/product is used for vulnerabilities stage. """ from HelloWorldV2 import ( fetch_assets, AssetsDatasetConfigs, VulnerabilitiesDatasetConfigs, ) # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Test assets stage last_run_assets = HelloWorldAssetsLastRun(stage="assets", id_offset=0, cumulative_count=0) mock_assets_response = util_load_json("test_data/assets.json") mocker.patch.object(client, "get_assets", return_value=mock_assets_response) mock_send_data = mocker.patch("HelloWorldV2.send_data_to_xsiam") mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="1234567890") fetch_assets(client, last_run_assets, should_push=True) # Verify vendor/product for assets assert mock_send_data.call_args.kwargs["vendor"] == AssetsDatasetConfigs.VENDOR.value assert mock_send_data.call_args.kwargs["product"] == AssetsDatasetConfigs.PRODUCT.value # Test vulnerabilities stage last_run_vulns = HelloWorldAssetsLastRun(stage="vulnerabilities", id_offset=0, cumulative_count=0, snapshot_id="1234567890") mock_vulns_response = util_load_json("test_data/vulnerabilities.json") mocker.patch.object(client, "get_vulnerabilities", return_value=mock_vulns_response) mock_send_data.reset_mock() fetch_assets(client, last_run_vulns, should_push=True) # Verify vendor/product for vulnerabilities assert mock_send_data.call_args.kwargs["vendor"] == VulnerabilitiesDatasetConfigs.VENDOR.value assert mock_send_data.call_args.kwargs["product"] == VulnerabilitiesDatasetConfigs.PRODUCT.value def test_fetch_assets_initial_snapshot_generation(mocker: MockerFixture): """ Given: - Valid client and last_run with no snapshot_id (first run). When: - Running fetch_assets. Then: - Assert generate_unix_timestamp is called to create new snapshot_id. - Assert snapshot_id is included in send_data_to_xsiam call. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run with no snapshot_id (first run) last_run = HelloWorldAssetsLastRun(stage=FetchAssetsStages.ASSETS, id_offset=0, cumulative_count=0, snapshot_id=None) # Mock response mock_response = util_load_json("test_data/assets.json") mock_response["has_more"] = True # assume has_more to ensure current snapshot_id is persisted # Mock client methods mocker.patch.object(client, "get_assets", return_value=mock_response) mock_send_data_to_xsiam = mocker.patch("HelloWorldV2.send_data_to_xsiam") # Mock generate_unix_timestamp expected_snapshot_id = "1234567890" mock_generate_timestamp = mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value=expected_snapshot_id) # Execute fetch_assets fetch_assets(client, last_run, should_push=True) # Verify generate_unix_timestamp was NOT called (snapshot_id generated inline) # The function generates snapshot_id using: last_run.snapshot_id or generate_unix_timestamp() # So it should be called once since response["has_more"] == True assert mock_generate_timestamp.call_count == 1 # Verify snapshot_id is used in send_data_to_xsiam assert mock_send_data_to_xsiam.call_args.kwargs["snapshot_id"] == expected_snapshot_id def test_fetch_assets_large_batch_pagination(mocker: MockerFixture): """ Given: - Valid client and last_run. - Mock API response with exactly 1000 items (batch limit). When: - Running fetch_assets. Then: - Assert function handles large batches correctly. - Assert offset is incremented by batch size. """ from HelloWorldV2 import fetch_assets # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Create last_run last_run = HelloWorldAssetsLastRun(stage="assets", id_offset=0, cumulative_count=0) # Mock response with 1000 items mock_data = [ {"id": i, "name": f"Asset-{i:04d}", "type": "server", "status": "active", "created": "2024-01-15T00:00:00"} for i in range(1, 1001) ] mock_response = {"has_more": True, "data": mock_data} # Mock client methods mocker.patch.object(client, "get_assets", return_value=mock_response) mocker.patch("HelloWorldV2.send_data_to_xsiam") mocker.patch("HelloWorldV2.generate_unix_timestamp", return_value="1234567890") # Execute fetch_assets next_run = fetch_assets(client, last_run, should_push=True) # Verify offset incremented by 1000 assert next_run.id_offset == 1000 assert next_run.cumulative_count == 1000 # endregion # region ip reputation class TestIpArgs: @pytest.mark.parametrize( "ip_input", [ pytest.param("8.8.8.8", id="Single IPv4"), pytest.param("2001:4860:4860::8888", id="Single IPv6"), pytest.param(["8.8.8.8", "1.1.1.1"], id="Multiple IPv4"), ], ) def test_ip_args_valid_ips(self, ip_input, mocker): """ Given: - Valid IP address(es). When: - Initializing IpArgs and accessing ips property. Then: - Assert model is created successfully. - Assert ips property returns valid IPs. """ from HelloWorldV2 import IpArgs # Mock demisto.error to avoid actual logging mocker.patch("HelloWorldV2.demisto.error") args = IpArgs(ip=ip_input) assert args.ip == ip_input # The ips property validates IPs valid_ips = args.ips assert len(valid_ips) > 0 def test_ip_args_missing_ip(self): """ Given: - No IP provided. When: - Initializing IpArgs. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import IpArgs with pytest.raises(DemistoException, match="ip"): IpArgs() # type: ignore[call-arg] def test_ip_args_all_invalid_ips(self, mocker): """ Given: - Only invalid IP addresses. When: - Initializing IpArgs and accessing ips property. Then: - Assert ValueError is raised when accessing ips property. """ from HelloWorldV2 import IpArgs # Mock demisto.error to avoid actual logging mocker.patch("HelloWorldV2.demisto.error") args = IpArgs(ip=["not-an-ip", "also-invalid"]) with pytest.raises(ValueError): _ = args.ips @pytest.mark.parametrize( "ip,threshold,expected_score,expected_reputation", [ pytest.param("8.8.8.8", 65, Common.DBotScore.NONE, 0, id="Bad IP - reputation below threshold/2"), pytest.param("1.1.1.1", 65, Common.DBotScore.SUSPICIOUS, 50, id="Suspicious IP - reputation below threshold"), pytest.param("151.1.1.1", 65, Common.DBotScore.GOOD, 70, id="Good IP - reputation above threshold"), pytest.param("192.168.1.1", 65, Common.DBotScore.NONE, 0, id="Unknown IP - reputation is 0"), pytest.param("10.0.0.1", 30, Common.DBotScore.SUSPICIOUS, 20, id="Custom threshold - suspicious"), ], ) def test_ip_reputation_command(mocker: MockerFixture, ip: str, threshold: int, expected_score: int, expected_reputation: int): """ Given: - Valid client, args with different IP addresses and thresholds. - Mock IP reputation data with varying reputation scores. When: - Running ip_reputation_command. Then: - Assert client.get_ip_reputation is called once with the provided IP. - Assert CommandResults is returned with correct DBotScore, IP context, and readable output. - Assert the reputation score is correctly mapped to DBotScore (0-3). """ from HelloWorldV2 import ip_reputation_command, IpArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), threshold_ip=threshold, ) client = HelloWorldClient(params) # Load base mock data and update with test-specific values mock_ip_data = util_load_json("test_data/ip_reputation.json") mock_ip_data["attributes"]["reputation"] = expected_reputation mock_ip_data["id"] = ip mock_ip_data["links"]["self"] = f"https://www.virustotal.com/api/v3/ip_addresses/{ip}" mock_get_ip_reputation = mocker.patch.object(client, "get_ip_reputation", return_value=mock_ip_data) # Create args args = IpArgs(ip=ip, threshold=threshold) # Execute command results = ip_reputation_command(client, args, params) # Assertions assert mock_get_ip_reputation.call_count == 1 assert mock_get_ip_reputation.call_args.args[0] == ip # Verify we got a list with one CommandResults assert len(results) == 1 # Verify outputs assert results[0].outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.IP" assert results[0].outputs_key_field == "ip" assert results[0].outputs["ip"] == ip assert "attributes" not in results[0].outputs # Should be excluded assert "whois" not in results[0].outputs # Should be excluded # Verify indicator (IP standard context) assert results[0].indicator is not None assert results[0].indicator.ip == ip assert results[0].indicator.dbot_score.score == expected_score assert results[0].indicator.dbot_score.indicator == ip assert results[0].indicator.dbot_score.indicator_type == "ip" assert results[0].indicator.dbot_score.integration_name == "HelloWorld" # Verify readable output contains IP data assert ip in results[0].readable_output assert "Attributes" in results[0].readable_output @pytest.mark.parametrize( "ips", [ pytest.param(["8.8.8.8", "1.1.1.1"], id="Multiple IPs"), pytest.param(["192.168.1.1", "10.0.0.1", "172.16.0.1"], id="Three IPs"), ], ) def test_ip_reputation_command_multiple_ips(mocker: MockerFixture, ips: list[str]): """ Given: - Valid client and args with multiple IP addresses. When: - Running ip_reputation_command. Then: - Assert client.get_ip_reputation is called once for each IP. - Assert a list of CommandResults is returned with one result per IP. """ from HelloWorldV2 import ip_reputation_command, IpArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock client.get_ip_reputation to return different data for each IP def mock_get_reputation(ip: str): return { "attributes": { "as_owner": "TEST", "asn": 12345, "reputation": 50, }, "id": ip, "links": {"self": f"https://example.com/{ip}"}, "type": "ip_address", } mock_get_ip_reputation = mocker.patch.object(client, "get_ip_reputation", side_effect=mock_get_reputation) # Create args args = IpArgs(ip=ips) # Execute command results = ip_reputation_command(client, args, params) # Assertions assert mock_get_ip_reputation.call_count == len(ips) assert len(results) == len(ips) # Verify each result corresponds to the correct IP for i, result in enumerate(results): assert result.outputs["ip"] == ips[i] assert result.indicator.ip == ips[i] def test_ip_reputation_command_with_relationships(mocker: MockerFixture): """ Given: - Valid client and args. - Mock IP reputation data with relationship links. When: - Running ip_reputation_command. Then: - Assert EntityRelationship objects are created for related URLs. - Assert relationships are included in CommandResults. """ from HelloWorldV2 import ip_reputation_command, IpArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock data with relationships ip = "8.8.8.8" related_url = "https://example.com/related" mock_ip_data = util_load_json("test_data/ip_reputation_with_relationships.json") mocker.patch.object(client, "get_ip_reputation", return_value=mock_ip_data) # Create args args = IpArgs(ip=ip) # Execute command results = ip_reputation_command(client, args, params) # Assertions assert len(results) == 1 result = results[0] # Verify relationships assert result.relationships is not None assert len(result.relationships) == 1 relationship = result.relationships[0] assert relationship._entity_a == ip assert relationship._entity_b == related_url assert relationship._name == "related-to" def test_ip_reputation_command_threshold_override(mocker: MockerFixture): """ Given: - Valid client and args with custom threshold in args. - Params with different default threshold. When: - Running ip_reputation_command. Then: - Assert args threshold is used instead of params threshold. - Assert DBotScore is calculated using args threshold. """ from HelloWorldV2 import ip_reputation_command, IpArgs # Create params with default threshold params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), threshold_ip=100, # High default threshold ) client = HelloWorldClient(params) # Mock client.get_ip_reputation ip = "8.8.8.8" reputation = 40 # Would be good with threshold=100, but suspicious with threshold=50 mock_ip_data = { "attributes": { "reputation": reputation, }, "id": ip, "links": {"self": ""}, "type": "ip_address", } mocker.patch.object(client, "get_ip_reputation", return_value=mock_ip_data) # Create args with custom threshold custom_threshold = 50 args = IpArgs(ip=ip, threshold=custom_threshold) # Execute command results = ip_reputation_command(client, args, params) # Assertions result = results[0] # With threshold=50, reputation=40 should be suspicious (score=2) # because 40 < 50 but 40 >= 50/2 (25) assert result.indicator.dbot_score.score == Common.DBotScore.SUSPICIOUS # endregion # region helloworld-alert-list class TestHelloworldAlertListArgs: @pytest.mark.parametrize( "alert_id,severity", [ pytest.param(123, None, id="With alert_id only"), pytest.param(None, "high", id="With severity only"), ], ) def test_alert_list_args_valid_combinations(self, alert_id, severity): """ Given: - Either alert_id or severity (but not both). When: - Initializing HelloworldAlertListArgs. Then: - Assert model is created successfully. """ from HelloWorldV2 import HelloworldAlertListArgs args = HelloworldAlertListArgs(alert_id=alert_id, severity=severity) assert args.alert_id == alert_id assert args.severity == severity @pytest.mark.parametrize( "alert_id,severity", [ pytest.param(None, None, id="Neither provided"), pytest.param(123, "high", id="Both provided"), ], ) def test_alert_list_args_invalid_combinations(self, alert_id, severity): """ Given: - Either both alert_id and severity, or neither. When: - Initializing HelloworldAlertListArgs. Then: - Assert DemistoException is raised with appropriate message. """ from HelloWorldV2 import HelloworldAlertListArgs with pytest.raises(DemistoException, match="Either 'alert_id' or 'severity' arguments need to be provided."): HelloworldAlertListArgs(alert_id=alert_id, severity=severity) @pytest.mark.parametrize( "alert_id,severity,limit,expected_method", [ pytest.param(123, None, 10, "get_alert", id="With alert_id"), pytest.param(None, HelloWorldSeverity.HIGH, 10, "get_alert_list", id="With severity"), ], ) def test_alert_list_command( mocker: MockerFixture, alert_id: int | None, severity: HelloWorldSeverity | None, limit: int, expected_method: str ): """ Given: - Valid client and args with either alert_id or severity. When: - Running alert_list_command. Then: - Assert correct client method is called (get_alert for alert_id, get_alert_list for severity). - Assert tableToMarkdown is called with correct args. - Assert CommandResults is returned with correct outputs. """ from HelloWorldV2 import alert_list_command, HelloworldAlertListArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Mock alert data mock_alert = { "id": alert_id or 1, "severity": severity.value if severity else "high", "user": "test@example.com", "action": "Testing", "date": "2026-01-15T00:00:00", "status": "Success", } # Mock the appropriate client method if expected_method == "get_alert": mock_get_alert = mocker.patch.object(client, "get_alert", return_value=mock_alert) mock_get_alert_list = mocker.patch.object(client, "get_alert_list") else: # get_alert_list mock_get_alert = mocker.patch.object(client, "get_alert") mock_get_alert_list = mocker.patch.object(client, "get_alert_list", return_value=[mock_alert]) # Mock tableToMarkdown mock_table_to_markdown = mocker.patch("HelloWorldV2.tableToMarkdown") # Create args args = HelloworldAlertListArgs(alert_id=alert_id, severity=severity, limit=limit) # Execute command result = alert_list_command(client, args) # Assertions - verify correct method was called if expected_method == "get_alert": assert mock_get_alert.call_count == 1 assert mock_get_alert.call_args.args == (alert_id,) assert mock_get_alert_list.call_count == 0 else: # get_alert_list assert mock_get_alert_list.call_count == 1 assert mock_get_alert_list.call_args.kwargs == {"limit": limit, "severity": severity} assert mock_get_alert.call_count == 0 # Verify tableToMarkdown was called assert mock_table_to_markdown.call_count == 1 assert mock_table_to_markdown.call_args.args[0] == "Items List (Sample Data)" assert isinstance(mock_table_to_markdown.call_args.args[1], list) # Verify CommandResults assert result.outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.Alert" assert result.outputs_key_field == "id" assert isinstance(result.outputs, list) # endregion # region helloworld-get-events class TestHelloWorldGetEventsArgs: @pytest.mark.parametrize( "limit,should_push_events", [ pytest.param(10, False, id="Defaults"), pytest.param(100, True, id="With limit and push"), pytest.param(50, False, id="With limit no push"), ], ) def test_get_events_args_valid(self, mocker: MockerFixture, limit: int, should_push_events: bool): """ Given: - Optional limit and should_push_events values. When: - Initializing HelloWorldGetEventsArgs. Then: - Assert model is created successfully with correct values. """ from HelloWorldV2 import HelloWorldGetEventsArgs mocker.patch("HelloWorldV2.CAN_SEND_EVENTS", True) args = HelloWorldGetEventsArgs(limit=limit, severity=HelloWorldSeverity.HIGH, should_push_events=should_push_events) assert args.limit == limit assert args.should_push_events == should_push_events def test_get_events_command(mocker: MockerFixture): """ Given: - Valid client and args for get_events_command. When: - Running get_events_command. Then: - Assert get_alert_list is called once with correct parameters. - Assert tableToMarkdown is called with correct args/kwargs. - Assert CommandResults is returned with correct readable output. """ from HelloWorldV2 import get_events_command, HelloWorldGetEventsArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock alert data mock_events = util_load_json("test_data/alert_events.json") mock_get_alert_list = mocker.patch("HelloWorldV2.get_alert_list", return_value=mock_events) mock_table_to_markdown = mocker.patch("HelloWorldV2.tableToMarkdown") args = HelloWorldGetEventsArgs(severity=HelloWorldSeverity.HIGH, limit=10, should_push_events=False) # Execute command get_events_command(client, args) # Assertions assert mock_get_alert_list.call_count == 1 # Verify the call was made with correct parameters assert mock_get_alert_list.call_args.kwargs == { "client": client, "start_time": None, "severity": HelloWorldSeverity.HIGH, "limit": 10, "should_push": False, } # Verify tableToMarkdown was called with correct args assert mock_table_to_markdown.call_count == 1 assert mock_table_to_markdown.call_args.args == ("HelloWorld Events", mock_events) # endregion # region helloworld-alert-note-create class TestHelloworldAlertNoteCreateArgs: @pytest.mark.parametrize( "alert_id,note_text", [ pytest.param(1, "Simple note", id="Simple note"), pytest.param(999, "Note with special chars: !@#$%", id="Special chars"), pytest.param(42, "Very long note " * 50, id="Long note"), ], ) def test_alert_note_create_args_valid(self, alert_id, note_text): """ Given: - A positive alert_id and note_text. When: - Initializing HelloworldAlertNoteCreateArgs. Then: - Assert model is created successfully. """ from HelloWorldV2 import HelloworldAlertNoteCreateArgs args = HelloworldAlertNoteCreateArgs(alert_id=alert_id, note_text=note_text) assert args.alert_id == alert_id assert args.note_text == note_text @pytest.mark.parametrize( "alert_id", [ pytest.param(0, id="Zero alert_id"), pytest.param(-1, id="Negative alert_id"), pytest.param(-999, id="Large negative alert_id"), ], ) def test_alert_note_create_args_invalid_alert_id(self, alert_id): """ Given: - A non-positive alert_id. When: - Initializing HelloworldAlertNoteCreateArgs. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloworldAlertNoteCreateArgs with pytest.raises(DemistoException, match="Please provide a valid 'alert_id' argument"): HelloworldAlertNoteCreateArgs(alert_id=alert_id, note_text="Test note") def test_alert_note_create_args_missing_note_text(self): """ Given: - No note_text provided. When: - Initializing HelloworldAlertNoteCreateArgs. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloworldAlertNoteCreateArgs with pytest.raises(DemistoException, match="note_text"): HelloworldAlertNoteCreateArgs(alert_id=1) # type: ignore[call-arg] def test_alert_note_create_command(mocker: MockerFixture): """ Given: - Valid client and args for alert_note_create_command. When: - Running alert_note_create_command. Then: - Assert client.create_note is called once with correct parameters. - Assert CommandResults is returned with correct outputs and readable output. """ from HelloWorldV2 import alert_note_create_command, HelloworldAlertNoteCreateArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock response data alert_id = 123 note_text = "This is a test note" mock_response = util_load_json("test_data/note_create_response.json") # Mock client.create_note mock_create_note = mocker.patch.object(client, "create_note", return_value=mock_response) # Create args args = HelloworldAlertNoteCreateArgs(alert_id=alert_id, note_text=note_text) # Execute command result = alert_note_create_command(client, args) # Assertions assert mock_create_note.call_count == 1 assert mock_create_note.call_args.kwargs == {"alert_id": alert_id, "comment": note_text} # Verify CommandResults assert result.outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.Note" assert result.outputs_key_field == "id" assert result.outputs == mock_response assert result.readable_output == "Note was created successfully." # endregion # region helloworld-job-submit class TestHelloWorldJobSubmitArgs: @pytest.mark.parametrize( "interval,timeout", [ pytest.param(30, 600, id="Defaults"), pytest.param("30", "300", id="String numbers"), ], ) def test_job_submit_args_valid(self, interval, timeout): """ Given: - Optional interval_in_seconds and timeout_in_seconds values. When: - Initializing HelloWorldJobSubmitArgs. Then: - Assert model is created successfully. - Assert values are converted to integers. """ from HelloWorldV2 import HelloWorldJobSubmitArgs args = HelloWorldJobSubmitArgs(interval_in_seconds=interval, timeout_in_seconds=timeout) assert isinstance(args.interval_in_seconds, int) assert isinstance(args.timeout_in_seconds, int) def test_job_submit_command(mocker: MockerFixture): """ Given: - Valid client and args for job_submit_command. When: - Running job_submit_command. Then: - Assert client.submit_job is called once. - Assert CommandResults is returned with correct outputs and ScheduledCommand. """ from HelloWorldV2 import job_submit_command, HelloWorldJobSubmitArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock job response mock_job_data = util_load_json("test_data/job_submit_response.json") # Mock client.submit_job mock_submit_job = mocker.patch.object(client, "submit_job", return_value=mock_job_data) # Create args interval_in_seconds = 30 timeout_in_seconds = 600 args = HelloWorldJobSubmitArgs(interval_in_seconds=interval_in_seconds, timeout_in_seconds=timeout_in_seconds) # Execute command result = job_submit_command(client, args) # Assertions assert mock_submit_job.call_count == 1 # Verify CommandResults assert result.outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.Job" assert result.outputs_key_field == "id" assert result.outputs == mock_job_data assert "Successfully submitted" in result.readable_output assert "test-job-123" in result.readable_output # Verify ScheduledCommand assert result.scheduled_command is not None assert result.scheduled_command._command == "helloworld-job-poll" assert result.scheduled_command._next_run == str(interval_in_seconds) assert result.scheduled_command._timeout == str(timeout_in_seconds) assert result.scheduled_command._args == { "job_id": "test-job-123", "interval_in_seconds": interval_in_seconds, "timeout_in_seconds": timeout_in_seconds, } # endregion # region helloworld-job-poll class TestHelloWorldJobPollArgs: @pytest.mark.parametrize( "job_id", [ pytest.param("abc-123", id="Alphanumeric ID"), pytest.param("job_12345", id="Underscore ID"), pytest.param("550e8400-e29b-41d4-a716-446655440000", id="UUID ID"), ], ) def test_job_poll_args_valid_job_ids(self, job_id): """ Given: - A valid job_id string. When: - Initializing HelloWorldJobPollArgs. Then: - Assert model is created successfully. """ from HelloWorldV2 import HelloWorldJobPollArgs args = HelloWorldJobPollArgs(job_id=job_id) assert args.job_id == job_id def test_job_poll_args_missing_job_id(self): """ Given: - No job_id provided. When: - Initializing HelloWorldJobPollArgs. Then: - Assert DemistoException is raised. """ from HelloWorldV2 import HelloWorldJobPollArgs with pytest.raises(DemistoException, match="job_id"): HelloWorldJobPollArgs() # type: ignore[call-arg] def test_job_poll_command_complete(mocker: MockerFixture): """ Given: - Valid client and args for job_poll_command. - Job status is "complete". When: - Running job_poll_command. Then: - Assert client.get_job_status is called once. - Assert client.get_job_result is called once. - Assert PollResult is returned with continue_to_poll=False and final results. """ from HelloWorldV2 import job_poll_command, HelloWorldJobPollArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock job status and result job_id = "test-job-123" mock_status_response = util_load_json("test_data/job_status_complete.json") mock_job_result = util_load_json("test_data/job_result.json") # Mock client methods mock_get_job_status = mocker.patch.object(client, "get_job_status", return_value=mock_status_response) mock_get_job_result = mocker.patch.object(client, "get_job_result", return_value=mock_job_result) mock_table_to_markdown = mocker.patch("HelloWorldV2.tableToMarkdown") # Create args args = HelloWorldJobPollArgs(job_id=job_id, interval_in_seconds=30, timeout_in_seconds=600) # Execute command result = job_poll_command(args, client) # Assertions assert mock_get_job_status.call_count == 1 assert mock_get_job_status.call_args.args == (job_id,) assert mock_get_job_result.call_count == 1 assert mock_get_job_result.call_args.args == (job_id,) # Verify tableToMarkdown was called assert mock_table_to_markdown.call_count == 1 assert mock_table_to_markdown.call_args.args[0] == f"HelloWorld Job {job_id} - Complete" assert mock_table_to_markdown.call_args.args[1] == mock_job_result # Verify PollResult assert result.scheduled_command is None # No more scheduled commands since polling is complete assert result.outputs_prefix == f"{BASE_CONTEXT_OUTPUT_PREFIX}.Job" assert result.outputs == mock_job_result def test_job_poll_command_in_progress(mocker: MockerFixture): """ Given: - Valid client and args for job_poll_command. - Job status is "running" (not complete). When: - Running job_poll_command. Then: - Assert client.get_job_status is called once. - Assert client.get_job_result is NOT called. - Assert PollResult is returned with continue_to_poll=True and partial results. """ from HelloWorldV2 import job_poll_command, HelloWorldJobPollArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock job status (running) job_id = "test-job-456" mock_status_response = util_load_json("test_data/job_status_running.json") # Mock client methods mock_get_job_status = mocker.patch.object(client, "get_job_status", return_value=mock_status_response) mock_get_job_result = mocker.patch.object(client, "get_job_result") # Create args interval = 30 args = HelloWorldJobPollArgs(job_id=job_id, interval_in_seconds=interval, timeout_in_seconds=600) # Execute command result = job_poll_command(args, client) # Assertions assert mock_get_job_status.call_count == 1 assert mock_get_job_status.call_args.args[0] == job_id assert mock_get_job_result.call_count == 0 # Should NOT be called when job is not complete # Verify No Command Results returned assert result is None # endregion # region helloworld-get-assets def test_get_assets_command(mocker: MockerFixture): """ Given: - Valid client and args for get_assets_command. When: - Running get_assets_command. Then: - Assert client.get_assets is called once with correct parameters. - Assert tableToMarkdown is called with correct args. - Assert CommandResults is returned with correct readable output. """ from HelloWorldV2 import get_assets_command, HelloWorldGetAssetsArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock asset data assets_raw_response = util_load_json("test_data/assets.json") mock_assets = assets_raw_response.get("data", []) # Mock client.get_assets mock_get_assets = mocker.patch.object(client, "get_assets", return_value=assets_raw_response) # Mock tableToMarkdown mock_table_to_markdown = mocker.patch("HelloWorldV2.tableToMarkdown", return_value="Mocked table") # Create args limit = 10 args = HelloWorldGetAssetsArgs(limit=limit) # Execute command result = get_assets_command(client, args) # Assertions assert mock_get_assets.call_count == 1 assert mock_get_assets.call_args.kwargs == {"limit": limit} # Verify tableToMarkdown was called assert mock_table_to_markdown.call_count == 1 assert mock_table_to_markdown.call_args.args == ("HelloWorld Assets", mock_assets) # Verify CommandResults assert result.readable_output == "Mocked table" # endregion # region helloworld-get-vulnerabilities def test_get_vulnerabilities_command(mocker: MockerFixture): """ Given: - Valid client and args for get_vulnerabilities_command. When: - Running get_vulnerabilities_command. Then: - Assert client.get_vulnerabilities is called once with correct parameters. - Assert tableToMarkdown is called with correct args. - Assert CommandResults is returned with correct readable output. """ from HelloWorldV2 import get_vulnerabilities_command, HelloWorldGetVulnerabilitiesArgs # Create params and client params = HelloWorldParams( url="https://api.example.com", credentials=Credentials(password=DUMMY_VALID_API_KEY), ) client = HelloWorldClient(params) # Load mock vulnerability data vulnerabilities_raw_response = util_load_json("test_data/vulnerabilities.json") mock_vulnerabilities = vulnerabilities_raw_response.get("data", []) # Mock client.get_vulnerabilities mock_get_vulnerabilities = mocker.patch.object(client, "get_vulnerabilities", return_value=vulnerabilities_raw_response) # Mock tableToMarkdown mock_table_to_markdown = mocker.patch("HelloWorldV2.tableToMarkdown", return_value="Mocked table") # Create args limit = 10 args = HelloWorldGetVulnerabilitiesArgs(limit=limit) # Execute command result = get_vulnerabilities_command(client, args) # Assertions assert mock_get_vulnerabilities.call_count == 1 assert mock_get_vulnerabilities.call_args.kwargs == {"limit": limit} # Verify tableToMarkdown was called assert mock_table_to_markdown.call_count == 1 assert mock_table_to_markdown.call_args.args == ("HelloWorld Vulnerabilities", mock_vulnerabilities) # Verify CommandResults assert result.readable_output == "Mocked table" # endregion