Darktrace ASM
This pack includes configurations to combine the world-class threat detection of Darktrace with the synchrony and automation abilities of XSOAR, allowing security teams to monitor their attack surface for risks, high-impact vulnerabilities and external threats.\nTo configure the connection to your Darktrace Attack Surface Management instance, you will provide:\n- Server URL of Darktrace ASM instance (ex: darktrace.yourcompany.com) and any necessary proxy information\n- The API Token from the Darktrace ASM instance.
Network Security · DarktraceASM
Details
| ID | Darktrace ASM |
|---|---|
| Provider | Darktrace |
| Category | Network Security |
| From Version | 6.6.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Darktrace is a Cyber AI platform for threat detection and response across cloud, email, industrial, and the network.
This integration was integrated and tested with version 6.0.0 of Darktrace
Configure Darktrace ASM in Cortex
| Parameter | Description | Required |
|---|---|---|
| url | Server URL (e.g. https://example.net) | True |
| isFetch | Fetch incidents | False |
| insecure | Trust any certificate (not secure) | False |
| api_token | API Token | True |
| alert_type | Incident types to fetch | False |
| min_severity | Minimum Risk severity to fetch | False |
| max_alerts | Maximum Risks per fetch | False |
| first_fetch | First fetch time | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
darktrace-asm-get-risk
Returns the Risk object associated with the given Risk ID.
Base Command
darktrace-asm-get-risk
Input
| Argument Name | Description | Required |
|---|---|---|
| risk_id | Darktrace ASM Risk ID | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.risk | dict | Darktrace Risk object. |
| Darktrace.risk.asset | dict | Darktrace ASM Asset object associated with the given Risk. |
| Darktrace.risk.asset.brand | string | Brand of associtated Asset. |
| Darktrace.risk.asset.tags | list | List of Tags associated with Asset. |
| Darktrace.risk.asset.id | string | Asset ID. |
| Darktrace.risk.asset.updatedAt | timestamp | Last time Asset was updated. |
| Darktrace.risk.asset.securityrating | string | Security rating of Asset. |
| Darktrace.risk.asset.isMalicious | boolean | Malicious state of the Asset. |
| Darktrace.risk.asset.createdAt | timestamp | Time Asset was created. |
| Darktrace.risk.asset.state | string | State of Asset. |
| Darktrace.risk.comments | dict | Dictionary of comments by comment ID. |
| Darktrace.risk.description | string | Description of Risk. |
| Darktrace.risk.endedAt | timestamp | End time of Risk. |
| Darktrace.risk.evidence | string | Evidence gathered indicating the Risk. |
| Darktrace.risk.id | string | Risk ID. |
| Darktrace.risk.mitigatedAt | timestamp | Mitigation time of Risk. |
| Darktrace.risk.proposedAction | string | Recommended action to solve Risk. |
| Darktrace.risk.securityRating | string | Security rating of Risk. |
| Darktrace.risk.startedAt | timestamp | Start time of Risk. |
| Darktrace.risk.title | string | Name of Risk. |
| Darktrace.risk.type | string | Type of Risk. |
Command Example
!darktrace-asm-get-risk risk_id=Umlza1R5cGU6MTE5Nzc=
Context Example
"risk": {
"id": "Umlza1R5cGU6MTE5Nzc=",
"type": "SSL",
"startedAt": "2022-05-27T18:38:45.439551+00:00",
"endedAt": "2023-06-07T09:59:49.344739+00:00",
"title": "HSTS header missing",
"description": "The HSTS header enforces users to always visit your website through SSL, after their first visit.",
"evidence": "No HSTS header present.",
"proposedAction": "Turn on the HSTS header, read more on https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html",
"asset": {
"id": "QXBwbGljYXRpb25UeXBlOjIyNjg0",
"state": "Unconfirmed",
"brand": "Darktrace",
"createdAt": "2022-05-27 14:18:24.264958+00:00",
"updatedAt": "2023-06-29 06:40:41.007652+00:00",
"securityrating": "f",
"isMalicious": true,
"tags": []
},
"securityRating": "b",
"mitigatedAt": 2023-06-06T09:59:49.344739+00:00,
"comments": {
"edges": [
{
"node": {
"id": "Q29tbWVudFR5cGU6ODM=",
"text": "API TEST EDIT"
}
}
]
}
}
Human Readable Output
Field Value asset id: QXBwbGljYXRpb25UeXBlOjIyNjg0
state: Unconfirmed
brand: Darktrace
createdAt: “2022-05-27 14:18:24.264958+00:00
updatedAt: 2023-06-29 06:40:41.007652+00:00
securityrating: f
isMalicious: true
tags: EXAMPLE_TAGcomments Q29tbWVudFR5cGU6ODM=: “XSOAR Test Comment”
Q29tbWVudFR5cGU6ODN=: “XSOAR Test Comment 2”descirption The HSTS header enforces users to always visit your website through SSL, after their first visit. endedAt 2023-06-07T09:59:49.344739+00:00 evidence No HSTS header present. id Umlza1R5cGU6MTE5Nzc= mitigatedAt 2023-06-06T09:59:49.344739+00:00 proposedAction Turn on the HSTS header, read more on https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html securityRating b startedAt 2022-05-27T18:38:45.439551+00:00 title HSTS header missing type SSL
darktrace-asm-get-asset
Returns the Asset object associated with the given Asset ID. The output will depend on the type of Asset(IP Address, Netblock, FQDN or Application).
Base Command
darktrace-asm-get-asset
Input
| Argument Name | Description | Required |
|---|---|---|
| asset_id | Darktrace ASM Asset ID | Required |
Context Output: All Asset types
| Path | Type | Description |
|---|---|---|
| Darktrace.asset | dict | Darktrace ASM Asset object. |
| Darktrace.asset.brand | string | Brand that the Asset is associated with. |
| Darktrace.asset.comments | list | List of comments by comment ID. |
| Darktrace.asset.createdAt | timestamp | Creation time of Asset. |
| Darktrace.asset.discoverySources | list | List of discovery sources. |
| Darktrace.asset.id | string | Asset ID. |
| Darktrace.asset.isMalicious | bool | Malicious state of Asset. |
| Darktrace.asset.risks | list | List of Risks associated with Asset. |
| Darktrace.asset.securityrating | string | Security rating of Asset. |
| Darktrace.asset.state | string | State of Asset. |
| Darktrace.asset.tags | list | List of tags applied to Asset within Darktrace UI. |
| Darktrace.asset.type | string | Type of Asset. |
| Darktrace.asset.updatedAt | timestamp | Last time Asset was updated. |
Context Output: Application Asset type
| Path | Type | Description |
|---|---|---|
| Darktrace.asset.fqdns | list | List of FQDNS associated with Asset. |
| Darktrace.asset.ipaddresses | list | List of IPs associated with Asset. |
| Darktrace.asset.protocol | string | Protocol associated with the Asset |
| Darktrace.asset.screenshot | string | Screenshot of webpage associated with Asset. |
| Darktrace.asset.technologies | list | List of technologies associated with Asset. |
| Darktrace.asset.uri | string | URI associated with Asset. |
Context Output: IP Address Asset type
| Path | Type | Description |
|---|---|---|
| Darktrace.asset.lat | string | Latitude of the Asset. |
| Darktrace.asset.lon | string | Longitude of the Asset. |
| Darktrace.asset.geoCity | string | City Asset is located. |
| Darktrace.asset.geoCountry | string | Country Asset is located. |
| Darktrace.asset.address | string | IP address of the Asset |
| Darktrace.asset.netblock | string | Netblock of the Asset. |
Context Output: FQDN Asset type
| Path | Type | Description |
|---|---|---|
| Darktrace.asset.name | string | Hostname associated with Asset. |
| Darktrace.asset.dnsRecords | string | DNS records associated with Asset. |
| Darktrace.asset.resolvesTo | list | List of IPs the Asset hostname resolves to. |
| Darktrace.asset.whois | string | WhoIs information associated with Asset. |
| Darktrace.asset.registeredDomain | string | Domain associated with Asset. |
Context Output: Netblock Asset type
| Path | Type | Description |
|---|---|---|
| Darktrace.asset.netname | string | Name of the Asset. |
| Darktrace.asset.ipAddresses | list | List of IP addresses associated with Asset. |
Command Example
!darktrace-asm-get-asset asset_id=QXBwbGljYXRpb25UeXBlOjI2NjI4
Context Example
"application": {
"brand": "Darktrace",
"comments": [
{
"id": "Q29tbWVudFR5cGU6OTc=",
"text": "Test comment"
}
],
"createdAt": "2022-06-27 18:34:50.473256+00:00",
"discoverySources": [
{
"id": "RGlzY292ZXJ5U291cmNlVHlwZTo1NDc0Ng==",
"description": "Record retrieved from FQDN careers.darktrace.com"
},
{
"id": "RGlzY292ZXJ5U291cmNlVHlwZTo1NDc1Nw==",
"description": "Application from https://careers.darktrace.com/"
}
],
"fqdns": [
{
"id": "RnFkblR5cGU6MjY2Mjc=",
"name": "careers.darktrace.com"
}
],
"id": "QXBwbGljYXRpb25UeXBlOjI2NjI4",
"ipaddresses": [
{
"id": "SVBBZGRyZXNzVHlwZToxNTU3Njc=",
"address": "1.1.1.1"
},
{
"id": "SVBBZGRyZXNzVHlwZToxNTU3Njg=",
"address": "1.1.1.1"
}
],
"isMalicious": false,
"risks": [
{
"id": "Umlza1R5cGU6NjYzNjA=",
"title": "Vulnerable software found - jquery ui/1.13.0 (highest CVE score 4.3)"
},
{
"id": "Umlza1R5cGU6MTU1ODQ=",
"title": "Excessive cookie lifetime (> 1 year)"
},
{
"id": "Umlza1R5cGU6MzQ4MzQ=",
"title": "Excessive cookie lifetime (> 1 year)"
}
],
"screenshot": "https://storage.googleapis.com/asm-prod-1931-z5b5n7ow5w-copy/http_screenshot/screenshot_155822.jpg?Expires=1710617440&GoogleAccessId=asm-prod-1931-cyberweb%40dt-asm-prod.iam.gserviceaccount.com&Signature=Vbz1hBo%2Bo3ZYTRvg5p%2F%2F%2FTFFf4PHRgPaVUrcpaDG8Kp%2BOT2dSm8O2NC1HFJXQW420yD2zppJ5IbOCt46vJ6LZMvx5kcdm7IY1U6yKbedRGACfbpUQaXEjmXN1gLhVawnoET94CYqnmlYue6%2Fy4B6cS4fZwvH6sllm2OnbDZ%2FZacoSw9Xmf214R0M%2FgY3OjKuXapaAnu779r5c8fkjL8cSvX8E8PzkxToGF9ysTNuWVqZc46H05xxUtb8QSauiggAijBeSLg%2Blol1wVj0ZuMP%2Fb1kJvXNpCr6x0Dem6ITe4C%2FPrbiqcNMvwSZChptiDBhgoXGRAm%2FRJokWqktST19Nw%3D%3D",
"securityrating": "b",
"state": "Confirmed",
"tags": [
"MANAGED BY INTERNAL DEV"
],
"updatedAt": "2023-08-21 00:31:57.299904+00:00",
"uri": "https://careers.darktrace.com",
"technologies": [
{
"id": "VGVjaG5vbG9neVR5cGU6MTU4MjY2",
"name": "Amazon ALB"
},
{
"id": "VGVjaG5vbG9neVR5cGU6MTU4MjY3",
"name": "Amazon Web Services"
},
{
"id": "VGVjaG5vbG9neVR5cGU6MTE1MjU3",
"name": "Bootstrap"
}
],
"protocol": "HTTP"
}
Human Readable Output
Field Value brand Darktrace comments Q29tbWVudFR5cGU6OTc=: “Test comment” createdAt 2022-06-27 18:34:50.473256+00:00 discoverySources RGlzY292ZXJ5U291cmNlVHlwZTo1NDc0Ng==: Record retrieved from FQDN careers.darktrace.com
RGlzY292ZXJ5U291cmNlVHlwZTo1NDc1Nw==: Application from https://careers.darktrace.com/fqdns RnFkblR5cGU6MjY2Mjc=: careers.darktrace.com id QXBwbGljYXRpb25UeXBlOjI2NjI4 ipaddresses SVBBZGRyZXNzVHlwZToxNTU3Njc=: 1.1.1.1
SVBBZGRyZXNzVHlwZToxNTU3Njg=: 1.1.1.1isMalicious false protocol HTTP risks Umlza1R5cGU6NjYzNjA=: Vulnerable software found - jquery ui/1.13.0 (highest CVE score 4.3)
Umlza1R5cGU6MTU1ODQ=: Excessive cookie lifetime (> 1 year)
Umlza1R5cGU6MzQ4MzQ=: Excessive cookie lifetime (> 1 year)screenshot https://storage.googleapis.com/asm-prod-1931-z5b5n7ow5w-copy/http_screenshot/screenshot_155822.jpg?Expires=1710617295&GoogleAccessId=asm-prod-1931-cyberweb%40dt-asm-prod.iam.gserviceaccount.com&Signature=HjT83fw4EV%2F6notDq7tQB24oAr049F4UZ8OUDJ3hiuAaD%2F3y7xFOniBLDyZNtZBMlUDDJgrG6%2BhXbuJ0Sdobhsk%2Bj6KZknqa6xao0eyv%2BT%2FQGysZSxol8YHn%2BykRBkX8Umajs%2F5KRR8GRWc46o7m%2FnW1Rdop4qUuGKPy82UUOWwbyfcI7yYOGH8nky2b0o95QyfvR4%2Fa4GeCEHL8cz8RksGh4imWICWcTDu18OlGNruI%2F0sAiivHVbzPnOnBBFwFunAIXez9THr5oItqIoTzV%2FrNdwIFHc0rRIvtvNpuUVcrQo7%2FqaDunYZSmPu0Hf6eaL7cR6ZbYbXuKchlr2eAOQ%3D%3D securityrating b state Confirmed tags MANAGED BY INTERNAL DEV technologies VGVjaG5vbG9neVR5cGU6MTU4MjY2: Amazon ALB
VGVjaG5vbG9neVR5cGU6MTU4MjY3: Amazon Web Services
VGVjaG5vbG9neVR5cGU6MTE1MjU3: Bootstraptype application updatedAt 2023-08-21 00:31:57.299904+00:00 uri https://careers.darktrace.com
darktrace-asm-mitigate-risk
Mitigates Risk within the Darktrace UI. Warning: Mitigating a Risk without taking action to resolve the Risk means you accept a Risk and it will no longer appear with the Darktrace UI.
Base Command
darktrace-asm-mitigate-risk
Input
| Argument Name | Description | Required |
|---|---|---|
| risk_id | Darktrace ASM Risk ID | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.risk.success | boolean | Status of mitigation. |
Command Example
!darktrace-asm-mitigate-risk risk_id=Umlza1R5cGU6MTE5Nzc=
Context Example
"closeRisk": {
"success": true,
}
Human Readable Output
Field Value success true
darktrace-asm-post-comment
Post a comment to a Risk or an Asset within the Darktrace UI.
Base Command
darktrace-asm-post-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Darktrace ASM Risk or Asset ID | Required |
| comment | Text of comment to be applied | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.comment.comment.id | string | Unique ID of Comment. |
| Darktrace.comment.comment.text | string | Text of Comment. |
| Darktrace.comment.success | boolean | Status of post. |
Command Example
!darktrace-asm-post-comment id=QXBwbGljYXRpb25UeXBlOjI2NjI4 comment="API Test Comment"
Context Example
"placeComment": {
"success": true,
"comment": {
"id": "Q29tbWVudFR5cGU6OTg=",
"text": "API Test Comment"
}
}
Human Readable Output
Field Value comment id: Q29tbWVudFR5cGU6OTg=
text: API Test Commentsuccess true
darktrace-asm-edit-comment
Edit an existing comment within the Darktrace UI.
Base Command
darktrace-asm-edit-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| comment_id | ID of comment to be edited | Required |
| comment | Text of comment to be applied | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.comment.comment.id | string | Unique ID of Comment. |
| Darktrace.comment.comment.text | string | Text of Comment. |
| Darktrace.comment.success | boolean | Status of edit. |
Command Example
!darktrace-asm-edit-comment comment_id=Q29tbWVudFR5cGU6OTg= comment="API Test Comment Edited"
Context Example
"editComment": {
"success": true,
"comment": {
"id": "Q29tbWVudFR5cGU6OTg=",
"text": "API Test Comment Edited"
}
}
Human Readable Output
Field Value comment id: Q29tbWVudFR5cGU6OTg=
text: API Test Comment Editedsuccess true
darktrace-asm-delete-comment
Delete an existing comment within the Darktrace UI.
Base Command
darktrace-asm-delete-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| comment_id | ID of comment to be deleted | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.comment.success | boolean | Status of deletion. |
Command Example
!darktrace-asm-delete-comment comment_id=Q29tbWVudFR5cGU6OTg=
Context Example
"deleteComment": {
"success": true
}
Human Readable Output
Field Value success true
darktrace-asm-create-tag
Creat a new Tag within the Darktrace UI. Tags can be applied to Assets.
Base Command
darktrace-asm-create-tag
Input
| Argument Name | Description | Required |
|---|---|---|
| tag_name | Name of Tag to create | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.tag.success | boolean | Status of creation. |
| Darktrace.tag.tag.id | string | Tag ID. |
| Darktrace.tag.tag.name | string | Name of Tag. |
Command Example
!darktrace-asm-create-tag tag_name="API TEST"
Context Example
"createTag": {
"success": true,
"tag": {
"id": "VGFnVHlwZTo1Mg==",
"name": "API TEST"
}
}
Human Readable Output
Field Value success true tag id: VGFnVHlwZTo1Mg==
name: API TEST
darktrace-asm-assign-tag
Assign an existing Tag to an Asset within the Darktrace UI.
Base Command
darktrace-asm-assign-tag
Input
| Argument Name | Description | Required |
|---|---|---|
| tag_name | Name of Tag to apply to Asset | Required |
| asset_id | Asset ID to apply Tag to | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.tag.success | boolean | Status of assignment. |
| Darktrace.tag.asset.id | string | Asset ID. |
| Darktrace.tag.asset.tags | list | List of Tags assigned to Asset. |
Command Example
!darktrace-asm-assign-tag tag_name="API TEST" asset_id=SVBBZGRyZXNzVHlwZTox
Context Example
"assignTag": {
"success": true,
"asset": {
"id": "SVBBZGRyZXNzVHlwZTox",
"tags": [
"API TEST"
]
}
}
Human Readable Output
Field Value asset id: SVBBZGRyZXNzVHlwZTox
tags: API TESTsuccess true
darktrace-asm-unassign-tag
Unssign an existing Tag from an Asset within the Darktrace UI.
Base Command
darktrace-asm-unassign-tag
Input
| Argument Name | Description | Required |
|---|---|---|
| tag_name | Name of Tag to remove from Asset | Required |
| asset_id | Asset ID to remove Tag from | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darktrace.tag.success | boolean | Status of assignment. |
| Darktrace.tag.asset.id | string | Asset ID. |
| Darktrace.tag.asset.tags | list | List of Tags assigned to Asset. |
Command Example
!darktrace-asm-unassign-tag tag_name="API TEST" asset_id=SVBBZGRyZXNzVHlwZTox
Context Example
"unassignTag": {
"success": true,
"asset": {
"id": "SVBBZGRyZXNzVHlwZTox",
"tags": []
}
}
Human Readable Output
Field Value asset id: SVBBZGRyZXNzVHlwZTox
tags:success true
Configuration parameters
url— Server URL (e.g. https://soar.monstersofhack.com) (required)isFetch— Fetch incidentsincidentType— Incident typemax_fetch— Maximum number of incidents per fetchapikey— API Key (required)alert_type— Fetch alerts with typemin_severity— Minimum severity of alerts to fetchfirst_fetch— First fetch timeincidentFetchInterval— Incidents Fetch Interval
Commands (9)
-
darktrace-asm-assign-tagAssign an existing tag to a Darktrace ASM Asset within the Darktrace UI.
-
darktrace-asm-create-tagCreate a tag to assign to Darktrace ASM Assets within the Darktrace UI.
-
darktrace-asm-delete-commentEdit a comment within the Darktrace UI.
-
darktrace-asm-edit-commentEdit a comment within the Darktrace UI.
-
darktrace-asm-get-assetGet a specific Darktrace ASM Asset.
-
darktrace-asm-get-riskGet a specific Darktrace ASM Risk.
-
darktrace-asm-mitigate-riskMitigate Darktrace ASM Risk within the Darktrace UI.
-
darktrace-asm-post-commentPost a comment to a Darktrace ASM risk or asset within the Darktrace UI.
-
darktrace-asm-unassign-tagUnassign an existing tag to a Darktrace ASM Asset within the Darktrace UI.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 """Imports""" import json import traceback from datetime import datetime from typing import Any import dateparser import urllib3 # Disable insecure warnings urllib3.disable_warnings() """CONSTANTS""" URL = demisto.params().get("url") ASM_URI = "/graph/v1.0/api" ASM_RISK_QUERY = """ id type startedAt endedAt title description evidence proposedAction securityRating mitigatedAt asset { id state brand createdAt updatedAt securityrating isMalicious tags } comments { edges { node { id text } } } """ ASM_ASSET_QUERY = """ id state brand createdAt updatedAt securityrating isMalicious tags comments { id text } discoverySources { id description } risks { id title } """ ASM_ASSET_QUERY_DICT = { "application": ASM_ASSET_QUERY + """ protocol uri fqdns { id name } ipaddresses { id address } screenshot technologies { id name } """, "fqdn": ASM_ASSET_QUERY + """ name dnsRecords resolvesTo { id address } whois registeredDomain { id name } """, "ipaddress": ASM_ASSET_QUERY + """ lat lon geoCity geoCountry address netblock { id netname } """, "netblock": ASM_ASSET_QUERY + """ netname ipAddresses { id address } """, } SEVERITY_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4} DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" MIN_SEVERITY_TO_FETCH = 1 MAX_INCIDENTS_TO_FETCH = 50 ALERT_TYPES = ["gdpr", "informational", "misconfiguration", "reported", "ssl", "vulnerable software"] PLEASE_CONTACT = "Please contact your Darktrace representative." DARKTRACE_API_ERRORS = { "SIGNATURE_ERROR": "API Signature Error. You have invalid credentials in your config.", "DATE_ERROR": "API Date Error. Check that the time on this machine matches that of the Darktrace instance.", "ENDPOINT_ERROR": f"Invalid Endpoint. - {PLEASE_CONTACT}", "PRIVILEGE_ERROR": "User has insufficient permissions to access the API endpoint.", "UNDETERMINED_ERROR": f"Darktrace was unable to process your request - {PLEASE_CONTACT}", "FAILED_TO_PARSE": "N/A", } """*****CUSTOM EXCEPTIONS*****""" class InvalidAssetStateError(Exception): def __init__(self, state: str): super().__init__(f'{state} is not a valid state. Valid states include "Confirmed" and "Unconfirmed".') class InvalidAssetID(Exception): def __init__(self, asset_id: Optional[str] = None): super().__init__(f'ASM Asset ID "{asset_id}" is not a valid ID.') class AssetNotFound(Exception): def __init__(self, asset_type: str, id: Optional[str] = None, message: Optional[str] = None): super().__init__(f'ASM {asset_type} Asset with id "{id}" not found. {message}') class MitigationError(Exception): def __init__(self, risk_id: str, message: Optional[str] = ""): super().__init__(f'Could not mitigate ASM Risk "{risk_id}" due to the following:\n{message}') class TagError(Exception): def __init__(self, action: str, name: str, id: Optional[str] = "", message: Optional[str] = ""): if action == "create": super().__init__(f'Could not create ASM Tag "{name}" due to the following:\n{message}') if action == "assign": super().__init__(f'Could not assign ASM Tag "{name}" to ASM object "{id}" due to the following:\n{message}') if action == "unassign": super().__init__(f'Could not unassign ASM Tag "{name}" from ASM object "{id}" due to the following:\n{message}') class CommentError(Exception): def __init__(self, action: str, id: Optional[str] = "", message: Optional[str] = ""): if action == "post": super().__init__(f'Could not post comment to ASM object "{id}" due to the following:\n{message}') elif action == "edit": super().__init__(f'Could not edit comment "{id}" due to the following:\n{message}') elif action == "delete": super().__init__(f'Could not delete comment "{id}" due to the following:\n{message}') """*****CLIENT CLASS***** Wraps all the code that interacts with the Darktrace API.""" class Client(BaseClient): """Client class to interact with the Darktrace API This Client implements API calls, and does not contain any Demisto logic. Should only do requests and return data. It inherits from BaseClient defined in CommonServer Python. Most calls use _http_request() that handles proxy, SSL verification, etc. """ def asm_post(self, query_uri: str, json: Optional[dict] = None): headers = self._headers return self._asm_api_call(query_uri, method="POST", json=json, headers=headers) def _asm_api_call( self, query_uri: str, method: str, params: Optional[dict] = None, data: Optional[dict] = None, json: Optional[dict] = None, headers: Optional[dict[str, str]] = None, ): """Handles Darktrace API calls""" try: res = self._http_request( method, url_suffix=query_uri, params=params, data=data, json_data=json, resp_type="response", headers=headers, error_handler=self.error_handler, ) if res.status_code not in [200, 204]: raise Exception( "Your request failed with the following error: " + str(res.content) + ". Response Status code: " + str(res.status_code) ) except Exception as e: raise Exception(e) try: return res.json() except Exception as e: raise ValueError(f"Failed to process the API response - {e!s}") def error_handler(self, res: requests.Response): """Handles authentication errors""" if res.status_code == 400: raise_message = "Invalid field names in query:\n" for error in res.json()["errors"]: error_message = error["message"] error_location = f"Line {error['locations'][0]['line']}, column {error['locations'][0]['column']}." raise_message += f"{error_message} {error_location}\n" raise ValueError(raise_message) elif res.status_code == 401: raise PermissionError(f"Authentication issue: {res.json()['detail']}") elif res.status_code == 404: raise ValueError("Error 404. Page not found." + PLEASE_CONTACT) elif res.status_code == 500: raise ValueError("Error 500. Possibly malformed GraphQL query.") def get_asm_risk(self, risk_id: str): """Function to query for specific Risk using a Risk ID. :type risk_id: ``str`` :param risk_id: Unique ID of Risk to query for. :return: dict containing Risk info. :rtype: ``Dict[str, Any]`` """ query = f"""query risk {{ risk(id:"{risk_id}") {{ id type startedAt endedAt title description evidence proposedAction securityRating mitigatedAt asset {{ id state brand createdAt updatedAt securityrating isMalicious tags }} comments {{ edges {{ node {{ id text }} }} }} }} }} """ payload = {"query": query} response = self.asm_post(ASM_URI, payload) return response["data"]["risk"] def mitigate_asm_risk(self, risk_id: str): """Function to manually mitigate a Risk given a specific Risk ID. :type risk_id: ``str`` :param risk_id: Unique ID of Risk to mitigate. :return: dict containing status of mitigation. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ closeRisk(id:"{risk_id}") {{ success }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["closeRisk"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise MitigationError(risk_id, errors_string) return response["data"]["closeRisk"] def get_asm_asset(self, asset_id: str): """Function to query for a specific Asset using the Asset's ID. :type asset_id: ``str`` :param asset_id: Unique ID of Asset to query for. :return: dict containing Asset info. :rtype: ``Dict[str, Any]`` """ if asset_id[0] == "Q": asset_type = "application" elif asset_id[0] == "R": asset_type = "fqdn" elif asset_id[0] == "T": asset_type = "netblock" elif asset_id[0] == "S": asset_type = "ipaddress" else: raise InvalidAssetID(asset_id) query_type = ASM_ASSET_QUERY_DICT[asset_type] query = f"""query {asset_type} {{ {asset_type}(id:"{asset_id}") {{ {query_type} }} }}""" payload = {"query": query} response = self.asm_post(ASM_URI, payload) if not response["data"][asset_type]: message = response["errors"][0]["message"] raise AssetNotFound(asset_type, asset_id, message) asset = response["data"][asset_type] asset["type"] = asset_type return asset def post_asm_comment(self, id: str, comment: str): """Function to post a comment to a Risk or Asset given a specific ID and comment string. :type id: ``str`` :param id: Unique ID of Risk or Asset to comment on. :type comment: ``str`` :param comment: Text of comment. :return: dict containing status of comment. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ placeComment(text:"{comment}", objectId:"{id}") {{ success comment {{ id text }} }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["placeComment"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise CommentError("post", id, errors_string) return response["data"]["placeComment"] def edit_asm_comment(self, comment_id: str, comment: str): """Function to edit an existing comment given the comment's ID and a new comment string. :type comment_id: ``str`` :param comment_id: Unique ID of Comment to edit. :type comment: ``str`` :param comment: New text to replace old Comment. :return: dict containing status of comment edit. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ editComment(text:"{comment}", id:"{comment_id}") {{ success comment {{ id text }} }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["editComment"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise CommentError("edit", comment_id, errors_string) return response["data"]["editComment"] def delete_asm_comment(self, comment_id: str): """Function to delete an existing comment given the comment's ID. :type comment_id: ``str`` :param comment_id: Unique ID of Comment to delete. :return: dict containing status of comment deletion. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ deleteComment(id:"{comment_id}") {{ success }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["deleteComment"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise CommentError("delete", comment_id, errors_string) return response["data"]["deleteComment"] def create_asm_tag(self, tag_name: str): """Function to create a new Tag. :type tag_name: ``str`` :param tag_name: Label of new Tag. :return: dict including the status and info of the new Tag. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ createTag(name:"{tag_name}") {{ success tag {{ id name }} }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["createTag"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise TagError("create", name=tag_name, message=errors_string) return response["data"]["createTag"] def assign_asm_tag(self, tag_name: str, asset_id: str): """Function to assign an existing tag to an Asset. :type tag_name: ``str`` :param tag_name: Label of Tag to assign. :type asset_id: ``str`` :param asset_id: Unique ID of Asset to apply Tag to. :return: dict including the status of assignment and info on Asset. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ assignTag(id:"{asset_id}", tagName:"{tag_name}") {{ success asset {{ id tags }} }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["assignTag"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise TagError("assign", id=asset_id, name=tag_name, message=errors_string) return response["data"]["assignTag"] def unassign_asm_tag(self, tag_name: str, asset_id: str): """Function to unassign an existing tag from an Asset. :type tag_name: ``str`` :param tag_name: Label of Tag to unassign. :type asset_id: ``str`` :param asset_id: Unique ID of Asset to remove Tag from. :return: dict including the status of unassignment and info on Asset. :rtype: ``Dict[str, Any]`` """ mutation = f"""mutation MyMutation {{ unassignTag(id:"{asset_id}", tagName:"{tag_name}") {{ success asset {{ id tags }} }} }} """ payload = {"query": mutation} response = self.asm_post(ASM_URI, payload) if not response["data"]["unassignTag"]: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) raise TagError("unassign", id=asset_id, name=tag_name, message=errors_string) return response["data"]["unassignTag"] def get_asm_risks(self, start_time) -> List[Dict[str, Any]]: """Function to pull all Risks after a given start time. :type start_time: ``datetime`` :param start_time: Date to start pulling Risks from. :return: list of Risk dicts. :rtype: ``List[Dict[str, Any]]`` """ start_string = start_time.strftime(DATE_FORMAT) query = f"""query allRisks {{ allRisks(startedAt:"{start_string}", orderBy:"startedAt") {{ edges {{ node {{ {ASM_RISK_QUERY} }} }} }} }} """ payload = {"query": query} response = self.asm_post(ASM_URI, payload) return response["data"]["allRisks"]["edges"] """*****HELPER FUNCTIONS****""" def arg_to_timestamp(arg: Any, arg_name: str, required: bool = False) -> float: """Converts an XSOAR argument to a timestamp (seconds from epoch) This function is used to quickly validate an argument provided to XSOAR via ``demisto.args()`` into an ``int`` containing a timestamp (seconds since epoch). It will throw a ValueError if the input is invalid. If the input is None, it will throw a ValueError if required is ``True``, or ``None`` if required is ``False. :type arg: ``Any`` :param arg: argument to convert :type arg_name: ``str`` :param arg_name: argument name :type required: ``bool`` :param required: throws exception if ``True`` and argument provided is None :return: returns a ``float`` containing a timestamp (seconds from epoch) if conversion works otherwise throws an Exception :rtype: ``float`` """ if arg is None: if required is True: raise ValueError(f"Missing '{arg_name}'") raise ValueError(f"'{arg_name}' cannot be None.") if isinstance(arg, str) and arg.isdigit(): # timestamp is a str containing digits - we just convert it to int return int(arg) if isinstance(arg, str): # we use dateparser to handle strings either in ISO8601 format, or # relative time stamps. # For example: format 2019-10-23T00:00:00 or "3 days", etc date = dateparser.parse(arg, settings={"TIMEZONE": "UTC"}) if date is None: # if d is None it means dateparser failed to parse it raise ValueError(f"Invalid date: {arg_name}") return float(date.timestamp()) if isinstance(arg, int): # Convert to float if the input is an int return float(arg) raise ValueError(f"Invalid date: '{arg_name}'") def check_required_fields(args, *fields): """Checks that required fields are found, raises a value error otherwise. :type args: ``Dict[str, Any]`` :param args: dict of arguments to search for given fields within. :type fields: ``str`` :param fields: Required fields to check for. :raise: ValueError if any fields not in args. """ for field in fields: if field not in args: raise ValueError(f"Argument error could not find {field} in {args}") def format_JSON_for_risk(risk: dict[str, Any]) -> dict[str, Any]: """Formats JSON for get_risk command, specifically reformat comments from API response. :type risk: ``Dict[str, Any]`` :param risk: JSON risk as returned by API for fetch incident. :return: Reformatted JSON risk. :rtype: ``Dict[str, Any]`` """ new_json: Dict[str, Any] = {} for key in risk: if key == "comments": if risk[key] is None: new_json[key] = {} else: comments = {comment["node"]["id"]: comment["node"]["text"] for comment in risk[key]["edges"]} new_json[key] = comments else: new_json[key] = risk[key] decoded_risk_id = decode_asm_id(risk["id"]) decoded_asset_id = decode_asm_id(risk["asset"]["id"]) new_json["risk_url"] = f"{URL}/app/#/detail/direct-risks/{decoded_asset_id}?risk_id={decoded_risk_id}" new_json["asset"]["asset_url"] = f"{URL}/app/#/detail/overview/{decoded_asset_id}" return new_json def format_JSON_for_asset(asset: dict[str, Any]) -> dict[str, Any]: """Formats JSON for get_asm command, specifically lists of dicts. :type asset: ``Dict[str, Any]`` :param asset: JSON asset as returned by API. :return: Reformatted JSON asset. :rtype: ``Dict[str, Any]`` """ new_json: Dict[str, Any] = {} for key in asset: if key == "comments": if asset[key] is None: new_json[key] = {} else: comments = {comment["id"]: comment["text"] for comment in asset[key]} new_json[key] = comments elif key == "discoverySources": if asset[key] is None: new_json[key] = {} else: sources = {source["id"]: source["description"] for source in asset[key]} new_json[key] = sources elif key == "risks": if asset[key] is None: new_json[key] = {} else: risks = {risk["id"]: risk["title"] for risk in asset[key]} new_json[key] = risks elif key in ["fqdns", "technologies"]: if asset[key] is None: new_json[key] = {} else: values = {value["id"]: value["name"] for value in asset[key]} new_json[key] = values elif key in ["ipaddresses", "ipAddresses", "resolvesTo"]: if asset[key] is None: new_json[key] = {} else: addresses = {ip["id"]: ip["address"] for ip in asset[key]} new_json[key] = addresses else: new_json[key] = asset[key] decoded_asset_id = decode_asm_id(asset["id"]) new_json["asset_url"] = f"{URL}/app/#/detail/overview/{decoded_asset_id}" return new_json def _compute_xsoar_severity(security_rating: str) -> int: """Translates Darktrace ASM security rating into XSOAR Severity. :type security_rating: ``str`` :param security_rating: ASM security rating to convert. :return: Integer equivalent of XSOAR severity scores. :rtype: ``int`` """ if security_rating in ["c", "d"]: return 2 if security_rating in ["e"]: return 3 if security_rating in ["f"]: return 4 return 1 def decode_asm_id(id: str): """Given a base64 encoded ASM ID returns the decoded id. Works for both Risk and Asset IDs. :type id: ``str`` :param id: Base64 encoded ASM ID. :return: Decoded ID of ASM object used in UI. :rtype: ``str`` """ decoded = str(base64.b64decode(id), encoding="utf-8").split(":")[1] return decoded """*****COMMAND FUNCTIONS****""" def test_module(client: Client, first_fetch_time: float) -> str: """ Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. :type client: ``Client`` :param client: Darktrace Client. :type first_fetch_time: ``float`` :param first_fetch_time: First fetch time. :return: A message to indicate the integration works as it is supposed to. :rtype: ``str`` """ try: first_fetch_datetime = datetime.fromtimestamp(first_fetch_time) client.get_asm_risks(start_time=first_fetch_datetime) except DemistoException as e: if "Forbidden" in str(e): return "Authorization Error: make sure API Key is correctly set" else: raise e return "ok" def fetch_incidents( client: Client, last_run: dict[str, str], first_fetch_time: float, max_alerts: int, min_severity: int, alert_types: list[str] ) -> tuple[dict[str, Any], list[dict]]: """Function used to pull incidents into XSOAR every few minutes.""" # Get the last fetch time, if exists # last_run is a dict with a single key, called last_fetch last_fetch = last_run.get("last_fetch", None) first_fetch_datetime = datetime.fromtimestamp(first_fetch_time) # Handle first fetch time if last_fetch is None: last_fetch_datetime = first_fetch_datetime else: last_fetch_datetime = datetime.strptime(last_fetch, DATE_FORMAT) latest_created_time = last_fetch_datetime # Each incident is a dict with a string as a key incidents: List[Dict[str, Any]] = [] asm_risks: List[Dict[str, Any]] = client.get_asm_risks(start_time=last_fetch_datetime) for alert in asm_risks: # Convert startedAt time to datetime object and add to alert # grabbing first 26 characters from start time since that provides ms resolution incident_created_time = datetime.strptime(alert["node"]["startedAt"][:26], DATE_FORMAT) # to prevent duplicates, we are only adding incidents with creation_time > last fetched incident if last_fetch_datetime and incident_created_time <= last_fetch_datetime: demisto.debug( f"""Incident created time: {incident_created_time} was part of a previous poll cycle. Last fetch time: {last_fetch_datetime}""" ) continue brand = alert.get("node", {}).get("asset", {}).get("brand") title = alert["node"]["title"] incident_name = f"Darktrace ASM | Risk Title: {title} | Brand: {brand}" xsoar_severity = _compute_xsoar_severity(alert["node"]["securityRating"]) # Skip incidents with a lower severity score than the desired minimum if xsoar_severity < min_severity: demisto.debug(f"Incident severity: {xsoar_severity} is lower than chosen minimum threshold: {min_severity}") continue incident_type = alert["node"]["type"].lower() # Skip incidents with a type not included in the chosen alert types to ingest if incident_type not in alert_types: demisto.debug(f"Alert type {incident_type} is not part of chosen alerts: {alert_types}") continue incident = { "name": incident_name, "occurred": alert["node"]["startedAt"], "rawJSON": json.dumps(alert["node"]), "severity": xsoar_severity, } incidents.append(incident) # Update last run and add incident if the incident is newer than last fetch if incident_created_time > latest_created_time: latest_created_time = incident_created_time if len(incidents) >= max_alerts: break # Save the next_run as a dict with the last_fetch key to be stored next_run = {"last_fetch": latest_created_time.strftime(DATE_FORMAT)} return next_run, incidents def get_asm_risk_command(client: Client, args: dict[str, Any]) -> CommandResults: check_required_fields(args, "risk_id") risk_id = str(args.get("risk_id", None)) response = client.get_asm_risk(risk_id) formatted_response = format_JSON_for_risk(response) readable_output = tableToMarkdown("Darktrace ASM Risk", formatted_response) return CommandResults( readable_output=readable_output, outputs_prefix="Darktrace.risk", outputs_key_field="id", outputs=response ) def mitigate_asm_risk_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "risk_id") risk_id = str(args.get("risk_id", None)) client.mitigate_asm_risk(risk_id) readable_output = f"Successfully mitigated risk. Risk ID: {risk_id}" return readable_output def get_asm_asset_command(client: Client, args: dict[str, Any]) -> CommandResults: check_required_fields(args, "asset_id") asset_id = str(args.get("asset_id", None)) response = client.get_asm_asset(asset_id) formatted_response = format_JSON_for_asset(response) readable_output = tableToMarkdown("Darktrace ASM Asset", formatted_response) return CommandResults( readable_output=readable_output, outputs_prefix="Darktrace.asset", outputs_key_field="id", outputs=response ) def post_asm_comment_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "id", "comment") id = str(args.get("id", None)) comment = str(args.get("comment", None)) response = client.post_asm_comment(id, comment) if response.get("success", False): comment_dict = response.get("comment", {}) readable_output = ( 'Comment successful.\n' f'Comment ID: {comment_dict.get("id", "Failed to get comment ID.")}\n' f'Comment Text: {comment_dict.get("text", "Failed to get comment text.")}' ) else: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) readable_output = f"Comment failed due to following errors:\n{errors_string}" return readable_output def edit_asm_comment_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "comment_id", "comment") comment_id = str(args.get("comment_id", None)) comment = str(args.get("comment", None)) response = client.edit_asm_comment(comment_id, comment) if response.get("success", False): comment_dict = response.get("comment", {}) readable_output = ( 'Comment successfully edited.\n' f'Comment ID: {comment_dict.get("id", "Failed to get comment ID.")}\n' f'Comment Text: {comment_dict.get("text", "Failed to get comment text.")}' ) else: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) readable_output = f"Failed to edit comment due to following errors:\n{errors_string}" return readable_output def delete_asm_comment_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "comment_id") comment_id = str(args.get("comment_id", None)) response = client.delete_asm_comment(comment_id) if response.get("success", False): readable_output = f"Comment successfully deleted. Comment ID: {comment_id}" else: errors = [error.get("message", "") for error in response.get("errors", {})] errors_string = "\n".join(errors) readable_output = f"Comment deletion failed due to following errors:\n{errors_string}" return readable_output def create_asm_tag_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "tag_name") tag_name = str(args.get("tag_name", None)) client.create_asm_tag(tag_name) # TODO: add error handling depending on XSOAR response on best practice readable_output = f"Successfully created tag {tag_name}." return readable_output def assign_asm_tag_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "tag_name", "asset_id") tag_name = str(args.get("tag_name", None)) asset_id = str(args.get("asset_id", None)) response = client.assign_asm_tag(tag_name, asset_id) asset = response.get("asset") tags = asset.get("tags") tags_string = "\n".join(tags) readable_output = f"Successfully assigned tag {tag_name} to asset {asset_id}. Tags applied to asset:\n{tags_string}" return readable_output def unassign_asm_tag_command(client: Client, args: dict[str, Any]) -> str: check_required_fields(args, "tag_name", "asset_id") tag_name = str(args.get("tag_name", None)) asset_id = str(args.get("asset_id", None)) response = client.unassign_asm_tag(tag_name, asset_id) asset = response.get("asset") tags = asset.get("tags") tags_string = "\n".join(tags) readable_output = f"Successfully unassigned tag {tag_name} from asset {asset_id}. Tags applied to asset:\n{tags_string}" return readable_output """*****MAIN FUNCTIONS**** Takes care of reading the integration parameters via the ``demisto.params()`` function, initializes the Client class and checks the different options provided to ``demisto.commands()``, to invoke the correct command function passing to it ``demisto.args()`` and returning the data to ``return_results()``. If implemented, ``main()`` also invokes the function ``fetch_incidents()``with the right parameters and passes the outputs to the ``demisto.incidents()`` function. ``main()`` also catches exceptions and returns an error message via ``return_error()``. """ def main() -> None: # pragma: no cover """main function, parses params and runs command functions :return: :rtype: """ # Collect Darktrace URL base_url = demisto.params().get("url") # API key api_token = demisto.params().get("apikey", "") headers = {"Authorization": f"Token {api_token}"} # Client class inherits from BaseClient, so SSL verification is # handled out of the box by it. Pass ``verify_certificate`` to # the Client constructor. verify_certificate = not demisto.params().get("insecure", False) # How much time before the first fetch to retrieve incidents first_fetch_time = arg_to_timestamp( arg=demisto.params().get("first_fetch", "1 day"), arg_name="First fetch time", required=True ) # Client class inherits from BaseClient, so system proxy is handled # out of the box by it, just pass ``proxy`` to the Client constructor proxy = demisto.params().get("proxy", False) # ``demisto.debug()``, ``demisto.info()``, prints information in the XSOAR server log. demisto.debug(f"Command being called is {demisto.command()}") try: client = Client(base_url=base_url, verify=verify_certificate, proxy=proxy, headers=headers) if demisto.command() == "test-module": # This is the call made when pressing the integration Test button. return_results(test_module(client, first_fetch_time)) elif demisto.command() == "fetch-incidents": # Set and define the fetch incidents command to run after activated via integration settings. # Convert the argument to an int using helper map or set to MIN_SEVERITY_TO_FETCH min_severity = SEVERITY_MAP.get(demisto.params().get("min_severity", None), None) if not min_severity or min_severity < MIN_SEVERITY_TO_FETCH: min_severity = MIN_SEVERITY_TO_FETCH # Get the list of alert types to ingest and make sure each item is all lower case or set to ALERT_TYPES alert_types = demisto.params().get("alert_type", None) if not alert_types: alert_types = ALERT_TYPES else: alert_types = [item.lower() for item in alert_types] # Convert the argument to an int using helper function or set to MAX_INCIDENTS_TO_FETCH max_alerts = arg_to_number( arg=demisto.params().get("max_fetch", MAX_INCIDENTS_TO_FETCH), arg_name="max_fetch", required=False ) if not max_alerts or max_alerts > MAX_INCIDENTS_TO_FETCH: max_alerts = MAX_INCIDENTS_TO_FETCH next_run, incidents = fetch_incidents( client=client, max_alerts=max_alerts, min_severity=min_severity, alert_types=alert_types, last_run=demisto.getLastRun(), # getLastRun() gets the last run dict first_fetch_time=first_fetch_time, ) # Use the variables defined above as the outputs of fetch_incidents to set up the next call and create incidents: # saves next_run for the time fetch-incidents is invoked demisto.setLastRun(next_run) # fetch-incidents calls ``demisto.incidents()`` to provide the list # of incidents to create demisto.incidents(incidents) elif demisto.command() == "darktrace-asm-get-risk": return_results(get_asm_risk_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-mitigate-risk": return_results(mitigate_asm_risk_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-post-comment": return_results(post_asm_comment_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-edit-comment": return_results(edit_asm_comment_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-delete-comment": return_results(delete_asm_comment_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-get-asset": return_results(get_asm_asset_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-create-tag": return_results(create_asm_tag_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-assign-tag": return_results(assign_asm_tag_command(client, demisto.args())) elif demisto.command() == "darktrace-asm-unassign-tag": return_results(unassign_asm_tag_command(client, demisto.args())) except Exception as e: demisto.error(traceback.format_exc()) # print the traceback return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}") """*****ENTRY POINT****""" if __name__ in ("__main__", "__builtin__", "builtins"): main()