Wiz
Agentless cloud security with bidirectional Issue mirroring, status sync, comment sync, and due-date sync between Wiz and Cortex XSOAR.
Utilities · Wiz
Details
| ID | Wiz |
|---|---|
| Provider | |
| Category | Utilities |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Agentless, context-aware and full-stack security and compliance across AWS, Azure, GCP, OCI, Kubernetes, and other supported cloud platforms.
This integration was integrated and tested with Wiz
Configure Wiz in Cortex
| Parameter | Description | Required | |
|---|---|---|---|
| Incident type | False | ß | |
| Service Account ID | True | ||
| Password | True | ||
| Authentication Endpoint | Wiz Authentication Endpoint, e.g., https://auth.app.wiz.io/oauth/token |
False | |
| API Endpoint | Wiz API Endpoint. Default: https://api.us1.app.wiz.io/graphql To find your API endpoint URL: 1. Log in to Wiz, then open your user profile 2. Copy the API Endpoint URL to use here. |
True | |
| Incidents Fetch Interval | False | ||
| First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) | False | ||
| Max Issues to Fetch | False | ||
| Use system proxy settings | False | ||
| Fetch incidents | Issue Streaming type. Either Fetch incidents (to constantly pull Issues) or Do not fetch (to push live Issues) |
False | |
| Issue severity to fetch | Select the severity levels of issues to fetch from Wiz. Multiple selections are allowed. Leave empty to fetch all severities. | False | |
| Issue status to fetch | Select the status of issues to fetch from Wiz. Multiple selections are allowed. | False | |
| Issue type to fetch | Select the type of issues to fetch from Wiz. Multiple selections are allowed. | False | |
| Incident Mirror Direction | Choose the mirroring direction for Wiz issues. Default is None (no mirroring). Cortex XSOAR only parameter. | False | |
| Mirror API page size | Page size for mirror API calls (1-500). All modified issues are fetched using this as the page size. | False | |
| Tag for comment mirroring | Add this tag to XSOAR war room entries to mirror them as Wiz issue notes. | False |
Mirroring
The Wiz integration supports bidirectional mirroring between Wiz Issues and XSOAR incidents. Configure direction via the Incident Mirror Direction instance setting:
| Direction | Behavior |
|---|---|
None |
Mirroring disabled. No dbotMirror* metadata is attached to fetched incidents. |
Incoming |
Wiz → XSOAR only. Pulls remote status changes and notes into the XSOAR incident. |
Outgoing |
XSOAR → Wiz only. Pushes XSOAR status changes, due-date changes, and tagged war room entries to the Wiz Issue. |
Incoming And Outgoing |
Both directions active. |
Mirrored fields
| Field | Direction | Notes |
|---|---|---|
| Issue status | Both | XSOAR closed/active map to Wiz RESOLVED/OPEN. in_progress maps to IN_PROGRESS. Reopen restores OPEN. |
| Resolution reason | Outgoing | When closing in XSOAR, set resolutionReason (e.g. ISSUE_FIXED, WONT_FIX). When omitted, defaults to WONT_FIX. |
| Notes / comments | Both | Incoming: all Wiz issue notes are added as war room entries (formatted **Author** (timestamp): text). Service-account notes use **[SA] <name>**. Outgoing: only war room entries tagged with comment_tag (default comments) are pushed to Wiz. |
Due date (dueAt) |
Outgoing | Setting/clearing the XSOAR wizissueduedate field updates the Wiz Issue. |
Loop prevention
Outgoing notes are prefixed with Mirrored from Cortex XSOAR and the Wiz integration filters them out on incoming sync, so mirrored notes are not echoed back into the war room.
First-sync behavior
On the first incoming sync after an incident is created in XSOAR, existing Wiz notes are not back-filled into the war room — only notes added after the first sync are mirrored. This avoids dumping the entire pre-existing note history into a fresh investigation.
Note truncation
Notes longer than 1400 characters are truncated and suffixed with ... [truncated] before being sent to the Wiz API. This applies to all mirrored notes and to the wiz-set-issue-note, wiz-resolve-issue, wiz-reject-issue, and wiz-defend-set-threat-comment commands.
Mirror engine commands
The following commands are invoked by the XSOAR mirroring engine and are not intended for manual use:
| Command | Purpose |
|---|---|
get-remote-data |
Fetches updates for a single incident from Wiz. |
get-modified-remote-data |
Returns the list of Wiz Issues modified since the last mirror cycle (paginated; page size = mirror_limit). |
update-remote-system |
Pushes local XSOAR changes (status, notes, due date) back to Wiz. |
get-mapping-fields |
Returns the schema of mappable fields. Used by the mapper UI. |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook or War Room.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
wiz-get-issue
Get the details for a Wiz Issue ID.
Base Command
wiz-get-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
Command Example
!wiz-get-issue issue_id="12345678-1234-1234-1234-cc0a24716e0b"
wiz-get-issues
Get the issues on cloud resources.
Base Command
wiz-get-issues
Input
| Argument Name | Description | Required |
|——————-|——————————————————————————————————————————————————————| — |
| issue_type | The type of Issue to get
Expected input: TOXIC_COMBINATION, THREAT_DETECTION, CLOUD_CONFIGURATION.
The chosen type will be fetched . | Optional |
| entity_type | The type of entity to get issues for. | Optional |
| resource_id | Get Issues of a specific resource_id.
Expected input: providerId | Optional |
| severity | Get Issues of a specific severuty.
Expected input: CRITICAL, HIGH, MEDIUM, LOW or INFORMATIONAL.
The chosen severity and above will be fetched | Optional |
entity_type and resource_id are mutually exclusive.
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Issues | String | All Issues |
Command Example
!wiz-get-issues entity_type="VIRTUAL_MACHINE"
!wiz-get-issues issue_type="THREAT_DETECTION"
!wiz-get-issues resource_id="arn:aws:ec2:us-east-2:123456789098:instance/i-0g03j4h5gd123d456"
!wiz-get-issues resource_id="arn:aws:ec2:us-east-2:123456789098:instance/i-0g03j4h5gd123d456" severity=HIGH
wiz-get-resource
Get Details of a resource. You should pass exactly one of resource_id, resource_name.
When searching by name, results are limited to 500 records.
Base Command
wiz-get-resource
Input
| Argument Name | Description | Required |
|---|---|---|
| resource_id | Resource provider id | optional |
| resource_name | search by name or external ID | optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Resource | String | Resource details |
Command Example
!wiz-get-resource resource_id="arn:aws:ec2:us-east-2:123456789098:instance/i-0g03j4h5gd123d456"
!wiz-get-resource resource_name="i-0g03j4h5gd123d456"
!wiz-get-resource resource_name="test_vm"
wiz-get-resources
Get details of multiple resources based on various filters.
Base Command
wiz-get-resources
Input
| Argument Name | Description | Required |
|---|---|---|
| search | Filter by free text search on cloud resource name. | Optional |
| entity_type | Filter cloud resources by specific entity types. Possible values are: ACCESS_ROLE, ACCESS_ROLE_BINDING, ACCESS_ROLE_PERMISSION, API_GATEWAY, APPLICATION, AUTHENTICATION_CONFIGURATION, BACKUP_SERVICE, BUCKET, CDN, CERTIFICATE, CICD_SERVICE, CLOUD_LOG_CONFIGURATION, CLOUD_ORGANIZATION, COMPUTE_INSTANCE_GROUP, CONFIG_MAP, CONTAINER, CONTAINER_GROUP, CONTAINER_IMAGE, CONTAINER_REGISTRY, CONTAINER_SERVICE, DAEMON_SET, DATABASE, DATA_WORKLOAD, DB_SERVER, DEPLOYMENT, DNS_RECORD, DNS_ZONE, DOMAIN, EMAIL_SERVICE, ENCRYPTION_KEY, ENDPOINT, FILE_SYSTEM_SERVICE, FIREWALL, GATEWAY, GOVERNANCE_POLICY, GOVERNANCE_POLICY_GROUP, HOSTED_APPLICATION, IAM_BINDING, IP_RANGE, KUBERNETES_CLUSTER, KUBERNETES_CRON_JOB, KUBERNETES_INGRESS, KUBERNETES_INGRESS_CONTROLLER, KUBERNETES_JOB, KUBERNETES_NETWORK_POLICY, KUBERNETES_NODE, KUBERNETES_PERSISTENT_VOLUME, KUBERNETES_PERSISTENT_VOLUME_CLAIM, KUBERNETES_POD_SECURITY_POLICY, KUBERNETES_SERVICE, KUBERNETES_STORAGE_CLASS, KUBERNETES_VOLUME, LOAD_BALANCER, MANAGED_CERTIFICATE, MANAGEMENT_SERVICE, NETWORK_ADDRESS, NETWORK_INTERFACE, NETWORK_ROUTING_RULE, NETWORK_SECURITY_RULE, PEERING, POD, PORT_RANGE, PRIVATE_ENDPOINT, PROXY, PROXY_RULE, RAW_ACCESS_POLICY, REGISTERED_DOMAIN, REPLICA_SET, RESOURCE_GROUP, SEARCH_INDEX, SECRET, SECRET_CONTAINER, SERVERLESS, SERVERLESS_PACKAGE, SERVICE_ACCOUNT, STORAGE_ACCOUNT, SUBNET, SUBSCRIPTION, SWITCH, USER_ACCOUNT, VIRTUAL_DESKTOP, VIRTUAL_MACHINE, VIRTUAL_MACHINE_IMAGE, VIRTUAL_NETWORK, VOLUME, WEB_SERVICE, DATA_WORKFLOW. | Optional |
| subscription_external_ids | Filter cloud resources according to these external subscription IDs (AWS Account, Azure Subscription, GCP Project, and OCI Compartment). You can provide multiple IDs separated by commas. | Optional |
| provider_unique_ids | Filter cloud resources according to these cloud service provider unique IDs. You can provide multiple IDs separated by commas. | Optional |
| project_ids | Filter by Wiz project IDs (comma-separated). | Optional |
| native_types | Filter by cloud-native resource types (comma-separated, e.g. aws_ec2_instance). | Optional |
| updated_at_before | Filter resources updated before this date (ISO 8601, e.g. 2024-01-01T00:00:00Z). | Optional |
| updated_at_after | Filter resources updated after this date (ISO 8601, e.g. 2024-01-01T00:00:00Z). | Optional |
At least one parameter must be provided.
Context Output
This command returns the raw response data from the Wiz API. The response includes resource details in JSON format.
Command Example
!wiz-get-resources search="web-server"
!wiz-get-resources entity_type="VIRTUAL_MACHINE"
!wiz-get-resources subscription_external_ids="123456789,987654321"
!wiz-get-resources provider_unique_ids="i-0g03j4h5gd123d456"
!wiz-get-resources entity_type="BUCKET" search="backup"
wiz-issue-in-progress
Set a Wiz Issue to in progress.
Base Command
wiz-issue-in-progress
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Issue | String | Issue details |
Command Example
!wiz-issue-in-progress issue_id="12345678-1234-1234-1234-cc0a24716e0b"
wiz-reopen-issue
Re-open an Issue.
Base Command
wiz-reopen-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
| reopen_note | Note for re-opening Issue | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Issue | String | Issue details |
Command Example
!wiz-reopen-issue issue_id="12345678-1234-1234-1234-cc0a24716e0b" reopen_note="still an issue"
wiz-reject-issue
Reject a Wiz Issue. Not supported for THREAT_DETECTION issues.
Base Command
wiz-reject-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
| reject_reason | Rejection reason. Possible values are: FALSE_POSITIVE, EXCEPTION, WONT_FIX. | Required |
| reject_note | Note for the rejection. Notes longer than 1400 characters are truncated and suffixed with ... [truncated]. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Issue | String | Issue details |
Command Example
!wiz-reject-issue issue_id="12345678-1234-1234-1234-cc0a24716e0b" reject_reason="WONT_FIX" reject_note="this is by design"
wiz-resolve-issue
Resolve a Wiz Issue.
Base Command
wiz-resolve-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
| resolution_reason | Issue resolution reason. Possible values are: OBJECT_DELETED, ISSUE_FIXED, FALSE_POSITIVE, EXCEPTION, WONT_FIX. | Required |
| resolution_note | Note to explain why the Issue has been resolved. Notes longer than 1400 characters are truncated and suffixed with ... [truncated]. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Wiz.Manager.Issue | String | Issue details |
Command Example
!wiz-resolve-issue issue_id="12345678-1234-1234-1234-cc0a24716e0b" resolution_note="won't fix this issue as this is low priority" resolution_reason="WONT_FIX"
wiz-set-issue-note
Set (append) a note to an Issue.
Base Command
wiz-set-issue-note
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
| note | Note for the Issue. Will be appended to existing notes. Notes longer than 1400 characters are truncated and suffixed with ... [truncated]. |
Required |
Command Example
!wiz-set-issue-note issue_id="12345678-1234-1234-1234-cc0a24716e0b" note="Checking with owner"
wiz-clear-issue-note
Clears a note from an Issue.
Base Command
wiz-clear-issue-note
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
Command Example
!wiz-clear-issue-note issue_id="12345678-1234-1234-1234-cc0a24716e0b"
wiz-get-issue-evidence
Get the evidence from an Issue.
Base Command
wiz-get-issue-evidence
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
Command Example
!wiz-get-issue-evidence issue_id="12345678-1234-1234-1234-cc0a24716e0b"
wiz-rescan-machine-disk
Deprecated
wiz-set-issue-due-date
Set a due date for an Issue.
Base Command
wiz-set-issue-due-date
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
| due_at | Due At Date. Format must be YYYY-MM-DD (e.g. 2026-12-31). |
Required |
Command Example
!wiz-set-issue-due-date issue_id="12345678-1234-1234-1234-cc0a24716e0b" due_at="2022-01-20"
wiz-clear-issue-due-date
Clear a due date for an Issue.
Base Command
wiz-clear-issue-due-date
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Issue id | Required |
Command Example
!wiz-clear-issue-due-date issue_id="12345678-1234-1234-1234-cc0a24716e0b"
wiz-get-project-team
Get the Project Owners and Security Champions details.
Base Command
wiz-get-project-team
Input
| Argument Name | Description | Required |
|---|---|---|
| project_name | Project Name | Required |
Command Example
!wiz-get-project-team project_name="project1"
wiz-copy-to-forensics-account
Copy VM’s Volumes to a Forensics Account
Base Command
wiz-copy-to-forensics-account
Input
| Argument Name | Description | Required |
|---|---|---|
| resource_id | Resource Id | Required |
Command Example
!wiz-copy-to-forensics-account resource_id="12345678-1234-1234-1234-cc0a24716e0b"
!wiz-copy-to-forensics-account resource_id="arn:aws:ec2:us-east-1:123455563321:instance/i-05r662bfb9708a4e8"
Configuration parameters
incidentType— Incident typecredentials— Service Account ID (required)auth_endpoint— Authentication Endpointapi_endpoint— API Endpoint (required)incidentFetchInterval— Incidents Fetch Intervalfirst_fetch— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)max_fetch— Max Issues to Fetchproxy— Use system proxy settingsisFetch— Fetch incidentsseverity— Issue severity to fetchstatus— Issue status to fetchissue_type— Issue type to fetchmirror_direction— Incident Mirror Directionmirror_limit— Mirror API page sizecomment_tag— Tag for comment mirroring
Commands (20)
-
get-mapping-fieldsReturns the list of fields for an incident type. Called by the mirroring engine, not for manual use.
-
get-modified-remote-dataGet the list of incidents modified since the last update. Called by the mirroring engine, not for manual use.
-
get-remote-dataGet remote data for a single incident. Called by the mirroring engine, not for manual use.
-
update-remote-systemPush local changes to Wiz. Called by the mirroring engine, not for manual use.
-
wiz-clear-issue-due-dateClear a due date on a Wiz Issue.
-
wiz-clear-issue-noteClear a note from a Wiz Issue.
-
wiz-copy-to-forensics-accountCopy VM's Volumes to a Forensics Account.
-
wiz-get-issueGet the details of a Wiz Issue Id.
-
wiz-get-issue-evidenceGet the Wiz Issue evidence.
-
wiz-get-issuesGet the Issues on cloud resources.
-
wiz-get-project-teamGet the Project Owners and Security Champions details.
-
wiz-get-resourceGet details of a resource.
-
wiz-get-resourcesGet details of resources.
-
wiz-issue-in-progressSet a Wiz Issue to in progress.
-
wiz-reject-issueReject a Wiz Issue.
-
wiz-reopen-issueReopen a Wiz Issue.
-
wiz-rescan-machine-diskDeprecatedDEPRECATED.
-
wiz-resolve-issueResolve a Wiz Threat Detection Issue. For non-Threat-Detection issues (Toxic Combination, Cloud Configuration, Attack Surface) use wiz-reject-issue — those types are auto-resolved by Wiz when the underlying problem is fixed and cannot be manually resolved.
-
wiz-set-issue-due-dateSet a due date on a Wiz Issue.
-
wiz-set-issue-noteSet a note on a Wiz Issue.
import time import uuid from CommonServerPython import * import demistomock as demisto from urllib import parse DEMISTO_OCCURRED_FORMAT = "%Y-%m-%dT%H:%M:%SZ" API_REQUEST_TIMEOUT = 115 # seconds; keep under the upstream gateway timeout to avoid hangs WIZ_HTTP_QUERIES_LIMIT = 500 # Request limit during run WIZ_API_LIMIT = 500 # limit number of returned records from the Wiz API MAX_NOTE_LENGTH = 1400 # Hard limit for issue note text length enforced by the Wiz API WIZ = "wiz" WIZ_VERSION = "1.7.0" INTEGRATION_GUID = "8864e131-72db-4928-1293-e292f0ed699f" NOT_DEFINED = "Not Defined" DEFAULT_FETCH_ISSUE_STATUS = ["OPEN", "IN_PROGRESS"] class ValidationType: """Class representing field names for validation results""" IS_VALID = "is_valid" ERROR_MESSAGE = "error_message" VALUE = "value" SEVERITY_LIST = "severity_list" MINUTES_VALUE = "minutes_value" DAYS_VALUE = "days_value" STATUS_LIST = "status_list" class ValidationResponse: """Class for standardized validation responses""" def __init__(self, is_valid=True, error_message=None, value=None): self.is_valid = is_valid self.error_message = error_message self.value = value self.days_value = None self.minutes_value = None self.severity_list = None self.status_list = None def to_dict(self): """Convert the response to a dictionary""" return { ValidationType.IS_VALID: self.is_valid, ValidationType.ERROR_MESSAGE: self.error_message, ValidationType.VALUE: self.value, ValidationType.DAYS_VALUE: self.days_value, ValidationType.MINUTES_VALUE: self.minutes_value, ValidationType.SEVERITY_LIST: self.severity_list, ValidationType.STATUS_LIST: self.status_list, } @classmethod def create_success(cls, value=None): """Create a successful validation response""" return cls(is_valid=True, error_message=None, value=value) @classmethod def create_error(cls, error_message): """Create a failed validation response""" return cls(is_valid=False, error_message=error_message, value=None) def get_integration_user_agent(): integration_user_agent = f"{INTEGRATION_GUID}/xsoar/{WIZ_VERSION}" return integration_user_agent # Standard headers HEADERS_AUTH = {"Content-Type": "application/x-www-form-urlencoded", "User-Agent": get_integration_user_agent()} HEADERS = {"Content-Type": "application/json", "User-Agent": get_integration_user_agent()} TOKEN = None URL = "" AUTH_E = "" AUTH_DEFAULT = "auth" # NEED TO BE REMOVED AFTER THAT AUTH0 IS DEPRECATED COGNITO_PREFIX = ["auth.app", "auth.gov", "auth.test"] AUTH0_PREFIX = ["auth", "auth0.gov", "auth0.test"] URL_SUFFIX = "wiz.io/oauth/token" URL_SUFFIX_FED = "wiz.us/oauth/token" # Issues Queries PULL_ISSUES_QUERY = """ query IssuesTable( $filterBy: IssueFilters $first: Int $after: String $orderBy: IssueOrder ) { issues:issuesV2(filterBy: $filterBy first: $first after: $after orderBy: $orderBy) { nodes { id sourceRule{ __typename ... on Control { id name description resolutionRecommendation securitySubCategories { title category { name framework { name } } } } ... on CloudEventRule{ id name description sourceType type } ... on CloudConfigurationRule{ id name description remediationInstructions serviceType } } type createdAt updatedAt dueAt projects { id name slug projectOwners{ name } securityChampions{ name } businessUnit riskProfile { businessImpact } } status severity entitySnapshot { id type nativeType name status cloudPlatform cloudProviderURL providerId region resourceGroupExternalId subscriptionExternalId subscriptionName subscriptionTags tags externalId } serviceTickets { externalId name url } notes { id createdAt updatedAt text user { name email } serviceAccount { name } } } pageInfo { hasNextPage endCursor } } } """ MODIFIED_ISSUE_IDS_QUERY = """ query ModifiedIssueIds( $filterBy: IssueFilters $first: Int $after: String $orderBy: IssueOrder ) { issues:issuesV2(filterBy: $filterBy first: $first after: $after orderBy: $orderBy) { nodes { id statusChangedAt } pageInfo { hasNextPage endCursor } } } """ PULL_ISSUE_WITH_EVIDENCE_PARAM_QUERY = """ query IssuesTable( $filterBy: IssueFilters $first: Int $after: String $orderBy: IssueOrder ) { issues:issuesV2(filterBy: $filterBy first: $first after: $after orderBy: $orderBy) { nodes { evidenceQuery threatDetectionDetails{ ...ThreatDetectionDetailsDetections ...ThreatDetectionDetailsActorsResources ...ThreatDetectionDetailsMainDetection ...ThreatDetectionDetailsCloudEventGroups } type } pageInfo { hasNextPage endCursor } } } fragment ThreatDetectionDetailsDetections on ThreatDetectionIssueDetails { detections(first: 500) { nodes { primaryResource { id type name externalId } actors { id name externalId providerUniqueId type } startedAt id severity description(format: MARKDOWN) primaryResource { region cloudAccount { id name externalId cloudProvider } } ruleMatch { rule { id name securitySubCategories { id title category { id name framework { id } } } } } } } } fragment ThreatDetectionDetailsActorsResources on ThreatDetectionIssueDetails { actors { id name externalId providerUniqueId type } resources { id name externalId providerUniqueId type nativeType } } fragment ThreatDetectionDetailsMainDetection on ThreatDetectionIssueDetails { mainDetection { id startedAt severity description(format: MARKDOWN) ruleMatch { rule { id name origins } } } } fragment ThreatDetectionDetailsCloudEventGroups on ThreatDetectionIssueDetails { cloudEventGroups(first: 500) { nodes { id name firstEventAt lastEventAt status kind origin groupType description cloudEvents { ...CloudEventGroupCloudEventResponse } } } } fragment CloudEventGroupCloudEventResponse on CloudEvent { id category externalName isForeignActorIP rawAuditLogRecord errorMessage timestamp origin path kind cloudPlatform actor { id externalId name type email userAgent accessKeyId providerUniqueId inactiveInLast90Days friendlyName hasAdminKubernetesPrivileges hasAdminPrivileges hasHighKubernetesPrivileges hasHighPrivileges isExternalCloudAccount actingAs { id name friendlyName externalId providerUniqueId type } } actorIP actorIPMeta { relatedAttackGroupNames city country countryCode reputation autonomousSystemOrganization } subjectResource { id type name nativeType externalId providerUniqueId region cloudAccount { id name externalId cloudProvider } containerService { id name type providerUniqueId } containerServiceGraphEntity { id name type providerUniqueId } kubernetesClusterGraphEntity { id name type providerUniqueId } kubernetesCluster { id name type providerUniqueId } kubernetesNamespaceGraphEntity { id name providerUniqueId } kubernetesNamespace { id name providerUniqueId } kubernetesControllerGraphEntity { id name type providerUniqueId } kubernetesController { id name type providerUniqueId } openToAllInternet } errorCode statusDetails { errorReason providerErrorMessage providerErrorCode } status matchedRules { rule { builtInId name id } } ...CloudEventExtraDetails } fragment CloudEventExtraDetails on CloudEvent { extraDetails { ...CloudEventRuntimeDetails ...CloudEventAdmissionReviewDetails ...CloudEventFimDetails ...CloudEventImageIntegrityDetails ...CloudEventCICDScanDetails } trigger { ...CloudEventSensorRulesMatch ...CloudEventAdmissionReviewTriggerDetails } } fragment CloudEventRuntimeDetails on CloudEventRuntimeDetails { sensor { id name lastSeenAt firstSeenAt sensorVersion definitionsVersion status ipAddress type workload { id name sensorName } cluster { id name type } } processTree { ...CloudEventRuntimeProcessBasicDetails userName userId hash executionTime stdin stdout name wizResponse enforcementResult { action errorMessage } containerGraphEntity { ...ProcessResourceGraphEntity properties } container { id name externalId imageGraphEntity { ...ProcessResourceGraphEntity } image { id externalId } podGraphEntity { ...ProcessResourceGraphEntity } pod { id name externalId ips namespace namespaceGraphEntity { ...ProcessResourceGraphEntity } } kubernetesControllerGraphEntity { ...ProcessResourceGraphEntity } kubernetesController { id name externalId type } kubernetesClusterGraphEntity { ...ProcessResourceGraphEntity } kubernetesCluster { id name externalId } serviceAccount ecsContainerDetails { ecsTask { id externalId } ecsTaskGraphEntity { ...ProcessResourceGraphEntity } ecsCluster { id name externalId } ecsClusterGraphEntity { ...ProcessResourceGraphEntity } ecsService { id name externalId } ecsServiceGraphEntity { ...ProcessResourceGraphEntity } } } } hostGraphEntity { properties ...ProcessResourceGraphEntity } host { id externalId type hostname kernelVersion computeInstanceGroupGraphEntity { id name type } } rawDetails type context { ... on CloudEventRuntimeTypeFileContext { fileName } ... on CloudEventRuntimeTypeNetworkConnectContext { remoteIP remotePort } ... on CloudEventRuntimeTypeDNSQueryContext { query } ... on CloudEventRuntimeTypeProcessStartContext { commandLine } ... on CloudEventRuntimeTypeIMDSQueryContext { query } ... on CloudEventRuntimeTypeChangeDirectoryContext { path } } } fragment CloudEventRuntimeProcessBasicDetails on CloudEventRuntimeProcess { id command path executionTime } fragment ProcessResourceGraphEntity on GraphEntity { id name type } fragment CloudEventAdmissionReviewDetails on CloudEventAdmissionReviewDetails { verdict policyEnforcement reviewDuration infoMatches lowMatches mediumMatches highMatches criticalMatches totalMatches policies { ...CICDScanPolicyDetails } cloudConfigurationFindings { cloudConfigurationRule { id shortId name severity cloudProvider } passedPolicies { ...CICDScanPolicyDetails } failedPolicies { ...CICDScanPolicyDetails } } } fragment CICDScanPolicyDetails on CICDScanPolicy { id name description policyLifecycleEnforcements { enforcementMethod deploymentLifecycle } params { __typename ... on CICDScanPolicyParamsIAC { severityThreshold } ... on CICDScanPolicyParamsVulnerabilities { severity } ... on CICDScanPolicyParamsSensitiveData { dataFindingSeverityThreshold } ... on CICDScanPolicyParamsHostConfiguration { hostConfigurationSeverity rulesScope { type securityFrameworks { id name } } failCountThreshold passPercentageThreshold } } } fragment CloudEventFimDetails on CloudEventFimDetails { previousHash } fragment CloudEventImageIntegrityDetails on CloudEventImageIntegrityAdmissionReviewDetails { verdict policyEnforcement reviewDuration policies { ...CICDScanPolicyDetails } images { id name imageVerdict sources digest policiesFailedBasedOnNoMatchingValidators { id name } imageIntegrityValidators { imageIntegrityValidator { ...ImageSignatureValidatorDetails } verdict failedPolicies { ...CICDScanPolicyDetails } passedPolicies { ...CICDScanPolicyDetails } extraDetails { ... on ImageIntegrityAdmissionReviewImageValidatorExtraDetailsWizScan { cicdScan { id status { verdict } } } } } } } fragment ImageSignatureValidatorDetails on ImageIntegrityValidator { id name description imagePatterns projects { id isFolder slug name } value { method notary { certificate } cosign { method key certificate certificateChain } wizScan { maxAgeHours policyId serviceAccountIds } } } fragment CloudEventCICDScanDetails on CloudEventCICDScanDetails { cicdScanPolicyEnforcement: policyEnforcement scanDuration trigger tags { key value } createdBy { serviceAccount { id name } user { id name email } } cliDetails { ...CICDScanCLIDetailsFragment } codeAnalyzerDetails { taskUrl commit { author infoURL messageSnippet ref sha } webhookEvent { createdAt hookID payload processedAt receivedAt source sourceRequestID type wizRequestID } pullRequest { author title baseCommit { sha ref infoURL } headCommit { sha ref infoURL } bodySnippet infoURL analytics { additions deletions changedFiles commits } } } warnedPolicies { ...CICDScanPolicyDetails } failedPolicies { ...CICDScanPolicyDetails } passedPolicies { ...CICDScanPolicyDetails } policies { ...CICDScanPolicyDetails } secretDetails { failedPolicyMatches { policy { __typename id name } } secrets { id contains { name type } details { __typename } failedPolicyMatches { policy { __typename id name } } description lineNumber offset path snippet type severity hasAdminPrivileges hasHighPrivileges relatedEntities { id type name properties } } } iacDetails { ruleMatches { rule { id shortId name description cloudProvider } deletedRuleFallback: rule { id name } severity failedResourceCount failedPolicyMatches { policy { id } } matches { resourceName fileName lineNumber matchContent expected found } } scanStatistics { infoMatches lowMatches highMatches mediumMatches criticalMatches totalMatches } } hostConfigurationDetails { ...HostConfigurationDetails } vulnerabilityDetails { vulnerableSBOMArtifactsByNameVersion { ...CICDSbomArtifactsByNameVersion } cpes { name version path vulnerabilities { ...CICDScanDiskScanVulnerabilityDetails } detectionMethod } osPackages { name version vulnerabilities { ...CICDScanDiskScanVulnerabilityDetails } detectionMethod } libraries { name version path vulnerabilities { ...CICDScanDiskScanVulnerabilityDetails } detectionMethod } applications { name vulnerabilities { path pathType version vulnerability { ...CICDScanDiskScanVulnerabilityDetails } } detectionMethod } } dataDetails { dataFindingsWithFullClassifierInfo: findings { dataClassifier { id name category originalDataClassifierOverridden } ...CICDScanDataFindingDetails } dataFindings: findings { dataClassifier { id name } ...CICDScanDataFindingDetails } } status { details state verdict } policies { __typename id name params { __typename } } } fragment CICDScanCLIDetailsFragment on CICDScanCLIDetails { scanOriginResource { name __typename ... on CICDScanOriginIAC { subTypes name } ... on CICDScanOriginContainerImage { digest id name } } scanOriginResourceType clientName clientVersion buildParams { commitUrl branch commitHash committedBy platform repository extraDetails { ... on CICDBuildParamsContainerImage { dockerfilePath dockerfileContents } } } } fragment HostConfigurationDetails on CICDHostConfigurationScanResult { hostConfigurationFrameworks { framework { id name } matches { policyMatch { policy { id } } } } hostConfigurationFindings { rule { description name id securitySubCategories { id resolutionRecommendation title description category { id name framework { id name enabled } } } } status severity failedPolicyMatches { policy { id } } } } fragment CICDSbomArtifactsByNameVersion on CICDDiskScanResultSBOMArtifactsByNameVersion { id name version filePath vulnerabilityFindings { fixedVersion remediation severities { criticalCount highCount infoCount lowCount mediumCount } findings { id vulnerabilityExternalId vulnerableAsset { ... on VulnerableAssetRepositoryBranch { id type name providerUniqueId repositoryName } } remediationPullRequestAvailable remediationPullRequestConnector { id name type { id name } } severity } } layerMetadata { id isBaseLayer details } type { ...SBOMArtifactTypeFragment } } fragment SBOMArtifactTypeFragment on SBOMArtifactType { group codeLibraryLanguage osPackageManager hostedTechnology { id name icon } plugin } fragment CICDScanDiskScanVulnerabilityDetails on DiskScanVulnerability { name severity fixedVersion source score exploitabilityScore hasExploit hasCisaKevExploit cisaKevReleaseDate cisaKevDueDate epssProbability epssPercentile epssSeverity publishDate fixPublishDate gracePeriodEnd gracePeriodRemainingHours failedPolicyMatches { policy { id name params { ... on CICDScanPolicyParamsHostConfiguration { failCountThreshold passPercentageThreshold rulesScope { type } } } } } weightedSeverity finding { id version } } fragment CICDScanDataFindingDetails on CICDDiskScanResultDataFinding { matchCount severity examples { path matchCount value } } fragment CloudEventSensorRulesMatch on CloudEventSensorRulesMatch { sensorEngineRules { rule { id name description MITRETactics MITRETechniques } version } fileReputationHashMatch { name md5 sha1 sha256 sampleFirstSeen sampleLastSeen scannerMatch scannerCount scannerPercent trustFactor malwareClassification { isGeneric type platform subPlatform family vulnerability { id } } } connectivityReputation { source { ip port } destination { ip ipReputation port } process { ...CloudEventRuntimeProcessBasicDetails } } dnsQueryReputation { domain domainReputation process { ...CloudEventRuntimeProcessBasicDetails } } } fragment CloudEventAdmissionReviewTriggerDetails on CloudEventAdmissionReview { cloudConfigurationRuleMatches { cloudConfigurationRule { id } cicdScanPolicies { id name params { __typename } } } } """ PULL_ISSUES_DEFAULT_VARIABLES = {"orderBy": {"field": "SEVERITY", "direction": "DESC"}} PULL_ISSUES_TEST_VARIABLES = test_variables = { "first": 1, "filterBy": {"status": ["OPEN", "IN_PROGRESS"]}, "orderBy": {"field": "SEVERITY", "direction": "DESC"}, } PULL_ISSUE_EVIDENCE_QUERY = """ query GraphSearch( $query: GraphEntityQueryInput $controlId: ID $projectId: String! $first: Int $after: String $fetchTotalCount: Boolean! $quick: Boolean = true $fetchPublicExposurePaths: Boolean = false $fetchInternalExposurePaths: Boolean = false $fetchIssueAnalytics: Boolean = false $fetchLateralMovement: Boolean = false $fetchKubernetes: Boolean = false ) { graphSearch( query: $query controlId: $controlId projectId: $projectId first: $first after: $after quick: $quick ) { totalCount @include(if: $fetchTotalCount) maxCountReached @include(if: $fetchTotalCount) pageInfo { endCursor hasNextPage } nodes { entities { ...PathGraphEntityFragment userMetadata { isInWatchlist isIgnored note } technologies { id icon } publicExposures(first: 10) @include(if: $fetchPublicExposurePaths) { nodes { ...NetworkExposureFragment } } otherSubscriptionExposures(first: 10) @include(if: $fetchInternalExposurePaths) { nodes { ...NetworkExposureFragment } } otherVnetExposures(first: 10) @include(if: $fetchInternalExposurePaths) { nodes { ...NetworkExposureFragment } } lateralMovementPaths(first: 10) @include(if: $fetchLateralMovement) { nodes { id pathEntities { entity { ...PathGraphEntityFragment } } } } kubernetesPaths(first: 10) @include(if: $fetchKubernetes) { nodes { id path { ...PathGraphEntityFragment } } } } aggregateCount } } } fragment PathGraphEntityFragment on GraphEntity { id name type properties issueAnalytics: issues(filterBy: { status: [IN_PROGRESS, OPEN] }) @include(if: $fetchIssueAnalytics) { highSeverityCount criticalSeverityCount } } fragment NetworkExposureFragment on NetworkExposure { id portRange sourceIpRange destinationIpRange path { ...PathGraphEntityFragment } applicationEndpoints { ...PathGraphEntityFragment } } """ PULL_ISSUE_EVIDENCE_PARAMS = { "quick": True, "fetchPublicExposurePaths": False, "fetchInternalExposurePaths": False, "fetchIssueAnalytics": False, "fetchLateralMovement": False, "fetchKubernetes": False, "first": 5, "projectId": "*", "query": None, "fetchTotalCount": False, } UPDATE_ISSUE_QUERY = """ mutation UpdateIssue( $issueId: ID! $patch: UpdateIssuePatch $override: UpdateIssuePatch ) { updateIssue(input: { id: $issueId, patch: $patch, override: $override }) { issue { id notes { ...IssueNoteDetails } status dueAt resolutionReason } } } fragment IssueNoteDetails on IssueNote { id text updatedAt createdAt user { id email } serviceAccount { id name } } """ CREATE_COMMENT_QUERY = """ mutation CreateIssueComment($input: CreateIssueNoteInput!) { createIssueNote(input: $input) { issueNote { createdAt id text user { id email } } } } """ DELETE_NOTE_QUERY = """ mutation DeleteIssueNote($input: DeleteIssueNoteInput!) { deleteIssueNote(input: $input) { _stub } } """ # Resources Queries PULL_RESOURCES_ID_NATIVE_QUERY = """ query CloudResourceSearch($filterBy: CloudResourceFilters, $first: Int, $after: String) { cloudResources(filterBy: $filterBy, first: $first, after: $after) { nodes { id } pageInfo { hasNextPage endCursor } } } """ PULL_CLOUD_RESOURCES_NATIVE_QUERY = """ query CloudResourceSearch($filterBy: CloudResourceFilters, $first: Int, $after: String) { cloudResources(filterBy: $filterBy, first: $first, after: $after) { nodes { id name type subscriptionId subscriptionExternalId graphEntity { id providerUniqueId name type projects { id } properties firstSeen lastSeen } } pageInfo { hasNextPage endCursor } } } """ # Forensics Queries COPY_TO_FORENSICS_ACCOUNT_MUTATION = """ mutation CopyResourceForensicsToExternalAccount($input: CopyResourceForensicsToExternalAccountInput!) { copyResourceForensicsToExternalAccount(input: $input) { systemActivityGroupId } } """ # Project Queries PULL_PROJECTS_QUERY = """ query ProjectsTable($filterBy: ProjectFilters, $first: Int, $after: String, $orderBy: ProjectOrder) { projects(filterBy: $filterBy, first: $first, after: $after, orderBy: $orderBy) { nodes { id name isFolder archived businessUnit description projectOwners { id name email } securityChampions { id name email } } } }""" class WizInputParam: ISSUE_ID = "issue_id" ISSUE_TYPE = "issue_type" ENTITY_TYPE = "entity_type" RESOURCE_ID = "resource_id" RESOURCE_NAME = "resource_name" SEVERITY = "severity" REJECT_REASON = "reject_reason" REJECT_NOTE = "reject_note" RESOLUTION_REASON = "resolution_reason" RESOLUTION_NOTE = "resolution_note" REOPEN_NOTE = "reopen_note" NOTE = "note" DUE_AT = "due_at" VM_ID = "vm_id" PROJECT_NAME = "project_name" SEARCH = "search" SUBSCRIPTION_EXTERNAL_IDS = "subscription_external_ids" PROVIDER_UNIQUE_IDS = "provider_unique_ids" PROJECT_IDS = "project_ids" NATIVE_TYPES = "native_types" UPDATED_AT_BEFORE = "updated_at_before" UPDATED_AT_AFTER = "updated_at_after" CREATED_AFTER = "created_after" CREATED_BEFORE = "created_before" LIMIT = "limit" STATUS = "status" class WizStatus: OPEN = "OPEN" IN_PROGRESS = "IN_PROGRESS" REJECTED = "REJECTED" RESOLVED = "RESOLVED" @classmethod def values(cls): """Get all available detection origins""" return [getattr(cls, attr) for attr in dir(cls) if not attr.startswith("_") and not callable(getattr(cls, attr))] class WizSeverity: CRITICAL = "CRITICAL" HIGH = "HIGH" MEDIUM = "MEDIUM" LOW = "LOW" INFORMATIONAL = "INFORMATIONAL" @classmethod def values(cls): """Get all available detection origins""" return [getattr(cls, attr) for attr in dir(cls) if not attr.startswith("_") and not callable(getattr(cls, attr))] class WizIssueType: TOXIC_COMBINATION = "TOXIC_COMBINATION" CLOUD_CONFIGURATION = "CLOUD_CONFIGURATION" THREAT_DETECTION = "THREAT_DETECTION" @classmethod def values(cls): """Get all available detection origins""" return [getattr(cls, attr) for attr in dir(cls) if not attr.startswith("_") and not callable(getattr(cls, attr))] class WizMirrorDirection: """Mirror direction values — maps config dropdown to XSOAR direction codes.""" NONE = None INCOMING = "In" OUTGOING = "Out" BOTH = "Both" DIRECTION_MAP = { "None": NONE, "Incoming": INCOMING, "Outgoing": OUTGOING, "Incoming And Outgoing": BOTH, } @classmethod def from_params(cls): """Get mirror direction from integration params.""" direction_str = demisto.params().get(WizMirrorParam.DIRECTION, "None") if direction_str not in cls.DIRECTION_MAP: demisto.debug(f"Invalid mirror_direction value: '{direction_str}', defaulting to None") return cls.DIRECTION_MAP.get(direction_str) class WizMirrorParam: """Parameter names for mirror configuration.""" DIRECTION = "mirror_direction" LIMIT = "mirror_limit" COMMENT_TAG = "comment_tag" LIMIT_MIN = 1 LIMIT_MAX = 500 LIMIT_DEFAULT = 50 class WizMirrorField: """Field names used in mirror metadata (added to rawJSON).""" DIRECTION = "mirror_direction" INSTANCE = "mirror_instance" ID = "mirror_id" TAGS = "mirror_tags" XSOAR_MIRROR_MARKER = "Mirrored from Cortex XSOAR" DEFAULT_RESOLUTION_REASON = "WONT_FIX" WIZ_MIRRORED_FIELDS = ["status", "notes", "dueAt", "resolutionReason"] # XSOAR's built-in `closeReason` singleSelect values → Wiz `resolutionReason` enum. # The outgoing mapper declares `resolutionReason <- resolutionReason`, but the Wiz Issue # incident type has no XSOAR field by that name, so the mapper alone never carries the # analyst's chosen reason. We translate XSOAR's `closeReason` (always populated on close) # as a fallback so the user-selected close intent reaches Wiz instead of defaulting to WONT_FIX. XSOAR_CLOSE_REASON_TO_WIZ = { "Resolved": "ISSUE_FIXED", "False Positive": "FALSE_POSITIVE", "Duplicate": "WONT_FIX", "Other": "WONT_FIX", } def _resolve_wiz_reason(delta, data=None): """Resolve Wiz `resolutionReason` from a mirror delta, with `closeReason` fallback. Order of precedence: 1. `delta.resolutionReason` — wins if a future XSOAR config adds the proper field. 2. `data.closeReason` (then `delta.closeReason`) — translated via XSOAR_CLOSE_REASON_TO_WIZ. 3. None — caller falls back to DEFAULT_RESOLUTION_REASON. """ if delta: explicit = delta.get("resolutionReason") if explicit: return explicit close_reason = None if data: close_reason = data.get("closeReason") if not close_reason and delta: close_reason = delta.get("closeReason") if not close_reason: return None return XSOAR_CLOSE_REASON_TO_WIZ.get(close_reason) def set_authentication_endpoint(auth_endpoint): global AUTH_E AUTH_E = generate_auth_urls(AUTH_DEFAULT)[1] if auth_endpoint == "" else auth_endpoint def set_api_endpoint(api_endpoint): global URL URL = api_endpoint def generate_auth_urls(prefix): auth_url = f"{prefix}.{URL_SUFFIX}" http_auth_url = f"https://{auth_url}" return auth_url, http_auth_url def generate_auth_urls_fed(prefix): auth_url = f"{prefix}.{URL_SUFFIX_FED}" http_auth_url = f"https://{auth_url}" return auth_url, http_auth_url def get_token(): """ Retrieve the token using the credentials """ audience = "" cognito_list = [] for cognito_prefix in COGNITO_PREFIX: cognito_list.extend(generate_auth_urls(cognito_prefix)) cognito_list.extend(generate_auth_urls_fed(cognito_prefix)) auth0_list = [] for auth0_prefix in AUTH0_PREFIX: auth0_list.extend(generate_auth_urls(auth0_prefix)) # check Wiz portal auth endpoint - Cognito or Auth0 if AUTH_E in cognito_list: audience = "wiz-api" elif AUTH_E in auth0_list: audience = "beyond-api" else: raise Exception("Not a valid authentication endpoint") demisto_params = demisto.params() said = demisto_params.get("credentials").get("identifier") sasecret = demisto_params.get("credentials").get("password") auth_payload = parse.urlencode( {"grant_type": "client_credentials", "audience": audience, "client_id": said, "client_secret": sasecret} ) try: response = requests.post(AUTH_E, headers=HEADERS_AUTH, data=auth_payload, timeout=API_REQUEST_TIMEOUT) except requests.Timeout: raise Exception( f"Wiz authentication request timed out after {API_REQUEST_TIMEOUT}s. Check Wiz API availability and retry." ) if response.status_code != requests.codes.ok: raise Exception(f"Error authenticating to Wiz [{response.status_code}] - {response.text}") try: response_json = response.json() TOKEN = response_json.get("access_token") if not TOKEN: demisto.debug(json.dumps(response_json)) message = "Could not retrieve token from Wiz: {}".format(response_json.get("message")) raise Exception(message) except ValueError as exception: demisto.debug(exception) raise Exception("Could not parse API response") HEADERS["Authorization"] = "Bearer " + TOKEN return TOKEN def checkAPIerrors(query, variables): if not TOKEN: get_token() data = {"variables": variables, "query": query} demisto.info(f"Invoking the API with {json.dumps(data)}") try: response = requests.post(url=URL, json=data, headers=HEADERS, timeout=API_REQUEST_TIMEOUT) except requests.Timeout: raise Exception( f"Wiz API request timed out after {API_REQUEST_TIMEOUT}s. " "Heavy queries (e.g., TOXIC_COMBINATION issue_type) may need a narrower filter." ) response_json = response.json() demisto.info(f"Wiz API response status code is {response.status_code}") demisto.debug(f"The response is {response_json}") error_message = "" if "errors" in response_json: error_message = f"Wiz API error details: {get_error_output(response_json)}" elif "data" in response_json and "issues" in response_json["data"] and len(response_json["data"]["issues"].get("nodes")) == 0: demisto.info("No Issue(/s) available to fetch.") if error_message: demisto.error("An error has occurred using:\n" f"\tQuery: {query}\n" f"\tVariables: {variables}\n" f"\t{error_message}") demisto.error(error_message) raise Exception(f"{error_message}\nCheck 'server.log' instance file to get additional information") return response_json FETCH_ALL_ISSUES_BUDGET_SECONDS = 240 # 5-min Docker timeout - 60s safety margin def _fetch_all_issue_nodes(query, variables, deadline_seconds=FETCH_ALL_ISSUES_BUDGET_SECONDS, max_records=None): """ Fetch all issue nodes from a paginated issues query. Bounded by `deadline_seconds` (default 240) to avoid the 5-min Docker script timeout that hard-kills the process. On budget exhaustion, returns the partial result accumulated so far and logs a warning. Callers that require completeness should narrow their filter; callers that tolerate partial results (mirror is the exception — it has its own single-page code path) get an actionable signal instead of a script crash. When `max_records` is set, pagination stops once that many nodes are accumulated and the result is truncated to exactly `max_records`. This gives callers a deterministic ceiling regardless of how many issues match, independent of the time budget. `None` keeps the fetch-everything behavior. """ variables = dict(variables) started = time.monotonic() response_json = checkAPIerrors(query, variables) nodes = list(response_json.get("data", {}).get("issues", {}).get("nodes", [])) while response_json.get("data", {}).get("issues", {}).get("pageInfo", {}).get("hasNextPage"): if max_records is not None and len(nodes) >= max_records: break if time.monotonic() - started > deadline_seconds: demisto.info( f"_fetch_all_issue_nodes: hit {deadline_seconds}s budget after {len(nodes)} nodes; " "returning partial result. Narrow your filter to retrieve more." ) break variables["after"] = response_json["data"]["issues"]["pageInfo"]["endCursor"] response_json = checkAPIerrors(query, variables) page_nodes = response_json.get("data", {}).get("issues", {}).get("nodes", []) if page_nodes: nodes += page_nodes if max_records is not None and len(nodes) > max_records: nodes = nodes[:max_records] return nodes def translate_severity(issue): """ Translate issue severity to demisto Might take risk grade into account in the future """ severity = demisto.get(issue, WizInputParam.SEVERITY) if severity == "CRITICAL": return 4 if severity == "HIGH": return 3 if severity == "MEDIUM": return 2 if severity == "LOW": return 1 if severity == "INFORMATIONAL": return 0.5 return None def build_incidents(issue): if issue is None: demisto.debug("build_incidents: Received None issue") return {} try: issue_id = issue.get("id", "unknown") source_rule = issue.get("sourceRule") if source_rule is None: demisto.debug("build_incidents: sourceRule is None") rule_name = "No sourceRule" else: rule_name = source_rule.get("name", "No sourceRule") demisto.debug(f"build_incidents: rule_name: {rule_name}") incident_name = f"{rule_name or 'Unknown Rule'} - {issue_id}" created_at = issue.get("createdAt", "") severity = translate_severity(issue) _attach_mirror_metadata(issue) incident = { "name": incident_name, "occurred": created_at, "rawJSON": json.dumps(issue), "severity": severity, } demisto.debug(f"build_incidents: Successfully created incident for {issue_id} " f"using {incident}") return incident except Exception as e: issue_id = issue.get("id", "unknown") if issue else "unknown" raise Exception(f"build_incidents: Error processing issue {issue_id}: {str(e)}") def validate_wiz_enum_parameter(parameter_value, enum_class, parameter_name): """ Generic validation function for Wiz enum parameters Args: parameter_value (str or list): The parameter value(s) to validate enum_class: The enum class that contains valid values (e.g., WizIssueType) parameter_name (str): The human-readable parameter name for error messages (e.g., "issue type") Returns: ValidationResponse: Response with validation results """ if not parameter_value: return ValidationResponse.create_success() values = argToList(parameter_value) valid_values = enum_class.values() invalid_values = [v for v in values if v not in valid_values] if invalid_values: error_msg = ( f"Invalid {parameter_name}(s): {', '.join(invalid_values)}. Valid {parameter_name}s are: {', '.join(valid_values)}" ) demisto.error(error_msg) return ValidationResponse.create_error(error_msg) return ValidationResponse.create_success(values) def validate_issue_type(issue_type): """ Validates if the issue type is supported Args: issue_type (str or list): The issue_type(s) to validate Returns: ValidationResponse: Response with validation results """ return validate_wiz_enum_parameter(issue_type, WizIssueType, "issue type") def validate_severity(severity): """ Validates if the severity is supported Args: severity (str or list): The severity(s) to validate Returns: ValidationResponse: Response with validation results """ return validate_wiz_enum_parameter(severity, WizSeverity, "severity") def validate_status(status): """ Validates if the status is supported Args: status (str or list): The status(es) to validate Returns: ValidationResponse: Response with validation results """ return validate_wiz_enum_parameter(status, WizStatus, "status") def validate_all_issues_parameters(parameters_dict): """ Validates all parameters in a centralized function Args: parameters_dict (dict): Dictionary containing all parameters to validate Returns: tuple: (success, error_message, validated_values) - success (bool): True if all validations pass - error_message (str): Error message if validation fails - validated_values (dict): Dictionary of validated values """ validated_values = {} # Extract parameters from dictionary issue_type = parameters_dict.get(WizInputParam.ISSUE_TYPE) status = parameters_dict.get(WizInputParam.STATUS) severity = parameters_dict.get(WizInputParam.SEVERITY) issue_type_validation = validate_issue_type(issue_type) if not issue_type_validation.is_valid: return False, issue_type_validation.error_message, None validated_values[WizInputParam.ISSUE_TYPE] = issue_type_validation.value status_validation = validate_status(status) if not status_validation.is_valid: return False, status_validation.error_message, None validated_values[WizInputParam.STATUS] = status_validation.value severity_validation = validate_severity(severity) if not severity_validation.is_valid: return False, severity_validation.error_message, None validated_values[WizInputParam.SEVERITY] = severity_validation.value return True, None, validated_values def apply_wiz_filter(variables, filter_value, api_field, equals_wrapper=True, nested_path=None): """ Generic function to apply filters to Wiz API query variables Args: variables (dict): The query variables to modify filter_value (str or list): The filter value(s) to apply api_field (str): The API field name (e.g., WizApiVariables.ORIGIN) equals_wrapper (bool): Whether to wrap the value in {"equals": [values]} structure nested_path (str): Additional nested path for complex filters (e.g., "relatedEntity") Returns: dict: Updated variables with the filter applied """ if not filter_value: return variables # Initialize filterBy if it doesn't exist if "filterBy" not in variables: variables["filterBy"] = {} # Convert single values to list for consistency if isinstance(filter_value, str): value_list = [filter_value] elif isinstance(filter_value, list): value_list = filter_value else: value_list = [filter_value] # Handle nested paths (e.g., for threats that use relatedEntity.cloudPlatform) filter_target = variables["filterBy"] if nested_path: if nested_path not in filter_target: filter_target[nested_path] = {} filter_target = filter_target[nested_path] # Apply the filter with or without equals wrapper if equals_wrapper: filter_target[api_field] = {"equals": value_list} else: filter_target[api_field] = value_list return variables def apply_severity_filter(variables, severity_list, is_detection=True): """Adds the severity filter to the query variables""" return apply_wiz_filter(variables, severity_list, "severity", equals_wrapper=False) def apply_status_filter(variables, status_list): """Adds the status filter to the query variables""" return apply_wiz_filter(variables, status_list, "status", equals_wrapper=False) def apply_issue_type_filter(variables, type_list): """Adds the status filter to the query variables""" return apply_wiz_filter(variables, type_list, "type", equals_wrapper=False) def apply_all_issue_filters(variables, validated_values): """ Applies all filters to the query variables in a centralized function Args: variables (dict): Base query variables validated_values (dict): Dictionary of validated values Returns: dict: Updated query variables with all filters applied """ variables = apply_severity_filter(variables, validated_values.get(WizInputParam.SEVERITY)) variables = apply_status_filter(variables, validated_values.get(WizInputParam.STATUS)) variables = apply_issue_type_filter(variables, validated_values.get(WizInputParam.ISSUE_TYPE)) return variables def get_fetch_issues_variables(max_fetch, last_run): demisto_params = demisto.params() parameters_dict = { WizInputParam.ISSUE_TYPE: demisto_params.get(WizInputParam.ISSUE_TYPE), WizInputParam.STATUS: demisto_params.get(WizInputParam.STATUS), WizInputParam.SEVERITY: demisto_params.get(WizInputParam.SEVERITY), } # Using default fetch parameters if ( not parameters_dict[WizInputParam.ISSUE_TYPE] and not parameters_dict[WizInputParam.STATUS] and not parameters_dict[WizInputParam.SEVERITY] ): demisto.info("No issue type, status or severity provided, fetching default issues") parameters_dict = { WizInputParam.STATUS: DEFAULT_FETCH_ISSUE_STATUS, } validation_success, error_message, validated_values = validate_all_issues_parameters(parameters_dict) if not validation_success or error_message: return_error(error_message) return None issue_variables: Dict[str, Any] = PULL_ISSUES_DEFAULT_VARIABLES.copy() issue_variables["first"] = max_fetch issue_variables["filterBy"] = {"createdAt": {"after": last_run}, "relatedEntity": {}} return apply_all_issue_filters(issue_variables, validated_values) def fetch_issues(max_fetch): """ Fetch all Issues (OOB XSOAR Fetch) """ if max_fetch > 500: max_fetch = 500 last_run = demisto.getLastRun().get("time") if not last_run: # first time fetch last_run = dateparser.parse(demisto.params().get("first_fetch", "7 days").strip()) last_run = last_run.isoformat()[:-3] + "Z" query = PULL_ISSUES_QUERY variables = get_fetch_issues_variables(max_fetch, last_run) demisto.info(f"Fetching Issues for {variables}") api_start_run_time = datetime.now().strftime(DEMISTO_OCCURRED_FORMAT) issues = _fetch_all_issue_nodes(query, variables) incidents = [] for issue in issues: incident = build_incidents(issue=issue) demisto.debug(f"Preparing to add incident: {incident}") incidents.append(incident) demisto.incidents(incidents) demisto.setLastRun({"time": api_start_run_time}) if incidents: demisto.info(f"Successfully fetched and created {len(incidents)} incidents - Set last run time to {api_start_run_time}.") else: demisto.info(f"No new incidents to fetch - Set last run time to {api_start_run_time}.") def get_issue(issue_id): demisto.info(f"Issue id is {issue_id}\n") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: demisto.debug(message) return message issue_variables = {"first": 5, "filterBy": {"id": issue_id}} response_json = checkAPIerrors(PULL_ISSUES_QUERY, issue_variables) demisto.debug(f"The API response is {response_json}") issues = {} if response_json["data"]["issues"]["nodes"] != []: issues = response_json["data"]["issues"]["nodes"] else: demisto.info(f"There was no result for Issue ID: {issue_id}") return issues def get_filtered_issues(entity_type, resource_id, severity, issue_type, limit, created_after=None, created_before=None): """ Retrieves Filtered Issues `limit` caps the total number of issues returned. When None, all matching issues are fetched (bounded only by the time budget in _fetch_all_issue_nodes). `created_after` / `created_before` are ISO 8601 timestamps mapped to the Wiz `createdAt` filter, letting callers bound heavy queries (e.g. TOXIC_COMBINATION) up front instead of relying on the time-budget safety net. """ demisto.info( f"Entity type is {entity_type}\n" f"Resource ID is {resource_id}\n" f"Severity is {severity}\n" f"Issue type is {issue_type}\n" f"Created after is {created_after}\n" f"Created before is {created_before}\n" f"Limit is {limit}" ) error_msg = "" if not severity and not entity_type and not resource_id and not issue_type and not created_after and not created_before: error_msg = ( "You should pass (at least) one of the following parameters:\n\tentity_type\n\tresource_id" "\n\tseverity\n\tissue_type\n\tcreated_after\n\tcreated_before\n" ) if entity_type and resource_id: error_msg = f"{error_msg}You cannot pass entity_type and resource_id together\n" if limit is not None and limit < 1: error_msg = f"{error_msg}limit must be a positive integer\n" if error_msg: demisto.error(error_msg) return error_msg # `first` is the per-page size (API max WIZ_API_LIMIT); `limit` is the total cap. page_size = WIZ_API_LIMIT if limit is None else min(limit, WIZ_API_LIMIT) issue_variables: Dict[str, Any] = {} query = PULL_ISSUES_QUERY if entity_type: issue_variables = { "first": page_size, "filterBy": { "status": ["OPEN", "IN_PROGRESS"], "relatedEntity": {"type": [entity_type]}, }, "orderBy": {"field": "SEVERITY", "direction": "DESC"}, } elif resource_id: get_resource_graph_id_helper_variables = { "projectId": "*", "query": {"type": ["CLOUD_RESOURCE"], "where": {"providerUniqueId": {"EQUALS": resource_id}}}, } get_resource_graph_id_helper_query = """ query GraphEntityResourceFilterAutosuggest( $query: GraphEntityQueryInput $projectId: String! ) { graphSearch( first: 100, query: $query, quick: true, projectId: $projectId ) { nodes { entities { id name type } } } } """ graph_resource_response_json = checkAPIerrors(get_resource_graph_id_helper_query, get_resource_graph_id_helper_variables) if graph_resource_response_json["data"]["graphSearch"]["nodes"] != []: graph_resource_id = graph_resource_response_json["data"]["graphSearch"]["nodes"][0]["entities"][0]["id"] issue_variables = { "first": page_size, "filterBy": {"status": ["OPEN", "IN_PROGRESS"], "relatedEntity": {"id": graph_resource_id}}, "orderBy": {"field": "SEVERITY", "direction": "DESC"}, } else: demisto.info("Resource not found.") return "Resource not found." if severity: if "filterBy" not in issue_variables: issue_variables["filterBy"] = {"severity": []} issue_variables["first"] = page_size if severity.upper() == "CRITICAL": issue_variables["filterBy"]["severity"] = ["CRITICAL"] elif severity.upper() == "HIGH": issue_variables["filterBy"]["severity"] = ["CRITICAL", "HIGH"] elif severity.upper() == "MEDIUM": issue_variables["filterBy"]["severity"] = ["CRITICAL", "HIGH", "MEDIUM"] elif severity.upper() == "LOW": issue_variables["filterBy"]["severity"] = ["CRITICAL", "HIGH", "MEDIUM", "LOW"] elif severity.upper() == "INFORMATIONAL": issue_variables["filterBy"]["severity"] = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"] else: demisto.info( "You should only use these severity types: CRITICAL, HIGH, MEDIUM, LOW or INFORMATIONAL " "in upper or lower case." ) return ( "You should only use these severity types: CRITICAL, HIGH, MEDIUM, LOW or INFORMATIONAL in " "upper or lower case." ) if issue_type: if "filterBy" not in issue_variables: issue_variables["filterBy"] = {} issue_variables["first"] = page_size issue_variables["filterBy"]["type"] = [issue_type] if created_after or created_before: if "filterBy" not in issue_variables: issue_variables["filterBy"] = {} issue_variables["first"] = page_size created_at: Dict[str, str] = {} if created_after: created_at["after"] = created_after if created_before: created_at["before"] = created_before issue_variables["filterBy"]["createdAt"] = created_at # Order by severity so a `limit` truncation keeps the most severe issues, not an arbitrary page. issue_variables.setdefault("orderBy", {"field": "SEVERITY", "direction": "DESC"}) demisto.info(f"Query is {query}") demisto.info(f"Issue variables is {issue_variables}") issues = _fetch_all_issue_nodes(query, issue_variables, max_records=limit) return issues def get_resources( search, entity_type, subscription_external_ids, provider_unique_ids, project_ids=None, native_types=None, updated_at_before=None, updated_at_after=None, ): """ Retrieves Resources """ demisto.info( f"Entity type is {entity_type}\n" f"Search is {search}\n" f"Subscription External IDs is {subscription_external_ids}\n" f"Provider Unique IDs is {provider_unique_ids}\n" f"Project IDs is {project_ids}\n" f"Native Types is {native_types}\n" f"Updated At Before is {updated_at_before}\n" f"Updated At After is {updated_at_after}" ) error_msg = "" if ( not search and not entity_type and not subscription_external_ids and not provider_unique_ids and not project_ids and not native_types and not updated_at_before and not updated_at_after ): error_msg = ( f"You should pass (at least) one of the following parameters:\n\t{WizInputParam.SEARCH}\n\t" f"{WizInputParam.ENTITY_TYPE}\n\t{WizInputParam.SUBSCRIPTION_EXTERNAL_IDS}\n\t" f"{WizInputParam.PROVIDER_UNIQUE_IDS}\n\t{WizInputParam.PROJECT_IDS}\n\t" f"{WizInputParam.NATIVE_TYPES}\n\t{WizInputParam.UPDATED_AT_BEFORE}\n\t" f"{WizInputParam.UPDATED_AT_AFTER}\n" ) if error_msg: demisto.error(error_msg) return error_msg variables: Dict[str, Any] = {"first": WIZ_API_LIMIT, "filterBy": {}} if search: variables["filterBy"]["search"] = search if entity_type: variables["filterBy"]["type"] = [entity_type] if subscription_external_ids: subscription_external_ids_formatted = [str(x) for x in re.split(r"[,\s]+", subscription_external_ids.strip())] variables["filterBy"]["subscriptionExternalId"] = subscription_external_ids_formatted if provider_unique_ids: provider_unique_ids_formatted = [str(x) for x in re.split(r"[,\s]+", provider_unique_ids.strip())] variables["filterBy"]["providerUniqueId"] = provider_unique_ids_formatted if project_ids: project_ids_formatted = [str(x) for x in re.split(r"[,\s]+", project_ids.strip())] variables["filterBy"]["projectId"] = project_ids_formatted if native_types: native_types_formatted = [str(x) for x in re.split(r"[,\s]+", native_types.strip())] variables["filterBy"]["nativeType"] = native_types_formatted if updated_at_before or updated_at_after: updated_at: Dict[str, str] = {} if updated_at_before: updated_at["before"] = updated_at_before if updated_at_after: updated_at["after"] = updated_at_after variables["filterBy"]["updatedAt"] = updated_at try: response_json = checkAPIerrors(PULL_CLOUD_RESOURCES_NATIVE_QUERY, variables) except DemistoException: demisto.debug( f"could not find resources with this entity_type {entity_type}, search {search}, " f"subscription_external_ids {subscription_external_ids}, provider_unique_ids {provider_unique_ids}" ) return {} if response_json["data"]["cloudResources"]["nodes"] is None or not response_json["data"]["cloudResources"]["nodes"]: demisto.info("Resources Not Found") return "Resources Not Found" else: return response_json def get_resource(resource_id, resource_name): """ Retrieves Resource Details """ demisto.debug("get_resource, enter") if resource_name and resource_id: demisto.error("You cannot pass both resource_name and resource_id together") return "You should pass exactly one of resource_name or resource_id" if not resource_name and not resource_id: demisto.error("You must pass either resource_name or resource_id") return "You should pass exactly one of resource_name or resource_id" resource_search = resource_name if resource_name else resource_id variables = {"first": WIZ_API_LIMIT, "filterBy": {"search": resource_search}} try: response_json = checkAPIerrors(PULL_CLOUD_RESOURCES_NATIVE_QUERY, variables) except DemistoException: demisto.debug(f"could not find resource with this resource_name {resource_name}") return {} if response_json["data"]["cloudResources"]["nodes"] is None or not response_json["data"]["cloudResources"]["nodes"]: demisto.info("Resource Not Found") return "Resource Not Found" else: return response_json def reject_issue(issue_id, reject_reason, reject_comment): """ Reject a Wiz Issue """ return reject_or_resolve_issue(issue_id, reject_reason, reject_comment, "REJECTED") def resolve_issue(issue_id, resolution_reason, resolution_note): """ Resolve a Wiz Threat Detection Issue. `status=RESOLVED` is only valid for Threat Detection issues. Non-Threat-Detection types (Toxic Combination, Cloud Configuration, Attack Surface) are auto-resolved when the underlying problem is fixed; use wiz-reject-issue for those types. """ is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message issue_type = _get_issue_type(issue_id) if issue_type is None: return f"Issue not found: {issue_id}" if issue_type != WizIssueType.THREAT_DETECTION: msg = ( f"Only a Threat Detection Issue can be resolved.\n" f"Received an Issue of type {issue_type}.\n" f"Use wiz-reject-issue for non-Threat-Detection issues." ) demisto.error(msg) return msg return reject_or_resolve_issue(issue_id, resolution_reason, resolution_note, "RESOLVED") def reject_or_resolve_issue(issue_id, reject_or_resolve_reason, reject_or_resolve_comment, status): """ Reject a Wiz Issue """ demisto.debug(f"reject_issue with status: {status}, enter") operation = "reject" if status == "REJECTED" else "resolution" is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message if not reject_or_resolve_reason or not reject_or_resolve_comment: demisto.error(f"You should pass all of: Issue ID, {operation} reason and {operation} note.") return f"You should pass all of: Issue ID, {operation} reason and {operation} note." variables = { "issueId": issue_id, "patch": { "status": status, "note": truncate_note(reject_or_resolve_comment), "resolutionReason": reject_or_resolve_reason, }, } query = UPDATE_ISSUE_QUERY response = checkAPIerrors(query, variables) return response def reopen_issue(issue_id, reopen_note): """ Re-open a Wiz Issue """ demisto.debug("reopen_issue, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message variables = {"issueId": issue_id, "patch": {"status": "OPEN"}} query = UPDATE_ISSUE_QUERY response = checkAPIerrors(query, variables) if reopen_note: set_issue_comment(issue_id, reopen_note) return response def issue_in_progress(issue_id): """ Set a Wiz Issue to In Progress """ return _set_status(issue_id, "IN_PROGRESS") def _set_status(issue_id, status): """ Set a Wiz Issue to In Progress """ demisto.debug(f"_set_status to {status}, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message variables = {"issueId": issue_id, "patch": {"status": status}} query = UPDATE_ISSUE_QUERY response = checkAPIerrors(query, variables) return response def _get_issue(issue_id, is_evidence=False): is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message issue_variables = { "first": 1, "filterBy": {"id": issue_id}, } issue_query = PULL_ISSUE_WITH_EVIDENCE_PARAM_QUERY if is_evidence else PULL_ISSUES_QUERY issue_response = checkAPIerrors(issue_query, issue_variables) return issue_response def _get_issue_type(issue_id): """ Return the Wiz Issue `type` (e.g. THREAT_DETECTION, TOXIC_COMBINATION) for an issue id, or None if the issue can't be found / has no type. Used to guard status mutations that are only valid for certain issue types. """ issue_object = _get_issue(issue_id, is_evidence=False) if not isinstance(issue_object, dict): return None nodes = issue_object.get("data", {}).get("issues", {}).get("nodes") or [] if not nodes: return None return nodes[0].get("type") def _can_resolve_issue(issue_id): """ Return True only when the issue may be manually set to RESOLVED in Wiz. Only Threat Detection issues can be manually resolved; all other types (Toxic Combination, Cloud Configuration, Attack Surface) auto-resolve when the underlying problem is remediated, and the backend rejects an explicit RESOLVED for them. The outgoing-mirror paths use this to skip the RESOLVED push for non-Threat-Detection issues (letting Wiz auto-resolve) instead of firing a mutation the backend will reject and silently swallowing the error. """ issue_type = _get_issue_type(issue_id) if issue_type != WizIssueType.THREAT_DETECTION: demisto.info( f"Skipping RESOLVED mirror for issue {issue_id}: type {issue_type} cannot be manually resolved " f"(only Threat Detection issues can; other types auto-resolve when remediated)." ) return False return True def truncate_note(text): """ Truncate a note to MAX_NOTE_LENGTH characters. If truncated, appends '... [truncated]' within the limit. """ if not text or len(text) <= MAX_NOTE_LENGTH: return text suffix = "... [truncated]" return text[: MAX_NOTE_LENGTH - len(suffix)] + suffix def set_issue_comment(issue_id, comment): """ Set a note on Wiz Issue """ is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message comment = truncate_note(comment) issue_variables = {"input": {"issueId": issue_id, "text": comment}} issue_query = CREATE_COMMENT_QUERY response = checkAPIerrors(issue_query, issue_variables) return response def get_error_output(wiz_api_response): error_output_message = "" first_error_message = "" if "errors" in wiz_api_response: for error_dict in wiz_api_response["errors"]: if "message" in error_dict: error_message = error_dict["message"] # Do not print duplicate errors if first_error_message and first_error_message == error_message: continue if not first_error_message: first_error_message = error_message error_output_message = error_output_message + error_message + "\n" return error_output_message if error_output_message else wiz_api_response def clear_issue_note(issue_id): """ Clear the note from a Wiz Issue """ demisto.debug("clear_issue_note, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message issue_object = _get_issue(issue_id) issue_notes = issue_object["data"]["issues"]["nodes"][0].get("notes") or [] demisto.info(f"The issue notes are: {issue_notes}") query = DELETE_NOTE_QUERY response = None for note in issue_notes: variables = {"input": {"id": note["id"]}} response = checkAPIerrors(query, variables) return response def set_issue_due_date(issue_id, due_at): """ Set a due date for a Wiz Issue """ demisto.debug("set_issue_due_date, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message if not due_at: demisto.error("issue_id and due_at parameters must be provided.") return "issue_id and due_at parameters must be provided." format = "%Y-%m-%d" try: datetime.strptime(due_at, format) demisto.info("This is the correct date string format.") except ValueError: demisto.error("This is the incorrect. It should be YYYY-MM-DD") return "The date format is the incorrect. It should be YYYY-MM-DD" due_at = due_at + "T00:00:00.000Z" variables = {"issueId": issue_id, "patch": {"dueAt": due_at}} query = UPDATE_ISSUE_QUERY try: response = checkAPIerrors(query, variables) except DemistoException: demisto.debug(f"could not find Issue with ID {issue_id}") return {} return response def clear_issue_due_date(issue_id): """ Clear a due date for a Wiz Issue """ demisto.debug("clear_issue_due_date, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message issue_query = UPDATE_ISSUE_QUERY issue_variables = {"issueId": issue_id, "override": {"dueAt": None}} issue_response = checkAPIerrors(issue_query, issue_variables) return issue_response def get_issue_evidence(issue_id): """ Get evidence on a Wiz Issue """ demisto.debug("get_issue_evidence, enter") is_valid_id, message = is_valid_issue_id(issue_id) if not is_valid_id: return message # Getting the Issue Evidence Query issue_object = _get_issue(issue_id, is_evidence=True) if not issue_object["data"]["issues"]["nodes"]: return f"Issue not found: {issue_id}" issue_type = issue_object["data"]["issues"]["nodes"][0]["type"] if issue_type == "THREAT_DETECTION": if issue_object["data"]["issues"]["nodes"][0]["threatDetectionDetails"] is not None: return issue_object["data"]["issues"]["nodes"][0]["threatDetectionDetails"] else: return f"No issue threat detection details evidence for Issue ID: {issue_id}" query_for_evidence = issue_object["data"]["issues"]["nodes"][0]["evidenceQuery"] if not query_for_evidence: return f"No issue evidence for Issue ID: {issue_id}" # Creating the query/variables to get the Issue Evidence query = PULL_ISSUE_EVIDENCE_QUERY variables = PULL_ISSUE_EVIDENCE_PARAMS variables["query"] = query_for_evidence try: response = checkAPIerrors(query, variables) except Exception as e: error_message = f"Failed getting Issue evidence on ID {issue_id}.\nError details: {str(e)}" demisto.error(error_message) raise Exception(error_message) if response.get("data", {}).get("graphSearch", {}).get("nodes") is None: return "Resource Not Found" elif len(response.get("data", {}).get("graphSearch", {}).get("nodes")) == 0: return "No Evidence Found" else: return response["data"]["graphSearch"]["nodes"][0].get("entities", {}) def get_project_team(project_name): """ Get the Project Owners and Security Champions details """ demisto.debug("wiz-get-project-team, enter") if not project_name: demisto.error("You should pass a Project name") return "You should pass an Project name." # find VM on the graph project_variables = { "first": 20, "filterBy": {"search": project_name, "includeArchived": False}, "orderBy": {"field": "SECURITY_SCORE", "direction": "ASC"}, } try: response_json = checkAPIerrors(PULL_PROJECTS_QUERY, project_variables) except DemistoException: demisto.debug(f"Error with finding Project with name {project_name}") return {} nodes = response_json.get("data", {}).get("projects", {}).get("nodes", []) if not nodes: demisto.error(f"Project with name {project_name} does not exist") return {} return nodes def copy_to_forensics_account(resource_id): """ Copy resource Volumes to a Forensics Account """ demisto.info(f"resource id is {resource_id}\n") demisto.debug("copy_to_forensics_account, enter") if not is_valid_uuid(resource_id): variables = {"first": 1, "filterBy": {"providerUniqueId": [resource_id]}} resource_id_response = checkAPIerrors(PULL_RESOURCES_ID_NATIVE_QUERY, variables) if resource_id_response["data"] is None or not resource_id_response["data"]["cloudResources"]["nodes"]: demisto.error(f"Resource with ID {resource_id} not found.") return f"Resource with ID {resource_id} not found." else: resource_id = resource_id_response["data"]["cloudResources"]["nodes"][0]["id"] copy_to_forensics_account_variables = {"input": {"id": resource_id}} response_json = checkAPIerrors(COPY_TO_FORENSICS_ACCOUNT_MUTATION, copy_to_forensics_account_variables) demisto.debug(f"The API response is {response_json}") if response_json["data"] is None and response_json["errors"] is not None: demisto.error(f"Resource with ID {resource_id} was not copied to Forensics Account.") return f"Resource with ID {resource_id} was not copied to Forensics Account. error: {response_json['errors']}" elif not response_json["data"]["copyResourceForensicsToExternalAccount"]["systemActivityGroupId"]: demisto.info(f"Resource with ID {resource_id} was not copied to Forensics Account.") return {} else: return response_json def get_mapping_fields_command(): mapping_response = GetMappingFieldsResponse() incident_type_scheme = SchemeTypeMapping(type_name="Wiz Issue") for field in WIZ_MIRRORED_FIELDS: incident_type_scheme.add_field(field) mapping_response.add_scheme_type(incident_type_scheme) return mapping_response MIRROR_CURSOR_KEY = "mirror_cursor" def get_modified_remote_data_command(args): """ Returns IDs of issues whose status changed since `last_update`. Single-page-per-call to stay under the 5-min Docker timeout. A persistent cursor in integration context (`mirror_cursor`) lets us drain large backlogs across consecutive mirror cycles instead of one giant call. Filter semantics: - filterBy.statusChangedAt.after is exclusive - When filterBy.statusChangedAt is set, results auto-order by statusChangedAt ASC - Issues with no statusChangedAt are filtered out automatically Known limitation: if multiple issues share the same microsecond-precision status_changed_at and a page boundary splits them, the trailing tied issues on the next page are skipped (the `>` filter excludes the cursor value). In practice rare; would require simultaneous bulk status changes from multiple writers. Documented test: test_get_modified_remote_data_microsecond_tie_known_loss. """ remote_args = GetModifiedRemoteDataArgs(args) last_update = remote_args.last_update or "" raw_limit = demisto.params().get(WizMirrorParam.LIMIT, WizMirrorParam.LIMIT_DEFAULT) try: raw_limit = int(raw_limit) except (ValueError, TypeError): raw_limit = WizMirrorParam.LIMIT_DEFAULT mirror_limit = max(WizMirrorParam.LIMIT_MIN, min(raw_limit, WizMirrorParam.LIMIT_MAX)) ctx = demisto.getIntegrationContext() or {} saved_cursor = ctx.get(MIRROR_CURSOR_KEY, "") or "" # Compare via datetime, not lex: XSOAR's lastUpdate is second-precision while # saved_cursor is microsecond-precision (Wiz). Lex-max would pick bare-Z over # the chronologically-later microsecond value (`Z` > `.`), rewinding the cursor. last_update_dt = _parse_iso_timestamp(last_update) saved_cursor_dt = _parse_iso_timestamp(saved_cursor) if saved_cursor_dt and (not last_update_dt or saved_cursor_dt >= last_update_dt): cursor = saved_cursor else: cursor = last_update or saved_cursor demisto.debug( f"get_modified_remote_data: cursor={cursor} " f"(last_update={last_update}, saved_cursor={saved_cursor}, limit={mirror_limit})" ) variables = { "first": mirror_limit, "filterBy": {"statusChangedAt": {"after": cursor}}, "orderBy": {"field": "STATUS_CHANGED_AT", "direction": "ASC"}, } response_json = checkAPIerrors(MODIFIED_ISSUE_IDS_QUERY, variables) nodes = response_json.get("data", {}).get("issues", {}).get("nodes", []) or [] modified_ids = [n["id"] for n in nodes if n.get("id")] if nodes: page_max = max((n["statusChangedAt"] for n in nodes if n.get("statusChangedAt")), default="") if page_max: page_max_dt = _parse_iso_timestamp(page_max) saved_dt = _parse_iso_timestamp(saved_cursor) if not saved_dt or (page_max_dt and page_max_dt > saved_dt): ctx[MIRROR_CURSOR_KEY] = page_max demisto.setIntegrationContext(ctx) demisto.debug(f"get_modified_remote_data: cursor advanced to {page_max}") has_next_page = response_json.get("data", {}).get("issues", {}).get("pageInfo", {}).get("hasNextPage", False) demisto.debug( f"get_modified_remote_data: returned {len(modified_ids)} ids, hasNextPage={has_next_page}, " f"new cursor={ctx.get(MIRROR_CURSOR_KEY, '')}" ) return GetModifiedRemoteDataResponse(modified_ids) def get_remote_data_command(args): parsed_args = GetRemoteDataArgs(args) issue_id = parsed_args.remote_incident_id last_update = parsed_args.last_update demisto.debug(f"get_remote_data: issue_id={issue_id}, last_update={last_update}") issues = get_issue(issue_id) if not issues or isinstance(issues, str): return GetRemoteDataResponse({}, []) issue = issues[0] _attach_mirror_metadata(issue) entries = _build_new_note_entries(issue, last_update) return GetRemoteDataResponse(issue, entries) def _attach_mirror_metadata(issue): """Add mirror metadata fields to issue dict.""" mirror_direction = WizMirrorDirection.from_params() if mirror_direction: issue[WizMirrorField.DIRECTION] = mirror_direction issue[WizMirrorField.INSTANCE] = demisto.integrationInstance() issue[WizMirrorField.ID] = issue.get("id") issue[WizMirrorField.TAGS] = [demisto.params().get(WizMirrorParam.COMMENT_TAG, "comments")] def _parse_iso_timestamp(value): """Parse an ISO-8601 timestamp tolerating `Z` suffix and fractional seconds. Lex-comparison of ISO strings is unsafe across precisions: `.` (0x2E) sorts before `Z` (0x5A), so `"2025-01-01T00:00:00.500000Z" < "2025-01-01T00:00:00Z"` even though it's chronologically later. Wiz returns microsecond precision while XSOAR's lastUpdate is second-precision — comparing them as strings silently drops notes added in the sub-second window. """ if not value: return None try: return datetime.fromisoformat(value.replace("Z", "+00:00")) except (ValueError, TypeError, AttributeError): return None def _build_new_note_entries(issue, last_update): """Build war room entries for notes added since last_update. First sync (no last_update) returns []: we intentionally avoid back-filling the entire pre-existing note history into a fresh war room. Subsequent syncs surface only notes newer than the previous sync. """ entries: list[dict] = [] if not last_update: return entries last_update_dt = _parse_iso_timestamp(last_update) for note in issue.get("notes") or []: if XSOAR_MIRROR_MARKER in note.get("text", ""): continue note_time = note.get("updatedAt") or note.get("createdAt", "") note_dt = _parse_iso_timestamp(note_time) is_newer = (note_dt and last_update_dt and note_dt > last_update_dt) or ( not (note_dt and last_update_dt) and note_time > last_update ) if is_newer: author = "" if note.get("user"): author = note["user"].get("name", "") elif note.get("serviceAccount"): author = f"[SA] {note['serviceAccount'].get('name', '')}" entries.append( { "Type": entryTypes["note"], "Contents": f"**{author}** ({note_time}):\n{note.get('text', '')}", "ContentsFormat": formats["markdown"], "Note": True, } ) return entries def update_remote_system_command(args): parsed_args = UpdateRemoteSystemArgs(args) remote_id = parsed_args.remote_incident_id if not remote_id: demisto.debug("update_remote_system: no remote_id, skipping") return remote_id demisto.debug(f"update_remote_system: remote_id={remote_id}") incident_closed = parsed_args.inc_status == IncidentStatus.DONE if parsed_args.incident_changed and parsed_args.delta: _handle_field_changes(remote_id, parsed_args.delta, data=parsed_args.data, skip_status=incident_closed) if incident_closed: resolution_reason = _resolve_wiz_reason(parsed_args.delta, parsed_args.data) _handle_incident_closed(remote_id, resolution_reason=resolution_reason) if parsed_args.entries: _handle_outgoing_entries(remote_id, parsed_args.entries) return remote_id def _handle_field_changes(remote_id, delta, data=None, skip_status=False): """Push field-level changes (status, due date) to Wiz.""" new_status = delta.get("status") if new_status and not skip_status: _mirror_status_to_wiz(remote_id, new_status, delta, data=data) if "dueAt" in delta: new_due_date = delta.get("dueAt") elif "wizissueduedate" in delta: new_due_date = delta.get("wizissueduedate") else: new_due_date = None if new_due_date is not None: if new_due_date: set_issue_due_date(issue_id=remote_id, due_at=new_due_date) else: clear_issue_due_date(issue_id=remote_id) def _mirror_status_to_wiz(issue_id, xsoar_status, delta, data=None): """Map XSOAR status string to Wiz issue status mutation.""" status_lower = str(xsoar_status).lower() try: if status_lower in ("resolved", "done", "closed"): if not _can_resolve_issue(issue_id): return resolution_reason = _resolve_wiz_reason(delta, data) or DEFAULT_RESOLUTION_REASON reject_or_resolve_issue(issue_id, resolution_reason, "Status mirrored from Cortex XSOAR", "RESOLVED") elif status_lower in ("rejected",): reject_reason = _resolve_wiz_reason(delta, data) or DEFAULT_RESOLUTION_REASON reject_or_resolve_issue(issue_id, reject_reason, "Status mirrored from Cortex XSOAR", "REJECTED") elif status_lower in ("active", "open", "reopened"): reopen_issue(issue_id=issue_id, reopen_note="") elif status_lower in ("in_progress", "in progress"): issue_in_progress(issue_id=issue_id) else: demisto.debug(f"_mirror_status_to_wiz: unmapped status '{xsoar_status}'") except Exception as e: demisto.error(f"_mirror_status_to_wiz: failed to update status to '{xsoar_status}': {e}") def _handle_incident_closed(remote_id, resolution_reason=None): """Handle XSOAR incident closed → resolve Wiz issue.""" reason = resolution_reason or DEFAULT_RESOLUTION_REASON demisto.debug(f"_handle_incident_closed: resolving {remote_id} with reason={reason}") try: if not _can_resolve_issue(remote_id): return reject_or_resolve_issue(remote_id, reason, "Resolved from Cortex XSOAR", "RESOLVED") except Exception as e: demisto.info(f"_handle_incident_closed: failed (may already be resolved): {e}") def _handle_outgoing_entries(remote_id, entries): """Push tagged XSOAR entries as Wiz issue notes. Defense-in-depth: even though XSOAR's mirror engine only forwards entries matching `dbotMirrorTags` (set from `comment_tag`), we re-check the tag here to prevent leaking arbitrary war-room content to Wiz if a customer renames `comment_tag` mid-flight (old incidents still carry the prior tag value) or if XSOAR's tag-filtering behavior changes in a future version. """ comment_tag = demisto.params().get(WizMirrorParam.COMMENT_TAG, "comments") for entry in entries: contents = entry.get("contents", "") if not contents: continue entry_tags = entry.get("tags") or [] if comment_tag not in entry_tags: demisto.debug(f"_handle_outgoing_entries: skipping entry without '{comment_tag}' tag (tags={entry_tags})") continue user = entry.get("user", "XSOAR") or "XSOAR" text = f"({user}): {contents}\n\n{XSOAR_MIRROR_MARKER}" try: set_issue_comment(issue_id=remote_id, comment=text) except Exception as e: demisto.error(f"_handle_outgoing_entries: failed to add comment: {e}") def is_valid_uuid(uuid_string): if not isinstance(uuid_string, str): uuid_string = str(uuid_string) try: uuid_obj = uuid.UUID(uuid_string) return str(uuid_obj) == uuid_string except ValueError: return False except Exception: return False def is_valid_issue_id(issue_id): if not issue_id: demisto.error("You should pass an Issue ID.") return False, "You should pass an Issue ID." if not is_valid_uuid(issue_id): demisto.error("Wrong format: The Issue ID should be in UUID format.") return False, "Wrong format: The Issue ID should be in UUID format." return True, f"The Issue ID {issue_id} is in a valid format" def main(): params = demisto.params() demisto_args = demisto.args() set_authentication_endpoint(params.get("auth_endpoint")) set_api_endpoint(params.get("api_endpoint", "")) try: command = demisto.command() if command == "test-module": auth_token = get_token() if "error" not in auth_token: test_response = checkAPIerrors(PULL_ISSUES_QUERY, PULL_ISSUES_TEST_VARIABLES) if "errors" not in test_response: demisto.results("ok") else: demisto.results(test_response) else: demisto.results("Invalid token") elif command == "fetch-incidents": max_fetch = int(params.get("max_fetch", "200")) fetch_issues(max_fetch=max_fetch) elif command == "wiz-get-issues": issue_type = demisto_args.get(WizInputParam.ISSUE_TYPE) resource_id = demisto_args.get(WizInputParam.RESOURCE_ID) severity = demisto_args.get(WizInputParam.SEVERITY) entity_type = demisto_args.get(WizInputParam.ENTITY_TYPE) created_after = demisto_args.get(WizInputParam.CREATED_AFTER) created_before = demisto_args.get(WizInputParam.CREATED_BEFORE) # No limit arg => None => fetch all (preserves prior behavior, where the # passed value only sized pages and pagination drained every match). limit = arg_to_number(demisto_args.get(WizInputParam.LIMIT), arg_name=WizInputParam.LIMIT) issues = get_filtered_issues( issue_type=issue_type, resource_id=resource_id, severity=severity, entity_type=entity_type, limit=limit, created_after=created_after, created_before=created_before, ) if isinstance(issues, str): # this means the Issue is an error command_result = CommandResults(readable_output=issues, raw_response=issues) else: command_result = CommandResults(outputs_prefix="Wiz.Manager.Issues", outputs=issues, raw_response=issues) return_results(command_result) elif command == "wiz-get-resource": resource_id = demisto_args.get(WizInputParam.RESOURCE_ID) resource_name = demisto_args.get(WizInputParam.RESOURCE_NAME) resource = get_resource(resource_id=resource_id, resource_name=resource_name) command_result = CommandResults( outputs_prefix="Wiz.Manager.Resource", readable_output=resource, outputs=resource, raw_response=resource ) return_results(command_result) elif command == "wiz-get-resources": resources_search = demisto_args.get(WizInputParam.SEARCH) resources_entity_type = demisto_args.get(WizInputParam.ENTITY_TYPE) resources_subscription_external_ids = demisto_args.get(WizInputParam.SUBSCRIPTION_EXTERNAL_IDS) resources_provider_unique_ids = demisto_args.get(WizInputParam.PROVIDER_UNIQUE_IDS) resources_project_ids = demisto_args.get(WizInputParam.PROJECT_IDS) resources_native_types = demisto_args.get(WizInputParam.NATIVE_TYPES) resources_updated_at_before = demisto_args.get(WizInputParam.UPDATED_AT_BEFORE) resources_updated_at_after = demisto_args.get(WizInputParam.UPDATED_AT_AFTER) resources = get_resources( search=resources_search, entity_type=resources_entity_type, subscription_external_ids=resources_subscription_external_ids, provider_unique_ids=resources_provider_unique_ids, project_ids=resources_project_ids, native_types=resources_native_types, updated_at_before=resources_updated_at_before, updated_at_after=resources_updated_at_after, ) command_result = CommandResults(readable_output=resources, raw_response=resources) return_results(command_result) elif command == "wiz-reject-issue": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) resolution_reason = demisto_args.get(WizInputParam.REJECT_REASON) resolution_note = demisto_args.get(WizInputParam.REJECT_NOTE) issue_response = reject_issue(issue_id=issue_id, reject_reason=resolution_reason, reject_comment=resolution_note) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-reopen-issue": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) reopen_note = demisto_args.get(WizInputParam.REOPEN_NOTE) issue_response = reopen_issue(issue_id=issue_id, reopen_note=reopen_note) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-resolve-issue": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) resolution_reason = demisto_args.get(WizInputParam.RESOLUTION_REASON) resolution_note = demisto_args.get(WizInputParam.RESOLUTION_NOTE) issue_response = resolve_issue( issue_id=issue_id, resolution_reason=resolution_reason, resolution_note=resolution_note ) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-get-issue": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) issue_result = get_issue( issue_id=issue_id, ) command_result = CommandResults(readable_output=issue_result, raw_response=issue_result) return_results(command_result) elif command == "wiz-issue-in-progress": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) issue_response = issue_in_progress(issue_id=issue_id) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-set-issue-note": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) note = demisto_args.get(WizInputParam.NOTE) issue_response = set_issue_comment(issue_id=issue_id, comment=note) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-clear-issue-note": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) issue_response = clear_issue_note(issue_id=issue_id) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-get-issue-evidence": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) issue_response = get_issue_evidence(issue_id=issue_id) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-set-issue-due-date": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) due_at = demisto_args.get("due_at") issue_response = set_issue_due_date(issue_id=issue_id, due_at=due_at) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-clear-issue-due-date": issue_id = demisto_args.get(WizInputParam.ISSUE_ID) issue_response = clear_issue_due_date(issue_id=issue_id) command_result = CommandResults(readable_output=issue_response, raw_response=issue_response) return_results(command_result) elif command == "wiz-get-project-team": project_name = demisto_args.get(WizInputParam.PROJECT_NAME) projects_response = get_project_team(project_name=project_name) command_result = CommandResults(readable_output=projects_response, raw_response=projects_response) return_results(command_result) elif command == "wiz-rescan-machine-disk": return_results( CommandResults(readable_output="This command is deprecated", raw_response="This command is deprecated") ) elif command == "wiz-copy-to-forensics-account": resource_id = demisto_args.get(WizInputParam.RESOURCE_ID) copy_mutation_response = copy_to_forensics_account(resource_id=resource_id) command_result = CommandResults(readable_output=copy_mutation_response, raw_response=copy_mutation_response) return_results(command_result) elif command == "get-mapping-fields": return_results(get_mapping_fields_command()) elif command == "get-modified-remote-data": return_results(get_modified_remote_data_command(demisto_args)) elif command == "get-remote-data": return_results(get_remote_data_command(demisto_args)) elif command == "update-remote-system": return_results(update_remote_system_command(demisto_args)) else: raise Exception("Unrecognized command: " + command) except Exception as err: demisto.error(traceback.format_exc()) return_error(f"An error occurred: {str(err)}") if __name__ in ("__main__", "__builtin__", "builtins"): main()