diff --git a/Packs/AIAgents/ReleaseNotes/1_0_34.md b/Packs/AIAgents/ReleaseNotes/1_0_34.md index 6e4c0a58ebd3..c8f7b0775cc5 100644 --- a/Packs/AIAgents/ReleaseNotes/1_0_34.md +++ b/Packs/AIAgents/ReleaseNotes/1_0_34.md @@ -1,3 +1,3 @@ ## AI Agents -- Locked dependencies of the pack to ensure stability for versioned core packs. No changes in this release. +This version contains a known issue and should not be used. Please upgrade to version 1.0.38. diff --git a/Packs/AIAgents/ReleaseNotes/1_0_35.md b/Packs/AIAgents/ReleaseNotes/1_0_35.md index 7f912137e61d..2a84b45f7702 100644 --- a/Packs/AIAgents/ReleaseNotes/1_0_35.md +++ b/Packs/AIAgents/ReleaseNotes/1_0_35.md @@ -1,6 +1,6 @@ ## AI Agents -This version contains a known issue and should not be used. Please upgrade to version 1.0.37. +This version contains a known issue and should not be used. Please upgrade to version 1.0.38. diff --git a/Packs/AIAgents/ReleaseNotes/1_0_36.md b/Packs/AIAgents/ReleaseNotes/1_0_36.md index aa9d75dc68b0..c8f7b0775cc5 100644 --- a/Packs/AIAgents/ReleaseNotes/1_0_36.md +++ b/Packs/AIAgents/ReleaseNotes/1_0_36.md @@ -1,3 +1,3 @@ ## AI Agents -This version contains a known issue and should not be used. Please upgrade to version 1.0.37. +This version contains a known issue and should not be used. Please upgrade to version 1.0.38. diff --git a/Packs/AIAgents/ReleaseNotes/1_0_37.md b/Packs/AIAgents/ReleaseNotes/1_0_37.md index f4a7d5168bc1..c8f7b0775cc5 100644 --- a/Packs/AIAgents/ReleaseNotes/1_0_37.md +++ b/Packs/AIAgents/ReleaseNotes/1_0_37.md @@ -1,3 +1,3 @@ ## AI Agents -- Documentation and metadata improvements. +This version contains a known issue and should not be used. Please upgrade to version 1.0.38. diff --git a/Packs/AIAgents/ReleaseNotes/1_0_38.md b/Packs/AIAgents/ReleaseNotes/1_0_38.md new file mode 100644 index 000000000000..18925a624d02 --- /dev/null +++ b/Packs/AIAgents/ReleaseNotes/1_0_38.md @@ -0,0 +1,3 @@ +## AI Agents + +Documentation and metadata improvements. diff --git a/Packs/AIAgents/pack_metadata.json b/Packs/AIAgents/pack_metadata.json index 17d9550a680f..b87a7518ddcb 100644 --- a/Packs/AIAgents/pack_metadata.json +++ b/Packs/AIAgents/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AI Agents", "description": "AI Agents", "support": "xsoar", - "currentVersion": "1.0.37", + "currentVersion": "1.0.38", "serverMinVersion": "8.13", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", diff --git a/Packs/ANYRUN/Integrations/AnyRunLookup/AnyRunLookup.yml b/Packs/ANYRUN/Integrations/AnyRunLookup/AnyRunLookup.yml index 42c4473bb462..b99bf30b103c 100644 --- a/Packs/ANYRUN/Integrations/AnyRunLookup/AnyRunLookup.yml +++ b/Packs/ANYRUN/Integrations/AnyRunLookup/AnyRunLookup.yml @@ -942,7 +942,7 @@ script: - contextPath: DBotScore.Vendor description: The vendor used to calculate the score. type: String - dockerimage: demisto/anyrun-sdk:1.0.0.6667586 + dockerimage: demisto/anyrun-sdk:1.0.0.10120494 subtype: python3 runonce: false script: '-' diff --git a/Packs/ANYRUN/ReleaseNotes/2_3_5.md b/Packs/ANYRUN/ReleaseNotes/2_3_5.md new file mode 100644 index 000000000000..a84ebd0b4278 --- /dev/null +++ b/Packs/ANYRUN/ReleaseNotes/2_3_5.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### ANY.RUN TI Lookup + +- Updated the Docker image to: *demisto/anyrun-sdk:1.0.0.10120494*. + diff --git a/Packs/ANYRUN/pack_metadata.json b/Packs/ANYRUN/pack_metadata.json index f15983c0de27..fee1316e0a22 100644 --- a/Packs/ANYRUN/pack_metadata.json +++ b/Packs/ANYRUN/pack_metadata.json @@ -2,7 +2,7 @@ "name": "ANY.RUN", "description": "Empowers SOC teams with a Cloud Sandbox for real-time malware analysis, Threat Intelligence Lookup, and high-quality feeds to enhance detection and threat coverage.", "support": "partner", - "currentVersion": "2.3.4", + "currentVersion": "2.3.5", "author": "ANY.RUN", "url": "https://any.run/", "email": "techsupport@any.run", diff --git a/Packs/AWS-ACM/Integrations/AWS-ACM/AWS-ACM.yml b/Packs/AWS-ACM/Integrations/AWS-ACM/AWS-ACM.yml index 7968cbf9f7a5..1685972292c9 100644 --- a/Packs/AWS-ACM/Integrations/AWS-ACM/AWS-ACM.yml +++ b/Packs/AWS-ACM/Integrations/AWS-ACM/AWS-ACM.yml @@ -470,7 +470,7 @@ script: description: The certificate chain that contains the root certificate issued by the certificate authority (CA). type: string description: Retrieves a certificate specified by an ARN and its certificate chain . The chain is an ordered list of certificates that contains the end entity certificate, intermediate certificates of subordinate CAs, and the root certificate in that order. The certificate and certificate chain are base64 encoded. If you want to decode the certificate to see the individual fields, you can use OpenSSL. - dockerimage: demisto/boto3py3:1.0.0.3575453 + dockerimage: demisto/boto3py3:1.0.0.10221838 subtype: python3 tests: - ACM-Test diff --git a/Packs/AWS-ACM/ReleaseNotes/1_1_47.md b/Packs/AWS-ACM/ReleaseNotes/1_1_47.md new file mode 100644 index 000000000000..dceb2cc35847 --- /dev/null +++ b/Packs/AWS-ACM/ReleaseNotes/1_1_47.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS - ACM + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-ACM/pack_metadata.json b/Packs/AWS-ACM/pack_metadata.json index 1d139961e458..748eaf037e8c 100644 --- a/Packs/AWS-ACM/pack_metadata.json +++ b/Packs/AWS-ACM/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - ACM", "description": "Amazon Web Services Certificate Manager Service (acm)", "support": "xsoar", - "currentVersion": "1.1.46", + "currentVersion": "1.1.47", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-CloudWatchLogs/Integrations/AWS-CloudWatchLogs/AWS-CloudWatchLogs.yml b/Packs/AWS-CloudWatchLogs/Integrations/AWS-CloudWatchLogs/AWS-CloudWatchLogs.yml index bd76931db25d..5768aefbc7dd 100644 --- a/Packs/AWS-CloudWatchLogs/Integrations/AWS-CloudWatchLogs/AWS-CloudWatchLogs.yml +++ b/Packs/AWS-CloudWatchLogs/Integrations/AWS-CloudWatchLogs/AWS-CloudWatchLogs.yml @@ -462,7 +462,7 @@ script: description: The name of the log group. type: string description: Lists the specified metric filters. You can list all the metric filters or filter the results by log name, prefix, metric name, or metric namespace. - dockerimage: demisto/boto3py3:1.0.0.3575453 + dockerimage: demisto/boto3py3:1.0.0.10221838 tests: - No Tests fromversion: 5.0.0 diff --git a/Packs/AWS-CloudWatchLogs/ReleaseNotes/1_2_32.md b/Packs/AWS-CloudWatchLogs/ReleaseNotes/1_2_32.md new file mode 100644 index 000000000000..8da45f56d3d5 --- /dev/null +++ b/Packs/AWS-CloudWatchLogs/ReleaseNotes/1_2_32.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS - CloudWatchLogs + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-CloudWatchLogs/pack_metadata.json b/Packs/AWS-CloudWatchLogs/pack_metadata.json index f604f8776713..3078b14f869c 100644 --- a/Packs/AWS-CloudWatchLogs/pack_metadata.json +++ b/Packs/AWS-CloudWatchLogs/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - CloudWatchLogs", "description": "Amazon Web Services CloudWatch Logs (logs).", "support": "xsoar", - "currentVersion": "1.2.31", + "currentVersion": "1.2.32", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-EC2/Integrations/AWS-EC2/AWS-EC2.yml b/Packs/AWS-EC2/Integrations/AWS-EC2/AWS-EC2.yml index 22d033ea6cb6..52b6aca06f2b 100644 --- a/Packs/AWS-EC2/Integrations/AWS-EC2/AWS-EC2.yml +++ b/Packs/AWS-EC2/Integrations/AWS-EC2/AWS-EC2.yml @@ -4175,7 +4175,7 @@ script: type: String description: Creates a VPC endpoint. name: aws-ec2-create-vpc-endpoint - dockerimage: demisto/boto3py3:1.0.0.7837600 + dockerimage: demisto/boto3py3:1.0.0.10221838 runonce: false script: '-' subtype: python3 diff --git a/Packs/AWS-EC2/ReleaseNotes/1_4_31.md b/Packs/AWS-EC2/ReleaseNotes/1_4_31.md new file mode 100644 index 000000000000..c24ebabe04ef --- /dev/null +++ b/Packs/AWS-EC2/ReleaseNotes/1_4_31.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS - EC2 + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-EC2/pack_metadata.json b/Packs/AWS-EC2/pack_metadata.json index c79421a08b4a..4e683f62c0ac 100644 --- a/Packs/AWS-EC2/pack_metadata.json +++ b/Packs/AWS-EC2/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - EC2", "description": "Amazon Web Services Elastic Compute Cloud (EC2)", "support": "xsoar", - "currentVersion": "1.4.30", + "currentVersion": "1.4.31", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.py b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.py index e09404de8ea8..1e9fe30a0c59 100644 --- a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.py +++ b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.py @@ -48,6 +48,61 @@ def convert_events_with_datetime_to_str(events: list) -> list: return output_events +def _normalize_last_ids_entry(value) -> set[str]: + """Coerce a stored ``last_ids`` value into a set of ids. + + The integration historically stored ``last_ids[detector_id]`` as a single + string (the last finding id seen). To fix XSUP-67097 we now track every + finding id sharing the cursor's ``UpdatedAt`` second, which means the + value is conceptually a set. ``demisto.setLastRun`` serializes as JSON, + so the on-disk representation must be a ``list``. This helper normalizes + all three legacy / current shapes into a ``set[str]``: + + * ``str`` → ``{value}`` (legacy state from <1.3.67) + * ``list`` / ``tuple`` → ``set(value)`` (rehydrated from setLastRun) + * ``set`` → ``set(value)`` (in-memory) + * ``None`` / ``""`` / falsy → ``set()`` + + Anything else logs a warning and falls back to an empty set so a single + bad cache entry never blocks a fetch cycle. + """ + if not value: + return set() + if isinstance(value, str): + return {value} + if isinstance(value, list | tuple | set): + return {item for item in value if isinstance(item, str)} + demisto.debug(f"AWSGuardDutyEventCollector - Unexpected last_ids value type {type(value).__name__}; treating as empty.") + return set() + + +def _build_finding_criterion(updated_at: Optional[datetime], severity: str, exclude_archived: bool) -> dict: + """Build the ``FindingCriteria.Criterion`` dict for ``list_findings``. + + Args: + updated_at: Inclusive lower bound on ``updatedAt``. + severity: Minimum severity label (Low/Medium/High). + exclude_archived: When ``True``, adds ``service.archived = false`` so suppressed/archived + findings (XSUP-67097 / XSUP-71079 complaint #2) are not re-fetched. + + Returns: + The criterion dict. + """ + criterion: dict = { + "updatedAt": {"Gte": date_to_timestamp(updated_at)}, + "severity": {"Gte": GD_SEVERITY_DICT.get(severity, 1)}, + } + if exclude_archived: + # GuardDuty represents the archived flag as the string "false"/"true" in FindingCriteria. + criterion["service.archived"] = {"Eq": ["false"]} + return criterion + + +def _event_updated_at(event: dict) -> Any: + """Return the timestamp used as the fetch cursor for a single finding.""" + return event.get("UpdatedAt", event.get("CreatedAt")) + + def get_events( aws_client: "GuardDutyClient", collect_from: dict, @@ -57,6 +112,7 @@ def get_events( limit: int = MAX_RESULTS, detectors_num: int = MAX_RESULTS, max_ids_per_req: int = MAX_IDS_PER_REQ, + exclude_archived: bool = False, ) -> tuple[list, dict, dict]: """Get events from AWSGuardDuty. @@ -64,17 +120,28 @@ def get_events( aws_client: AWSClient session to get events from. collect_from: Dict of {detector_id: datestring to start collecting from}, used when fetching. collect_from_default: datetime to start collecting from if detector id is not found in collect_from keys. - last_ids: Dict of {detector_id: last fetched id}, used to avoid duplicates. + last_ids: Dict of {detector_id: }, used to avoid duplicates and to + prevent same-second sibling loss. Each value may be a ``set``, ``list``, ``tuple``, or — for + backwards compatibility with state written by integration versions <1.3.67 — a single ``str``. + All shapes are normalized to ``set[str]`` internally. severity: The minimum severity to start fetching from. (inclusive) limit: The maximum number of events to fetch. detectors_num: The maximum number of detectors to fetch. max_ids_per_req: The maximum number of findings to get per API request. + exclude_archived: When ``True``, archived/suppressed findings are excluded from the fetch. Returns: (events, new_last_ids, new_collect_from) events (list): The events fetched. new_last_ids (dict): The new last_ids dict, expected to receive as last_ids input in the next run. + Each value is a ``list[str]`` (JSON-serializable for setLastRun). new_collect_from (dict): The new collect_from dict, expected to receive as collect_from input in the next run. + + Note (XSUP-71079): The fetch cursor is second-resolution and the ``updatedAt`` filter is inclusive + (``Gte``). To avoid silently skipping findings, the cursor is NEVER advanced into a second that was + only partially consumed because ``limit`` was reached. When a fetch is truncated mid-second the cursor + is rolled back to the last fully-drained second (and its sibling ids are persisted) so the next run + re-queries the truncated second from its start. This guarantees forward progress without data loss. """ events: list = [] @@ -106,17 +173,14 @@ def get_events( finding_ids: list = [] detector_events: list = [] updated_at = parse_date_string(collect_from.get(detector_id)) if collect_from.get(detector_id) else collect_from_default + # XSUP-67097: dedup against ALL ids seen at the cursor second, not just one. + seen_ids = _normalize_last_ids_entry(last_ids.get(detector_id)) # List all finding ids while next_token and len(events) + len(finding_ids) < limit: demisto.debug(f"AWSGuardDutyEventCollector - Getting more finding ids with {next_token=}, {updated_at=}") list_finding_args = { "DetectorId": detector_id, - "FindingCriteria": { - "Criterion": { - "updatedAt": {"Gte": date_to_timestamp(updated_at)}, - "severity": {"Gte": GD_SEVERITY_DICT.get(severity, 1)}, - } - }, + "FindingCriteria": {"Criterion": _build_finding_criterion(updated_at, severity, exclude_archived)}, "SortCriteria": {"AttributeName": "updatedAt", "OrderBy": "ASC"}, "MaxResults": min(limit - (len(events) + len(set(finding_ids))), MAX_RESULTS), } @@ -126,18 +190,6 @@ def get_events( finding_ids += list_findings.get("FindingIds", []) next_token = list_findings.get("NextToken", "") - # Handle duplicates and findings updated at the same time. - if last_ids.get(detector_id) and last_ids.get(detector_id) in finding_ids: - demisto.debug( - f"AWSGuardDutyEventCollector - Cutting {finding_ids=} " - f"for {detector_id=} and last_id={last_ids.get(detector_id)}." - ) - finding_ids = finding_ids[finding_ids.index(last_ids.get(detector_id)) + 1 :] - demisto.debug( - f"AWSGuardDutyEventCollector - New {finding_ids=} after cut " - f"for {detector_id=} and last_id={last_ids.get(detector_id)}." - ) - # Handle duplicates in response while preserving order finding_ids_unique = list(dict.fromkeys(finding_ids)) demisto.debug(f"Detector id {detector_id} unique finding ids found: {finding_ids_unique}") @@ -150,15 +202,85 @@ def get_events( findings_response = aws_client.get_findings(DetectorId=detector_id, FindingIds=chunk_of_finding_ids) detector_events += findings_response.get("Findings", []) + # Dedup already-seen findings — but only at the cursor second. + # + # XSUP-67097: drop every finding we already ingested that still shares the cursor's UpdatedAt + # second (same-second siblings re-returned by the inclusive Gte query). + # + # XSUP-72455: do NOT drop a finding whose UpdatedAt has advanced past the cursor second. GuardDuty + # findings are long-lived and update in place; when a recurring finding gets a new occurrence its + # UpdatedAt moves forward and AWS returns it again. That is a legitimate new event and must be + # ingested. The previous ID-only dedup dropped it because its id was in last_ids, which produced an + # empty result and, because the cursor only advances when events are ingested, pinned the fetch + # behind that finding indefinitely. + if seen_ids: + before_ids = [ev.get("Id") for ev in detector_events] + detector_events = [ + ev + for ev in detector_events + if ev.get("Id") not in seen_ids or parse_date_string(_event_updated_at(ev)) != updated_at + ] + after_ids = [ev.get("Id") for ev in detector_events] + if before_ids != after_ids: + demisto.debug( + f"AWSGuardDutyEventCollector - Dedup removed already-seen same-second findings " + f"for {detector_id=}. Before: {before_ids}, after: {after_ids}, removed via {seen_ids=} " + f"at cursor second {updated_at=}." + ) + demisto.debug(f"AWSGuardDutyEventCollector - {detector_id=} findings found ({len(detector_events)}): {detector_events}") events += detector_events demisto.debug(f"AWSGuardDutyEventCollector - Number of events is {len(events)}") - if finding_ids: - new_last_ids[detector_id] = finding_ids[-1] - + # XSUP-71079: advance the cursor safely. + # + # The cursor is second-resolution and the updatedAt query is inclusive (Gte). Two failure modes + # are guarded here: + # 1. Same-second siblings (XSUP-67097): persist EVERY finding id whose UpdatedAt equals the + # cursor second so the next run can dedup them all (not just one). + # 2. Mid-second truncation (XSUP-71079): if this fetch stopped because it hit `limit` while + # there were still un-fetched findings (next_token is truthy) AND the last second is only + # partially consumed, advancing the cursor to that last second would skip the remaining + # siblings of that second (they fall on the same inclusive boundary but AWS may order them + # after the truncation point). To guarantee no loss we roll the cursor back to the last + # FULLY-drained second and persist its sibling ids, so the next run re-queries the + # truncated second from its start and makes forward progress. + truncated_by_limit = bool(next_token) # loop exited with a pending token => stopped due to limit if detector_events: - new_collect_from[detector_id] = detector_events[-1].get("UpdatedAt", detector_events[-1].get("CreatedAt")) + last_cursor_ts = _event_updated_at(detector_events[-1]) + cursor_ts = last_cursor_ts + if truncated_by_limit: + # Find the latest second strictly older than the last (partial) second. + distinct_seconds = {_event_updated_at(ev) for ev in detector_events} + fully_drained = sorted((s for s in distinct_seconds if s != last_cursor_ts), key=parse_date_string) + if fully_drained: + cursor_ts = fully_drained[-1] + demisto.debug( + f"AWSGuardDutyEventCollector - Fetch truncated by limit for {detector_id=}. " + f"Rolling cursor back from partial second {last_cursor_ts} to last fully-drained " + f"second {cursor_ts} to avoid skipping same-second siblings." + ) + else: + # The entire page is a single second that we could not fully drain. Keep the cursor + # on that second and accumulate seen ids so progress happens via dedup next run. + demisto.debug( + f"AWSGuardDutyEventCollector - Fetch truncated by limit for {detector_id=} within a " + f"single second {last_cursor_ts}; keeping cursor and accumulating seen ids." + ) + new_collect_from[detector_id] = cursor_ts + cursor_sibling_ids = {ev.get("Id") for ev in detector_events if _event_updated_at(ev) == cursor_ts} + cursor_sibling_ids.discard(None) + # Carry forward previously-seen ids when the cursor second did not advance past them, + # so we never forget same-second siblings across runs. + if seen_ids and parse_date_string(cursor_ts) == updated_at: + cursor_sibling_ids |= seen_ids + # Stored as list so demisto.setLastRun can JSON-serialize it; round-trips via + # _normalize_last_ids_entry on the next call. + new_last_ids[detector_id] = sorted(cursor_sibling_ids) + elif finding_ids: + # No detector_events but we did see ids — keep the prior seen_ids as-is so + # we don't forget about them on the next fetch. + new_last_ids[detector_id] = sorted(seen_ids) if seen_ids else [] demisto.debug(f"AWSGuardDutyEventCollector - Total number of events is {len(events)}") events = convert_events_with_datetime_to_str(events) @@ -182,6 +304,7 @@ def main(): # pragma: no cover limit = arg_to_number(params.get("limit")) sts_endpoint_url = params.get("sts_endpoint_url") or None endpoint_url = params.get("endpoint_url") or None + exclude_archived = argToBoolean(params.get("exclude_archived", False)) try: validate_params(aws_default_region, aws_role_arn, aws_role_session_name, aws_access_key_id, aws_secret_access_key) @@ -228,6 +351,7 @@ def main(): # pragma: no cover last_ids={}, severity=severity, limit=command_limit if command_limit else MAX_RESULTS, + exclude_archived=exclude_archived, ) command_results = CommandResults( @@ -251,6 +375,7 @@ def main(): # pragma: no cover last_ids=last_ids, severity=aws_gd_severity, limit=limit if limit else MAX_RESULTS, + exclude_archived=exclude_archived, ) send_events_to_xsiam(events, VENDOR, PRODUCT) diff --git a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.yml b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.yml index ad5c57c8d762..7e46f0a062b5 100644 --- a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.yml +++ b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector.yml @@ -1,5 +1,10 @@ +name: AWS - GuardDuty Event Collector +display: AWS - GuardDuty Event Collector category: Analytics & SIEM provider: Amazon +sectionorder: +- Connect +- Collect commonfields: id: AWS - GuardDuty Event Collector version: -1 @@ -9,39 +14,48 @@ configuration: name: defaultRegion required: true type: 0 + section: Connect - display: Role ARN additionalinfo: The Amazon Resource Name (ARN) role used for EC2 instance authentication. If this is used, an access key and secret key are not required. name: roleArn type: 0 required: false + section: Connect - display: Role Session Name additionalinfo: A descriptive name for the assumed role session. For example, xsiam-IAM.integration-Role_SESSION name: roleSessionName type: 0 required: false + section: Connect - display: Role Session Duration additionalinfo: 'The maximum length of each session in seconds. The integration will have the permissions assigned only when the session is initiated and for the defined duration.' name: sessionDuration type: 0 required: false + section: Connect - display: Access Key additionalinfo: The access key ID and secret key used for authentication that was configured during IAM user configuration. If this is used, Role ARN is not required. name: credentials displaypassword: Secret Key type: 9 required: false + section: Connect - display: Timeout name: timeout additionalinfo: The time in seconds until a timeout exception is reached. You can specify just the read timeout (for example 60) or also the connect timeout preceded by a comma (for example 60,10). If a connect timeout is not specified, a default of 10 seconds will be used. defaultvalue: 60,10 type: 0 required: false + section: Connect + advanced: true - display: Retries name: retries defaultvalue: 5 additionalinfo: "The maximum number of retry attempts when connection or throttling errors are encountered. Set to 0 to disable retries. The default value is 5 and the limit is 10. Note: Increasing the number of retries will increase the execution time." type: 0 required: false + section: Connect + advanced: true - display: PrivateLink service URL. name: endpoint_url type: 0 @@ -68,11 +82,13 @@ configuration: defaultvalue: '3 days ago' type: 0 required: false + section: Collect - display: Number of events to fetch per fetch. name: limit type: 0 defaultvalue: 1000 required: false + section: Collect - display: Guard Duty Severity level name: gd_severity required: true @@ -83,17 +99,27 @@ configuration: - 'Medium' - 'High' defaultvalue: 'Low' + section: Collect - display: Trust any certificate (not secure) name: insecure type: 8 required: false + section: Connect + advanced: true - display: Use system proxy settings name: proxy type: 8 required: false + section: Connect + advanced: true +- display: Exclude archived/suppressed findings + name: exclude_archived + type: 8 + required: false + defaultvalue: 'false' + additionalinfo: When selected, findings that have been archived in GuardDuty (including those archived by suppression rules) are not fetched. + section: Collect description: Amazon Web Services Guard Duty Service (gd) event collector integration for Cortex XSIAM. -display: AWS - GuardDuty Event Collector -name: AWS - GuardDuty Event Collector script: script: "" type: python diff --git a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector_test.py b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector_test.py index adb75e18acea..c24e9d27828e 100644 --- a/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector_test.py +++ b/Packs/AWS-GuardDuty/Integrations/AWSGuardDutyEventCollector/AWSGuardDutyEventCollector_test.py @@ -2,8 +2,9 @@ from datetime import datetime from unittest.mock import call +import demistomock as demisto import pytest -from AWSGuardDutyEventCollector import get_events +from AWSGuardDutyEventCollector import _event_updated_at, _normalize_last_ids_entry, get_events from test_data.finding_for_test import FINDING, FINDING_OUTPUT, MOST_GENERAL_FINDING, MOST_GENERAL_FINDING_STR LIST_DETECTORS_RESPONSE = {"DetectorIds": ["detector_id1"]} @@ -459,7 +460,9 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin [call(DetectorId="detector_id1", FindingIds=["finding_id1"])], [update_finding_id(FINDING_OUTPUT.copy(), "finding_id1", updated_at="2022-09-28T10:12:39.923854")], {"detector_id1": "2022-09-28T10:12:39.923854"}, - {"detector_id1": "finding_id1"}, + # XSUP-67097: last_ids is now stored as list[str] of all ids sharing + # the cursor's UpdatedAt second, not just one id. + {"detector_id1": ["finding_id1"]}, id="1 detector, 1 new finding", ), pytest.param( @@ -467,7 +470,17 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin {"detector_id1": "finding_id0"}, [{"DetectorIds": ["detector_id1"]}], [{"FindingIds": ["finding_id0", "finding_id1"]}], - [{"Findings": [update_finding_id(FINDING.copy(), "finding_id1", updated_at="2022-09-28T10:12:39.923854")]}], + # XSUP-72455: both ids are fetched so the collector can inspect each finding's + # UpdatedAt. finding_id0 still shares the cursor second (already ingested) and is + # deduped after the fetch; finding_id1 is new and is ingested. + [ + { + "Findings": [ + update_finding_id(FINDING.copy(), "finding_id0", updated_at="2022-08-28T10:12:39.923854"), + update_finding_id(FINDING.copy(), "finding_id1", updated_at="2022-09-28T10:12:39.923854"), + ] + } + ], [call(MaxResults=50)], [ call( @@ -476,10 +489,10 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin ) ) ], - [call(DetectorId="detector_id1", FindingIds=["finding_id1"])], + [call(DetectorId="detector_id1", FindingIds=["finding_id0", "finding_id1"])], [update_finding_id(FINDING_OUTPUT.copy(), "finding_id1", updated_at="2022-09-28T10:12:39.923854")], {"detector_id1": "2022-09-28T10:12:39.923854"}, - {"detector_id1": "finding_id1"}, + {"detector_id1": ["finding_id1"]}, id="1 detector, 1 new finding, 1 old finding", ), pytest.param( @@ -487,7 +500,9 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin {"detector_id1": "finding_id0"}, [{"DetectorIds": ["detector_id1"]}], [{"FindingIds": ["finding_id0"]}], - [], + # XSUP-72455: the id is fetched so its UpdatedAt can be inspected. It still shares the + # cursor second (already ingested), so it is deduped after the fetch — no events, cursor unchanged. + [{"Findings": [update_finding_id(FINDING.copy(), "finding_id0", updated_at="2022-08-28T10:12:39.923854")]}], [call(MaxResults=50)], [ call( @@ -496,10 +511,11 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin ) ) ], - [], + [call(DetectorId="detector_id1", FindingIds=["finding_id0"])], [], {"detector_id1": "2022-08-28T10:12:39.923854"}, - {"detector_id1": "finding_id0"}, + # last_ids preserves the already-seen id at the unchanged cursor second. + {"detector_id1": ["finding_id0"]}, id="1 detector, 1 old finding", ), pytest.param( @@ -553,7 +569,7 @@ def test_get_events_returns_datetime_as_str(mocker, list_detectors_res, list_fin update_finding_id(FINDING_OUTPUT.copy(), "finding_id2", updated_at="2022-07-29T10:12:39.923854"), ], {"detector_id1": "2022-09-28T10:12:39.923854", "detector_id2": "2022-07-29T10:12:39.923854"}, - {"detector_id1": "finding_id1", "detector_id2": "finding_id2"}, + {"detector_id1": ["finding_id1"], "detector_id2": ["finding_id2"]}, id="1 old detector, 1 new detector, 1 new finding each", ), ], @@ -608,3 +624,732 @@ def test_fetch_events( assert events == expected_events assert new_collect_from == expected_new_collect_from assert new_last_ids == expected_new_last_ids + + +# --------------------------------------------------------------------------- +# Regression test for XSUP-67097 / XSUP-67552 — same-second sibling-loss bug. +# +# Scenario: three findings (A, B, C) all share the same UpdatedAt timestamp. +# Run 1: AWS pagination returns [A, B] (limit=2 cuts off page 2). +# Cursor stored: last_ids[detector] = "B", collect_from = T. +# Run 2: Filter is updatedAt: {Gte: T} (inclusive), so AWS returns the same +# set again. AWS does NOT guarantee a stable order across calls when +# findings share an UpdatedAt second, so it returns [A, C, B]. +# The dedup at AWSGuardDutyEventCollector.py:130-135 finds B, slices +# AFTER B's index → result is []. Finding C is permanently dropped. +# +# Expected (after fix): A, B fetched in run 1; C fetched in run 2. +# Actual (with bug): A, B fetched in run 1; C is LOST. +# --------------------------------------------------------------------------- + + +def test_same_second_sibling_loss_xsup_67097(mocker): + """ + Given: + Three findings (A, B, C) on a single detector, all sharing the same + UpdatedAt second. Pagination splits them across two fetch cycles. + + When: + Run 1 fetches with limit=2 and stores last_ids[det] = "B". + Run 2's mocked AWS returns the same findings in a different valid + intra-second order ([A, C, B]) — permitted by AWS GuardDuty since + sort is stable only on updatedAt, not on id. + + Then: + After both runs, all three findings should be ingested exactly once. + With the current single-id dedup in get_events(), finding C is lost + on run 2 (the dedup slices AFTER B's index, dropping C). + + Reference: + AWSGuardDutyEventCollector.py:130-135 (single-id dedup slice) + AWSGuardDutyEventCollector.py:158 (single-id storage) + """ + same_second_ts = "2026-04-10T01:35:09.000000" + finding_a = update_finding_id(FINDING.copy(), "finding_A", updated_at=same_second_ts) + finding_b = update_finding_id(FINDING.copy(), "finding_B", updated_at=same_second_ts) + finding_c = update_finding_id(FINDING.copy(), "finding_C", updated_at=same_second_ts) + + # ------------------------------------------------------------------ Run 1 + # AWS returns [A, B] only (limit=2 stops the loop before page 2 of [C]). + run1_client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_A", "finding_B"]}], + get_findings_res=[{"Findings": [finding_a, finding_b]}], + ) + + events_run1, last_ids_after_run1, collect_from_after_run1 = get_events( + aws_client=run1_client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids={}, + severity="Low", + limit=2, + ) + + run1_ids = sorted(e["Id"] for e in events_run1) + assert run1_ids == ["finding_A", "finding_B"], f"Sanity check failed: run 1 should ingest A and B, got {run1_ids}" + # Cursor state after run 1: with the XSUP-67097 fix, last_ids stores ALL ids + # whose UpdatedAt equals the cursor (the same-second siblings), not just one. + # Both A and B share the cursor second so both must be remembered. + assert last_ids_after_run1 == {"det1": ["finding_A", "finding_B"]} + assert collect_from_after_run1 == {"det1": same_second_ts} + + # ------------------------------------------------------------------ Run 2 + # AWS re-queries with Gte: T (inclusive). It returns the same three + # findings, but in a DIFFERENT intra-second order: [A, C, B]. + # This is valid AWS behavior — sort key is updatedAt only, ties are + # not guaranteed stable across calls. The dedup in get_events() will + # find B at index 2 and slice AFTER it, producing []. C is dropped. + mocker.resetall() + run2_client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_A", "finding_C", "finding_B"]}], + # get_findings is only invoked if there are surviving ids after dedup. + # The bug means there will be none, so this side-effect is never hit. + # If a fix is applied, it WILL be hit with FindingIds=["finding_C"]. + get_findings_res=[{"Findings": [finding_c]}], + ) + + events_run2, _, _ = get_events( + aws_client=run2_client, + collect_from=collect_from_after_run1, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids=last_ids_after_run1, + severity="Low", + limit=10, + ) + + # ------------------------------------------------------------ Assertion + # Combined across both runs, every finding (A, B, C) must appear exactly + # once. Today's code drops C — this assertion fails as proof of the bug. + all_ingested_ids = sorted(e["Id"] for e in (events_run1 + events_run2)) + assert all_ingested_ids == ["finding_A", "finding_B", "finding_C"], ( + f"XSUP-67097 same-second sibling loss: expected all three findings " + f"to be ingested across the two fetch cycles, but got {all_ingested_ids}. " + f"Finding 'finding_C' was silently dropped because the dedup at " + f"AWSGuardDutyEventCollector.py:135 slices after the single stored " + f"last_id, losing any same-second siblings that AWS returned in a " + f"position before that last_id on the next page." + ) + + +def test_legacy_last_ids_str_shape_still_works(mocker): + """ + Given: + A last_ids dict using the legacy str shape (single id per detector), + as written by integration versions prior to 1.3.67. + + When: + get_events runs with that legacy state and AWS returns the stored id + again (because Gte is inclusive on updatedAt). + + Then: + The legacy id is treated as already-seen and dropped from the new + ingestion. State is migrated forward — the new run writes the + list-shaped value going forward. + + Reference: + AWSGuardDutyEventCollector._normalize_last_ids_entry — accepts + str | list | tuple | set, preserving compatibility with stored + state from older versions. + """ + same_second_ts = "2026-04-10T01:35:09.000000" + # Only finding_new is constructed because the dedup drops finding_old before + # it ever reaches get_findings — there's no need to materialize the old one. + finding_new = update_finding_id(FINDING.copy(), "finding_new", updated_at=same_second_ts) + + # Legacy single-string last_ids shape, as written by versions <1.3.67. + legacy_last_ids: dict = {"det1": "finding_old"} + legacy_collect_from = {"det1": same_second_ts} + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + # AWS returns both findings; old one must be deduped via legacy str. + list_finding_ids_res=[{"FindingIds": ["finding_old", "finding_new"]}], + get_findings_res=[{"Findings": [finding_new]}], + ) + + events, new_last_ids, _ = get_events( + aws_client=client, + collect_from=legacy_collect_from, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids=legacy_last_ids, + severity="Low", + limit=10, + ) + + # Only the new finding ingests; the legacy id is recognized and dropped. + assert [e["Id"] for e in events] == ["finding_new"] + # State is migrated forward to the list shape. Both old and new share the + # cursor second, so both are remembered for the NEXT run's dedup. + assert new_last_ids == {"det1": ["finding_new", "finding_old"]} + + +# --------------------------------------------------------------------------- +# Regression test for XSUP-71079 — mid-second cursor truncation. +# +# Scenario: a fetch is truncated by `limit` in the MIDDLE of a second that has +# more siblings than were fetched. The cursor must NOT advance into that +# partially-consumed second, otherwise the un-fetched siblings (which fall on +# the same inclusive `Gte` boundary) are skipped on the next run. +# --------------------------------------------------------------------------- + + +def test_mid_second_truncation_does_not_advance_cursor_xsup_71079(mocker): + """ + Given: + Two seconds of findings on a single detector: + - second T1 = "...:08.000000" with finding_1 (fully drained) + - second T2 = "...:09.000000" with finding_2 (only one of several + siblings fetched before `limit` truncated the page; next_token is + still set, signalling more findings remain). + + When: + get_events runs with limit=2 and AWS returns a pending NextToken, + indicating the fetch was truncated before T2 was fully drained. + + Then: + The cursor (collect_from) is rolled back to T1 (the last fully-drained + second), and the persisted last_ids contains T1's id so the next run + re-queries from T1 (inclusive) and re-reads T2 in full without skipping + any of its siblings. + + Reference: + AWSGuardDutyEventCollector.get_events — truncated_by_limit rollback. + """ + t1 = "2026-04-10T01:35:08.000000" + t2 = "2026-04-10T01:35:09.000000" + finding_1 = update_finding_id(FINDING.copy(), "finding_1", updated_at=t1) + finding_2 = update_finding_id(FINDING.copy(), "finding_2", updated_at=t2) + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + # NextToken is set => the loop exits due to `limit`, not exhaustion. + list_finding_ids_res=[{"FindingIds": ["finding_1", "finding_2"], "NextToken": "more"}], + get_findings_res=[{"Findings": [finding_1, finding_2]}], + ) + + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids={}, + severity="Low", + limit=2, + ) + + # Both fetched findings are still returned to XSIAM (no data dropped this run). + assert sorted(e["Id"] for e in events) == ["finding_1", "finding_2"] + # The cursor is rolled back to the last FULLY-drained second (T1), NOT T2, + # so T2's un-fetched siblings are re-queried next run. + assert new_collect_from == {"det1": t1} + # last_ids reflects T1's siblings so finding_1 is not re-ingested next run. + assert new_last_ids == {"det1": ["finding_1"]} + + +def test_exclude_archived_adds_criterion_xsup_71079(mocker): + """ + Given: + exclude_archived=True is passed to get_events. + + When: + get_events builds the list_findings FindingCriteria. + + Then: + The criterion includes service.archived == "false" so archived / + suppressed findings are not fetched. When exclude_archived is False + (default) the criterion does NOT include the archived filter. + + Reference: + AWSGuardDutyEventCollector._build_finding_criterion — XSUP-71079 #2. + """ + finding = update_finding_id(FINDING.copy(), "finding_1", updated_at="2026-04-10T01:35:09.000000") + + # exclude_archived=True + client, _, list_findings_mock, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_1"]}], + get_findings_res=[{"Findings": [finding]}], + ) + get_events( + aws_client=client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids={}, + severity="Low", + limit=10, + exclude_archived=True, + ) + criterion = list_findings_mock.call_args.kwargs["FindingCriteria"]["Criterion"] + assert criterion.get("service.archived") == {"Eq": ["false"]} + + # exclude_archived=False (default) — no archived filter. + mocker.resetall() + client2, _, list_findings_mock2, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_1"]}], + get_findings_res=[{"Findings": [finding]}], + ) + get_events( + aws_client=client2, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids={}, + severity="Low", + limit=10, + exclude_archived=False, + ) + criterion2 = list_findings_mock2.call_args.kwargs["FindingCriteria"]["Criterion"] + assert "service.archived" not in criterion2 + + +# --------------------------------------------------------------------------- +# Regression test for XSUP-72455 — a recurring finding that is RE-UPDATED after +# being stored in last_ids must not be suppressed forever. +# +# Scenario: finding_X is ingested at cursor second T1 and stored in last_ids. +# Later GuardDuty aggregates a new occurrence into the same finding, moving its +# UpdatedAt to a strictly-later second T2. AWS returns it again (Gte is +# inclusive). The old ID-only dedup dropped it because its id was in last_ids, +# producing an empty result — which also prevented the cursor from advancing, +# pinning the fetch behind that finding indefinitely. +# --------------------------------------------------------------------------- + + +def test_reupdated_finding_is_not_suppressed_xsup_72455(mocker): + """ + Given: + finding_X was ingested on a previous run at cursor second T1 and is + stored in last_ids[det1]. On the next run GuardDuty returns finding_X + again, but its UpdatedAt has advanced to a strictly-later second T2 + (a real new occurrence aggregated into the long-lived finding). + + When: + get_events runs with collect_from=T1 and last_ids={det1: [finding_X]}. + + Then: + finding_X must be ingested (the update is a legitimate new event) and + the cursor must advance to T2. The old ID-only dedup dropped it and + left the cursor pinned at T1 with zero events — this test fails on the + buggy code and passes once dedup is scoped to the cursor second. + + Reference: + AWSGuardDutyEventCollector.get_events — dedup must only drop findings + whose UpdatedAt equals the stored cursor second, never later updates. + """ + t1 = "2026-07-03T15:48:55.563000" + t2 = "2026-07-04T08:04:55.843000" + # Same finding id as what is already stored in last_ids, but re-updated to T2. + reupdated = update_finding_id(FINDING.copy(), "finding_X", updated_at=t2) + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + # AWS returns the already-seen id again because Gte(T1) is inclusive and + # the finding's UpdatedAt (T2) is >= T1. + list_finding_ids_res=[{"FindingIds": ["finding_X"]}], + get_findings_res=[{"Findings": [reupdated]}], + ) + + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={"det1": t1}, + collect_from_default=datetime(2026, 7, 3, 7, 0, 0), + last_ids={"det1": ["finding_X"]}, + severity="Low", + limit=10, + ) + + # The re-updated finding is a legitimate new event and must be ingested. + assert [e["Id"] for e in events] == ["finding_X"], ( + "XSUP-72455: a finding whose UpdatedAt advanced past the stored cursor " + "second was dropped by ID-only dedup, so no events were ingested." + ) + # The cursor must advance to the finding's new second (T2), not stay pinned at T1. + assert new_collect_from == {"det1": t2}, ( + "XSUP-72455: cursor stayed pinned at the old second because the " + "re-updated finding was suppressed, blocking all forward progress." + ) + # last_ids now reflects the new cursor second (T2), holding finding_X so a + # same-second re-query does not re-ingest it. + assert new_last_ids == {"det1": ["finding_X"]} + + +def test_same_id_same_second_still_deduped_xsup_72455(mocker): + """ + Given: + finding_X is stored in last_ids at cursor second T1 and GuardDuty + returns it again with the SAME UpdatedAt (T1) — the inclusive-Gte + re-read of an already-ingested finding, not a new update. + + When: + get_events runs with collect_from=T1 and last_ids={det1: [finding_X]}. + + Then: + finding_X must be deduped (not re-ingested) and the cursor stays at T1. + This guards that the XSUP-72455 fix does not regress the original + same-second dedup (XSUP-67097) behavior. + + Reference: + AWSGuardDutyEventCollector.get_events — same-second re-reads are still deduped. + """ + t1 = "2026-07-03T15:48:55.563000" + same_second_again = update_finding_id(FINDING.copy(), "finding_X", updated_at=t1) + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_X"]}], + get_findings_res=[{"Findings": [same_second_again]}], + ) + + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={"det1": t1}, + collect_from_default=datetime(2026, 7, 3, 7, 0, 0), + last_ids={"det1": ["finding_X"]}, + severity="Low", + limit=10, + ) + + # Already ingested at T1 with the same UpdatedAt — must not be re-ingested. + assert events == [] + # Cursor unchanged; finding_X remembered for the next same-second re-query. + assert new_collect_from == {"det1": t1} + assert new_last_ids == {"det1": ["finding_X"]} + + +# --------------------------------------------------------------------------- +# _normalize_last_ids_entry — bad-path coercion. +# +# The helper must never propagate a malformed cache entry into the fetch loop. +# Unexpected value types (dict, int, etc.) fall back to an empty set() and a +# log line is emitted so operators can see a bad cache entry was ignored. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("bad_value", [{"unexpected": "dict"}, 42]) +def test_normalize_last_ids_entry_bad_path_returns_empty_set_and_logs(mocker, bad_value): + """ + Given: + A stored last_ids entry of an unexpected type (a dict and an int). + + When: + _normalize_last_ids_entry is asked to coerce it into a set of ids. + + Then: + The result is an empty set() (a single bad cache entry never blocks a + fetch), and a log line is emitted noting the unexpected type so the + silent fallback is observable. + + Reference: + AWSGuardDutyEventCollector._normalize_last_ids_entry — bad-path branch. + """ + # The helper logs via demisto's logging channel on the bad path. Patch it so + # we can assert the fallback was recorded rather than silently swallowed. + log_mock = mocker.patch.object(demisto, "debug") + + result = _normalize_last_ids_entry(bad_value) + + assert result == set() + assert isinstance(result, set) + # A log line must be emitted so the ignored bad cache entry is observable. + assert log_mock.called, "Expected a log line to be emitted for the unexpected last_ids value type." + logged_message = log_mock.call_args.args[0] + assert type(bad_value).__name__ in logged_message + + +# --------------------------------------------------------------------------- +# Multi-detector truncation — detectors advance their cursors independently. +# +# detector_A is truncated by `limit` mid-boundary (rolls its cursor back to the +# last fully-drained second) while detector_B drains fully (advances normally). +# Each detector's new_collect_from and new_last_ids must be computed in +# isolation — one detector's truncation must not affect the other's cursor. +# --------------------------------------------------------------------------- + + +def test_multi_detector_truncation_advances_cursors_independently(mocker): + """ + Given: + Two detectors whose fetches end differently: + - detector_A returns findings across two seconds (A_t1 fully drained, + A_t2 partial) and the finding-ids loop exits with a pending + NextToken because it hit `limit` mid-boundary at A_t2. + - detector_B returns a single finding on one second and NO pending + NextToken => fully drained, advancing its cursor normally. + + When: + get_events processes both detectors in the same run (the finding-ids + loop shares one `limit` budget, so det_A must reach the limit on its + own page for its truncation to be attributable to the limit while + det_B still has budget to drain). + + Then: + detector_A's cursor rolls back to its last fully-drained second (A_t1) + and its last_ids holds only A_t1's sibling, while detector_B advances + normally to its finding's second with its own last_ids. Each detector's + new_collect_from and new_last_ids are computed independently — det_A's + rollback does not touch det_B's forward advance. + + Reference: + AWSGuardDutyEventCollector.get_events — per-detector cursor bookkeeping + (XSUP-71079 truncation rollback applied per detector). + """ + a_t1 = "2026-04-10T01:35:08.000000" + a_t2 = "2026-04-10T01:35:09.000000" + b_t1 = "2026-04-10T02:00:00.000000" + + finding_a1 = update_finding_id(FINDING.copy(), "finding_A1", updated_at=a_t1) + finding_a2 = update_finding_id(FINDING.copy(), "finding_A2", updated_at=a_t2) + finding_b1 = update_finding_id(FINDING.copy(), "finding_B1", updated_at=b_t1) + + # limit=3: det_A returns 3 finding ids on one page (reaching the limit so its + # loop exits by limit) but one id is a duplicate, so it ingests only 2 events + # (A1, A2) and leaves budget for det_B to drain its single finding. + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det_A", "det_B"]}], + list_finding_ids_res=[ + # det_A: 3 ids fill the limit => loop exits by limit; NextToken still + # pending => truncated mid-boundary at the partial second A_t2. + {"FindingIds": ["finding_A1", "finding_A2", "finding_A2"], "NextToken": "more"}, + # det_B: no NextToken => fully drained. + {"FindingIds": ["finding_B1"]}, + ], + get_findings_res=[ + {"Findings": [finding_a1, finding_a2]}, + {"Findings": [finding_b1]}, + ], + ) + + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 0, 0), + last_ids={}, + severity="Low", + limit=3, + ) + + # All distinct fetched findings are still returned this run (nothing dropped). + assert sorted(e["Id"] for e in events) == ["finding_A1", "finding_A2", "finding_B1"] + + # detector_A: truncated mid-boundary => cursor rolls back to the last + # fully-drained second (A_t1), NOT the partial A_t2. last_ids holds A_t1's + # sibling so it is not re-ingested next run. + assert new_collect_from["det_A"] == a_t1 + assert new_last_ids["det_A"] == ["finding_A1"] + + # detector_B: fully drained => cursor advances normally to its finding's + # second with its own independent last_ids. + assert new_collect_from["det_B"] == b_t1 + assert new_last_ids["det_B"] == ["finding_B1"] + + +# --------------------------------------------------------------------------- +# Single-boundary-cannot-drain — an entire truncated page shares one UpdatedAt. +# +# When every fetched finding falls on the SAME second and the page is truncated +# by `limit` (NextToken set), there is no earlier second to roll back to. The +# cursor must stay pinned on that second and seen_ids must accumulate every id +# on it, so the next run makes progress via same-second dedup rather than +# skipping the un-fetched siblings. +# --------------------------------------------------------------------------- + + +def test_single_boundary_cannot_drain_keeps_cursor_and_accumulates_seen_ids(mocker): + """ + Given: + A single detector whose truncated page (NextToken set) contains two + findings that all share ONE UpdatedAt second — there is no earlier, + fully-drained second to roll the cursor back to. + + When: + get_events runs and the loop exits due to limit with a pending token. + + Then: + The cursor stays put on that single second and last_ids (seen_ids) + accumulates every id observed on that second, so the next run dedups + them and ingests the remaining siblings instead of skipping them. + + Reference: + AWSGuardDutyEventCollector.get_events — single-second truncation branch + (fully_drained is empty; keep cursor and accumulate seen ids). + """ + ts = "2026-04-10T01:35:09.000000" + finding_1 = update_finding_id(FINDING.copy(), "finding_1", updated_at=ts) + finding_2 = update_finding_id(FINDING.copy(), "finding_2", updated_at=ts) + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + # NextToken set => truncated by limit; every finding shares the same second. + list_finding_ids_res=[{"FindingIds": ["finding_1", "finding_2"], "NextToken": "more"}], + get_findings_res=[{"Findings": [finding_1, finding_2]}], + ) + + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 35, 0), + last_ids={}, + severity="Low", + limit=2, + ) + + # Both fetched findings are returned this run. + assert sorted(e["Id"] for e in events) == ["finding_1", "finding_2"] + # No earlier second to roll back to => the cursor stays on the single second. + assert new_collect_from == {"det1": ts} + # seen_ids accumulates EVERY id on the pinned second so the next run dedups + # them and can make forward progress via same-second dedup. + assert new_last_ids == {"det1": ["finding_1", "finding_2"]} + + +# --------------------------------------------------------------------------- +# Cursor-pin regression guard — a finding re-updated at the SAME boundary every +# run must not freeze the fetch forever. Ingestion must resume when a +# later-boundary update finally arrives. +# --------------------------------------------------------------------------- + + +def test_same_boundary_reupdate_does_not_freeze_fetch_forever(mocker): + """ + Given: + finding_X keeps being re-returned at the SAME cursor second T1 across + several runs (an already-ingested finding re-read by the inclusive Gte + query). The cursor stays pinned at T1 during those runs — that is + correct, because nothing new has actually happened. + + When: + On a later run GuardDuty finally moves finding_X's UpdatedAt to a + strictly-later second T2 (a genuine new occurrence). + + Then: + Ingestion resumes: finding_X is ingested and the cursor advances to T2. + This proves the same-boundary pinning is a bounded, correct pause — not + a permanent freeze — and guards against a regression where a + perpetually same-second finding would block the fetch indefinitely. + + Reference: + AWSGuardDutyEventCollector.get_events — dedup is scoped to the cursor + second, so a later-boundary update always resumes forward progress. + """ + t1 = "2026-07-03T15:48:55.563000" + t2 = "2026-07-04T08:04:55.843000" + + # ---- Runs that keep re-reading finding_X at the SAME second T1 ---------- + for run_number in range(3): + same_second_again = update_finding_id(FINDING.copy(), "finding_X", updated_at=t1) + mocker.resetall() + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_X"]}], + get_findings_res=[{"Findings": [same_second_again]}], + ) + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={"det1": t1}, + collect_from_default=datetime(2026, 7, 3, 7, 0, 0), + last_ids={"det1": ["finding_X"]}, + severity="Low", + limit=10, + ) + # Nothing new happened this run: no re-ingestion, cursor pinned at T1. + assert events == [], f"run {run_number}: same-second re-read must not re-ingest finding_X" + assert new_collect_from == {"det1": t1}, f"run {run_number}: cursor must stay pinned while only T1 re-reads arrive" + assert new_last_ids == {"det1": ["finding_X"]} + + # ---- Later run: a genuine later-boundary update finally arrives --------- + reupdated = update_finding_id(FINDING.copy(), "finding_X", updated_at=t2) + mocker.resetall() + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_X"]}], + get_findings_res=[{"Findings": [reupdated]}], + ) + events, new_last_ids, new_collect_from = get_events( + aws_client=client, + collect_from={"det1": t1}, + collect_from_default=datetime(2026, 7, 3, 7, 0, 0), + last_ids={"det1": ["finding_X"]}, + severity="Low", + limit=10, + ) + + # Ingestion resumes the moment a later-boundary update arrives — the pin was + # a bounded pause, never a permanent freeze. + assert [e["Id"] for e in events] == ["finding_X"], ( + "Cursor-pin regression: a finding re-updated to a later boundary must " + "resume ingestion, but nothing was ingested (fetch frozen)." + ) + assert new_collect_from == {"det1": t2}, "Cursor must advance to the later boundary once a real update arrives." + assert new_last_ids == {"det1": ["finding_X"]} + + +# --------------------------------------------------------------------------- +# _time / timestamp mapping — every emitted event must expose a field XSIAM can +# map to _time. XSIAM derives _time from the finding's UpdatedAt, which the +# fetch cursor is also built from, so every event must carry a string UpdatedAt. +# --------------------------------------------------------------------------- + + +def test_emitted_events_expose_updated_at_for_time_mapping(mocker): + """ + Given: + A detector returning findings with distinct UpdatedAt timestamps. + + When: + get_events fetches and normalizes them for emission to XSIAM. + + Then: + Every emitted event exposes an UpdatedAt field (the source XSIAM maps to + _time), it is a string (datetime fields are stringified before emission + so JSON serialization succeeds), and it matches the value the fetch + cursor is derived from via _event_updated_at. + + Reference: + AWSGuardDutyEventCollector.convert_events_with_datetime_to_str and + _event_updated_at — the cursor/_time source field. + """ + ts_1 = "2026-04-10T01:35:08.000000" + ts_2 = "2026-04-10T01:35:09.000000" + finding_1 = update_finding_id(FINDING.copy(), "finding_1", updated_at=ts_1) + finding_2 = update_finding_id(FINDING.copy(), "finding_2", updated_at=ts_2) + + client, _, _, _ = create_mocked_client( + mocker=mocker, + list_detectors_res=[{"DetectorIds": ["det1"]}], + list_finding_ids_res=[{"FindingIds": ["finding_1", "finding_2"]}], + get_findings_res=[{"Findings": [finding_1, finding_2]}], + ) + + events, _, _ = get_events( + aws_client=client, + collect_from={}, + collect_from_default=datetime(2026, 4, 10, 1, 0, 0), + last_ids={}, + severity="Low", + limit=10, + ) + + assert len(events) == 2 + for event in events: + # XSIAM maps _time from UpdatedAt — it must be present on every event. + assert "UpdatedAt" in event, "Every emitted event must expose UpdatedAt for XSIAM _time mapping." + # Stringified before emission so setLastRun / send_events_to_xsiam can serialize it. + assert isinstance(event["UpdatedAt"], str) + # The _time source must equal the field the fetch cursor is derived from. + assert _event_updated_at(event) == event["UpdatedAt"] + + # Each event's _time source is the expected, distinct UpdatedAt value. + assert sorted(_event_updated_at(e) for e in events) == [ts_1, ts_2] diff --git a/Packs/AWS-GuardDuty/ReleaseNotes/1_4_0.md b/Packs/AWS-GuardDuty/ReleaseNotes/1_4_0.md new file mode 100644 index 000000000000..d1084894d604 --- /dev/null +++ b/Packs/AWS-GuardDuty/ReleaseNotes/1_4_0.md @@ -0,0 +1,9 @@ + +#### Integrations + +##### AWS - GuardDuty Event Collector + +- Added the **Exclude archived/suppressed findings** parameter. When selected, findings archived in GuardDuty (including those archived by suppression rules) are no longer fetched. +- Fixed an issue where GuardDuty findings sharing the same update time (second resolution) could be silently dropped between fetch cycles. The integration now tracks every finding ID at the cursor second instead of a single ID, eliminating same-second sibling loss. +- Fixed an issue where findings could be skipped when a fetch cycle was truncated by the configured number of events to fetch. The fetch cursor is no longer advanced into a partially-consumed second, guaranteeing that same-second findings are not lost between cycles. +- Fixed an issue where a recurring finding that was re-updated after being recorded in the fetch state was silently suppressed by deduplication, which also prevented the fetch cursor from advancing. Deduplication is now scoped to the finding's cursor second, so a finding whose update time advances is correctly ingested as a new event. diff --git a/Packs/AWS-GuardDuty/pack_metadata.json b/Packs/AWS-GuardDuty/pack_metadata.json index 1962088878cb..202384ded98a 100644 --- a/Packs/AWS-GuardDuty/pack_metadata.json +++ b/Packs/AWS-GuardDuty/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - GuardDuty", "description": "Amazon Web Services Guard Duty Service (gd)", "support": "xsoar", - "currentVersion": "1.3.72", + "currentVersion": "1.4.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-Lambda/Integrations/AWS_Lambda/AWS_Lambda.yml b/Packs/AWS-Lambda/Integrations/AWS_Lambda/AWS_Lambda.yml index d479c97a65ff..65d43efa7aae 100644 --- a/Packs/AWS-Lambda/Integrations/AWS_Lambda/AWS_Lambda.yml +++ b/Packs/AWS-Lambda/Integrations/AWS_Lambda/AWS_Lambda.yml @@ -1363,7 +1363,7 @@ script: - description: The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) up to the maximum session duration setting for the role. name: roleSessionDuration outputs: [] - dockerimage: demisto/boto3py3:1.0.0.3575453 + dockerimage: demisto/boto3py3:1.0.0.10221838 runonce: false subtype: python3 script: '' diff --git a/Packs/AWS-Lambda/ReleaseNotes/1_3_18.md b/Packs/AWS-Lambda/ReleaseNotes/1_3_18.md new file mode 100644 index 000000000000..8c10e9ebbc68 --- /dev/null +++ b/Packs/AWS-Lambda/ReleaseNotes/1_3_18.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS - Lambda + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-Lambda/pack_metadata.json b/Packs/AWS-Lambda/pack_metadata.json index 7b4a7c774398..aeb88894bbd3 100644 --- a/Packs/AWS-Lambda/pack_metadata.json +++ b/Packs/AWS-Lambda/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - Lambda", "description": "Amazon Web Services Serverless Compute service (lambda)", "support": "xsoar", - "currentVersion": "1.3.17", + "currentVersion": "1.3.18", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-NetworkFirewall/Integrations/AWS-NetworkFirewall/AWS-NetworkFirewall.yml b/Packs/AWS-NetworkFirewall/Integrations/AWS-NetworkFirewall/AWS-NetworkFirewall.yml index 909c5b59887f..143c6e043aba 100644 --- a/Packs/AWS-NetworkFirewall/Integrations/AWS-NetworkFirewall/AWS-NetworkFirewall.yml +++ b/Packs/AWS-NetworkFirewall/Integrations/AWS-NetworkFirewall/AWS-NetworkFirewall.yml @@ -1522,7 +1522,7 @@ script: - contextPath: AWS-NetworkFirewall.SubnetChangeProtection description: A setting indicating whether the firewall is protected against changes to the subnet associations. Use this setting to protect against accidentally modifying the subnet associations for a firewall that is in use. When you create a firewall, the operation initializes this setting to TRUE. type: Unknown - dockerimage: demisto/boto3py3:1.0.0.3575453 + dockerimage: demisto/boto3py3:1.0.0.10221838 runonce: false script: '-' subtype: python3 diff --git a/Packs/AWS-NetworkFirewall/ReleaseNotes/1_0_14.md b/Packs/AWS-NetworkFirewall/ReleaseNotes/1_0_14.md new file mode 100644 index 000000000000..7e1337367cad --- /dev/null +++ b/Packs/AWS-NetworkFirewall/ReleaseNotes/1_0_14.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS Network Firewall + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-NetworkFirewall/pack_metadata.json b/Packs/AWS-NetworkFirewall/pack_metadata.json index 027095373583..3cfe1f018a43 100644 --- a/Packs/AWS-NetworkFirewall/pack_metadata.json +++ b/Packs/AWS-NetworkFirewall/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - Network Firewall", "description": "Amazon Web Services Network Firewall", "support": "xsoar", - "currentVersion": "1.0.13", + "currentVersion": "1.0.14", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS-SecurityHub/Integrations/AWS_SecurityHub/AWS_SecurityHub.yml b/Packs/AWS-SecurityHub/Integrations/AWS_SecurityHub/AWS_SecurityHub.yml index 758ff3f2b5a6..d9158b623e51 100644 --- a/Packs/AWS-SecurityHub/Integrations/AWS_SecurityHub/AWS_SecurityHub.yml +++ b/Packs/AWS-SecurityHub/Integrations/AWS_SecurityHub/AWS_SecurityHub.yml @@ -2082,7 +2082,7 @@ script: description: The UTC timestamp in seconds since the last update. The incident is only updated if it was modified after the last update time. - name: get-mapping-fields description: Returns the list of fields to map in outgoing mirroring. This command is only used for debugging purposes. - dockerimage: demisto/boto3py3:1.0.0.7704170 + dockerimage: demisto/boto3py3:1.0.0.10221838 isfetch: true ismappable: true isremotesyncin: true diff --git a/Packs/AWS-SecurityHub/ReleaseNotes/1_3_55.md b/Packs/AWS-SecurityHub/ReleaseNotes/1_3_55.md new file mode 100644 index 000000000000..748245515caa --- /dev/null +++ b/Packs/AWS-SecurityHub/ReleaseNotes/1_3_55.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS - Security Hub + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS-SecurityHub/pack_metadata.json b/Packs/AWS-SecurityHub/pack_metadata.json index 1e4ace7705fb..aaa6f7a805c8 100644 --- a/Packs/AWS-SecurityHub/pack_metadata.json +++ b/Packs/AWS-SecurityHub/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - Security Hub", "description": "Amazon Web Services Security Hub Service.", "support": "xsoar", - "currentVersion": "1.3.54", + "currentVersion": "1.3.55", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS_DynamoDB/Integrations/AWS_DynamoDB/AWS_DynamoDB.yml b/Packs/AWS_DynamoDB/Integrations/AWS_DynamoDB/AWS_DynamoDB.yml index b1b6b008d6f6..4bf7b4e03699 100644 --- a/Packs/AWS_DynamoDB/Integrations/AWS_DynamoDB/AWS_DynamoDB.yml +++ b/Packs/AWS_DynamoDB/Integrations/AWS_DynamoDB/AWS_DynamoDB.yml @@ -2999,7 +2999,7 @@ script: description: The name of the TTL attribute used to store the expiration time for items in the table. - contextPath: AWS-DynamoDB.TimeToLiveSpecification description: Represents the output of an UpdateTimeToLive operation. - dockerimage: demisto/boto3py3:1.0.0.3575453 + dockerimage: demisto/boto3py3:1.0.0.10221838 script: '' subtype: python3 type: python diff --git a/Packs/AWS_DynamoDB/ReleaseNotes/1_0_41.md b/Packs/AWS_DynamoDB/ReleaseNotes/1_0_41.md new file mode 100644 index 000000000000..3304600c75e6 --- /dev/null +++ b/Packs/AWS_DynamoDB/ReleaseNotes/1_0_41.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Amazon DynamoDB + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS_DynamoDB/pack_metadata.json b/Packs/AWS_DynamoDB/pack_metadata.json index f8194dd1f494..0ab71ac23c50 100644 --- a/Packs/AWS_DynamoDB/pack_metadata.json +++ b/Packs/AWS_DynamoDB/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Amazon DynamoDB", "description": "Amazon DynamoDB Amazon DynamoDB is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability. DynamoDB lets you offload the administrative burdens of operating and scaling a distributed database, so that you don't have to worry about hardware provisioning, setup and configuration, replication, software patching, or cluster scaling. With DynamoDB, you can create database tables that can store and retrieve any amount of data, and serve any level of request traffic. You can scale up or scale down your tables' throughput capacity without downtime or performance degradation, and use the AWS Management Console to monitor resource utilization and performance metrics. DynamoDB automatically spreads the data and traffic for your tables over a sufficient number of servers to handle your throughput and storage requirements, while maintaining consistent and fast performance. All of your data is stored on solid state disks (SSDs) and automatically replicated across multiple Availability Zones in an AWS region, providing built-in high availability and data durability. ", "support": "xsoar", - "currentVersion": "1.0.40", + "currentVersion": "1.0.41", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AWS_Sagemaker/Integrations/AWSSagemaker/AWSSagemaker.yml b/Packs/AWS_Sagemaker/Integrations/AWSSagemaker/AWSSagemaker.yml index c625fd33e603..6dec8972b154 100644 --- a/Packs/AWS_Sagemaker/Integrations/AWSSagemaker/AWSSagemaker.yml +++ b/Packs/AWS_Sagemaker/Integrations/AWSSagemaker/AWSSagemaker.yml @@ -73,7 +73,7 @@ script: description: The predication probability (range 0-1). type: number description: Classify input text (usually email content). - dockerimage: demisto/boto3py3:1.0.0.7260599 + dockerimage: demisto/boto3py3:1.0.0.10221838 tests: - Test Sagemaker fromversion: 5.0.0 diff --git a/Packs/AWS_Sagemaker/ReleaseNotes/1_1_14.md b/Packs/AWS_Sagemaker/ReleaseNotes/1_1_14.md new file mode 100644 index 000000000000..082d2a50a8c6 --- /dev/null +++ b/Packs/AWS_Sagemaker/ReleaseNotes/1_1_14.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AWS Sagemaker + +- Updated the Docker image to: *demisto/boto3py3:1.0.0.10221838*. + diff --git a/Packs/AWS_Sagemaker/pack_metadata.json b/Packs/AWS_Sagemaker/pack_metadata.json index 8723de377c48..7ce06173bbbf 100644 --- a/Packs/AWS_Sagemaker/pack_metadata.json +++ b/Packs/AWS_Sagemaker/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS Sagemaker", "description": "AWS Sagemaker - Demisto Phishing Email Classifier", "support": "xsoar", - "currentVersion": "1.1.13", + "currentVersion": "1.1.14", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/Active_Directory_Query/Integrations/Active_Directory_Query/Active_Directory_Query.yml b/Packs/Active_Directory_Query/Integrations/Active_Directory_Query/Active_Directory_Query.yml index cb8a76c0e380..9bb89b30b98f 100644 --- a/Packs/Active_Directory_Query/Integrations/Active_Directory_Query/Active_Directory_Query.yml +++ b/Packs/Active_Directory_Query/Integrations/Active_Directory_Query/Active_Directory_Query.yml @@ -850,7 +850,7 @@ script: outputs: - contextPath: ActiveDirectory.ValidCredentials description: List of usernames that successfully logged in. - dockerimage: demisto/ldap:2.9.1.6911241 + dockerimage: demisto/ldap:2.9.1.9062583 ismappable: true isremotesyncout: true runonce: false diff --git a/Packs/Active_Directory_Query/ReleaseNotes/1_7_3.md b/Packs/Active_Directory_Query/ReleaseNotes/1_7_3.md new file mode 100644 index 000000000000..4cf3586d6966 --- /dev/null +++ b/Packs/Active_Directory_Query/ReleaseNotes/1_7_3.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Active Directory Query v2 + +- Updated the Docker image to: *demisto/ldap:2.9.1.9062583*. + diff --git a/Packs/Active_Directory_Query/ReleaseNotes/1_7_4.md b/Packs/Active_Directory_Query/ReleaseNotes/1_7_4.md new file mode 100644 index 000000000000..758ea3f9be5a --- /dev/null +++ b/Packs/Active_Directory_Query/ReleaseNotes/1_7_4.md @@ -0,0 +1,7 @@ + +#### Scripts + +##### IAMInitADUser + +- Updated the Docker image to: *demisto/py3-tools:1.0.0.10120494*. + diff --git a/Packs/Active_Directory_Query/ReleaseNotes/1_7_5.md b/Packs/Active_Directory_Query/ReleaseNotes/1_7_5.md new file mode 100644 index 000000000000..0481d7c3a52a --- /dev/null +++ b/Packs/Active_Directory_Query/ReleaseNotes/1_7_5.md @@ -0,0 +1,7 @@ + +#### Scripts + +##### SendEmailToManager + +- Updated the Docker image to: *demisto/python3:3.12.13.10116658*. + diff --git a/Packs/Active_Directory_Query/Scripts/IAMInitADUser/IAMInitADUser.yml b/Packs/Active_Directory_Query/Scripts/IAMInitADUser/IAMInitADUser.yml index 594182f5c7e0..b66729f29728 100644 --- a/Packs/Active_Directory_Query/Scripts/IAMInitADUser/IAMInitADUser.yml +++ b/Packs/Active_Directory_Query/Scripts/IAMInitADUser/IAMInitADUser.yml @@ -80,7 +80,7 @@ tags: - active directory - Utility type: python -dockerimage: demisto/py3-tools:1.0.0.4027705 +dockerimage: demisto/py3-tools:1.0.0.10120494 runas: DBotWeakRole tests: - Active Directory Test diff --git a/Packs/Active_Directory_Query/Scripts/SendEmailToManager/SendEmailToManager.yml b/Packs/Active_Directory_Query/Scripts/SendEmailToManager/SendEmailToManager.yml index ee3676b54b6a..1d47d2c81a3e 100644 --- a/Packs/Active_Directory_Query/Scripts/SendEmailToManager/SendEmailToManager.yml +++ b/Packs/Active_Directory_Query/Scripts/SendEmailToManager/SendEmailToManager.yml @@ -42,7 +42,7 @@ dependson: timeout: 0s tests: - No test -dockerimage: demisto/python3:3.12.8.3296088 +dockerimage: demisto/python3:3.12.13.10116658 supportedModules: - agentix - xsiam diff --git a/Packs/Active_Directory_Query/pack_metadata.json b/Packs/Active_Directory_Query/pack_metadata.json index 37c20a0fdaf4..417700581a07 100644 --- a/Packs/Active_Directory_Query/pack_metadata.json +++ b/Packs/Active_Directory_Query/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Active Directory Query", "description": "Active Directory Query integration enables you to access and manage Active Directory objects (users, contacts, and computers).", "support": "xsoar", - "currentVersion": "1.7.2", + "currentVersion": "1.7.5", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AggregatedScripts/ReleaseNotes/1_3_51.md b/Packs/AggregatedScripts/ReleaseNotes/1_3_51.md new file mode 100644 index 000000000000..b061a8e46587 --- /dev/null +++ b/Packs/AggregatedScripts/ReleaseNotes/1_3_51.md @@ -0,0 +1,33 @@ +<~PLATFORM> +#### Scripts + +##### isolate-endpoint + +- Documentation and metadata improvements. + +##### ip-enrichment + +- Updated the Docker image to: *demisto/python3:3.12.13.10404775*. +- Documentation and metadata improvements. + +##### domain-enrichment + +- Documentation and metadata improvements. + +##### get-endpoint-data + +- Documentation and metadata improvements. + +##### file-enrichment + +- Documentation and metadata improvements. + +##### quarantine-file + +- Documentation and metadata improvements. + +##### get-user-data + +- Documentation and metadata improvements. + + diff --git a/Packs/AggregatedScripts/ReleaseNotes/1_3_52.md b/Packs/AggregatedScripts/ReleaseNotes/1_3_52.md new file mode 100644 index 000000000000..45cbe928e68c --- /dev/null +++ b/Packs/AggregatedScripts/ReleaseNotes/1_3_52.md @@ -0,0 +1,3 @@ +## Aggregated Scripts + +Documentation and metadata improvements. diff --git a/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment.py b/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment.py index 14d39e586f6a..941540efe639 100644 --- a/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment.py +++ b/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment.py @@ -60,15 +60,28 @@ def domain_enrichment_script( command_batch2: list[Command] = [] if is_xsiam(): demisto.debug("Command Batch 2: Internal commands (for XSIAM)") - command_batch2.append( - Command( - name="core-get-domain-analytics-prevalence", - args={"domain_name": valid_inputs}, - command_type=CommandType.INTERNAL, - brand="Cortex Core - IR", # keep the brand you use elsewhere - context_output_mapping={"Core.AnalyticsPrevalence.Domain": "Core.AnalyticsPrevalence.Domain"}, + # On platform, we use the built-in commands and brand Builtin + if is_platform(): + command_batch2.append( + Command( + name="getDomainAnalyticsPrevalence", + args={"domain_name": valid_inputs}, + command_type=CommandType.BUILTIN, + brand="Builtin", + context_output_mapping={"Core.AnalyticsPrevalence.Domain": "Core.AnalyticsPrevalence.Domain"}, + ignore_using_brand=True, + ) + ) + else: + command_batch2.append( + Command( + name="core-get-domain-analytics-prevalence", + args={"domain_name": valid_inputs}, + command_type=CommandType.INTERNAL, + brand="Cortex Core - IR", # keep the brand you use elsewhere + context_output_mapping={"Core.AnalyticsPrevalence.Domain": "Core.AnalyticsPrevalence.Domain"}, + ) ) - ) demisto.debug("Command Batch 2: Enriching indicators") command_batch2.append( Command( diff --git a/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment_test.py b/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment_test.py index 79c8580a188a..08ddbf3301d9 100644 --- a/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment_test.py +++ b/Packs/AggregatedScripts/Scripts/DomainEnrichment/DomainEnrichment_test.py @@ -1,5 +1,7 @@ import json + import demistomock as demisto +import pytest from DomainEnrichment import domain_enrichment_script @@ -38,6 +40,7 @@ def test_domain_enrichment_script_end_to_end_with_batch_file(mocker): mocker.patch.object(demisto, "args", return_value={"domain_list": ",".join(domain_list)}) mocker.patch("DomainEnrichment.is_xsiam", return_value=True) + mocker.patch("DomainEnrichment.is_platform", return_value=False) # extractIndicators -> validates input mocker.patch( "AggregatedCommandApiModule.execute_command", @@ -163,3 +166,55 @@ def _fake_execute_list_of_batches(self, list_of_batches, brands_to_run=None, ver assert isinstance(core_ctx, list) assert len(core_ctx) == 2 assert {d["Domain"] for d in core_ctx} == {"example.com", "example2.com"} + + +def test_domain_enrichment_uses_builtin_command_on_platform(mocker): + """ + Given: + - Running on the unified Cortex platform (is_xsiam and is_platform both True). + When: + - domain_enrichment_script builds its command batches. + Then: + - The prevalence command is the built-in "getDomainAnalyticsPrevalence". + - Its command_type is CommandType.BUILTIN (not the legacy INTERNAL core command). + """ + from AggregatedCommandApiModule import CommandType + + domain_list = ["example.com"] + captured_batches: dict = {} + + class _StopAfterCapture(Exception): + pass + + def _capture_batches(self, list_of_batches, brands_to_run=None, verbose=False): + captured_batches["batches"] = list_of_batches + raise _StopAfterCapture + + mocker.patch.object(demisto, "args", return_value={"domain_list": ",".join(domain_list)}) + mocker.patch("DomainEnrichment.is_xsiam", return_value=True) + mocker.patch("DomainEnrichment.is_platform", return_value=True) + mocker.patch( + "AggregatedCommandApiModule.execute_command", + return_value=[{"EntryContext": {"ExtractedIndicators": {"Domain": domain_list}}}], + ) + mocker.patch("AggregatedCommandApiModule.IndicatorsSearcher", return_value=iter([])) + mocker.patch.object( + demisto, + "getModules", + return_value={"coreir": {"state": "active", "brand": "Cortex Core - IR"}}, + ) + mocker.patch("AggregatedCommandApiModule.BatchExecutor.execute_list_of_batches", _capture_batches) + + with pytest.raises(_StopAfterCapture): + domain_enrichment_script( + domain_list=domain_list, + external_enrichment=True, + verbose=True, + enrichment_brands=["Cortex Core - IR"], + additional_fields=False, + ) + + b2_cmds = captured_batches["batches"][1] + prevalence_cmds = [c for c in b2_cmds if c.name == "getDomainAnalyticsPrevalence"] + assert len(prevalence_cmds) == 1 + assert prevalence_cmds[0].command_type == CommandType.BUILTIN diff --git a/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment.py b/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment.py index 68f2c3d2f7bd..1e09bec54035 100644 --- a/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment.py +++ b/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment.py @@ -91,15 +91,28 @@ def file_enrichment_script( # Add the prevalence command only when at least one SHA256 hash is present. sha256_inputs = [file for file in valid_inputs if get_hash_type(file) == "sha256"] if sha256_inputs: - command_batch2.append( - Command( - name="core-get-hash-analytics-prevalence", - args={"sha256": sha256_inputs}, - brand="Cortex Core - IR", - command_type=CommandType.INTERNAL, - context_output_mapping={}, + # On platform, we use the built-in commands and brand Builtin + if is_platform(): + command_batch2.append( + Command( + name="getHashAnalyticsPrevalence", + args={"sha256": sha256_inputs}, + brand="Builtin", + command_type=CommandType.BUILTIN, + context_output_mapping={}, + ignore_using_brand=True, + ) + ) + else: + command_batch2.append( + Command( + name="core-get-hash-analytics-prevalence", + args={"sha256": sha256_inputs}, + brand="Cortex Core - IR", + command_type=CommandType.INTERNAL, + context_output_mapping={}, + ) ) - ) commands = [command_batch1, command_batch2] demisto.debug("Commands Batches") diff --git a/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment_test.py b/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment_test.py index e6b85d85972c..eef6a0435dbf 100644 --- a/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment_test.py +++ b/Packs/AggregatedScripts/Scripts/FileEnrichment/FileEnrichment_test.py @@ -1,4 +1,5 @@ import json + import demistomock as demisto from FileEnrichment import file_enrichment_script @@ -64,6 +65,7 @@ def __iter__(self): "core": {"state": "active", "brand": "Cortex Core - IR"}, }, ) + mocker.patch("FileEnrichment.is_platform", return_value=False) # ---------- Mock BatchExecutor.execute_list_of_batches using JSON ---------- def _fake_execute_list_of_batches(self, list_of_batches, brands_to_run=None, verbose=False): @@ -158,10 +160,11 @@ def _fake_execute_list_of_batches(self, list_of_batches, brands_to_run=None, ver assert wf2.get("Reliability") == "Low" -def _capture_built_commands(mocker, file_list): +def _capture_built_commands(mocker, file_list, is_platform=False): """Helper: runs file_enrichment_script with mocks and returns the command batches exactly as they were built by file_enrichment_script, captured before any brand/type filtering is applied.""" mocker.patch.object(demisto, "args", return_value={"file_hash": ",".join(file_list)}) + mocker.patch("FileEnrichment.is_platform", return_value=is_platform) def extractIndicators_side_effect(cmd, args=None, extract_contents=False, fail_on_error=True): if cmd == "extractIndicators": @@ -260,3 +263,24 @@ def test_prevalence_command_included_when_sha256_present(mocker): prevalence_cmds = [cmd for batch in batches for cmd in batch if cmd.name == "core-get-hash-analytics-prevalence"] assert len(prevalence_cmds) == 1 assert prevalence_cmds[0].args == {"sha256": [sha256_hash]} + + +def test_file_enrichment_uses_builtin_command_on_platform(mocker): + """ + Given: + - A SHA256 hash as input, running on the unified Cortex platform (is_platform True). + When: + - file_enrichment_script builds the command batches. + Then: + - The prevalence command is the built-in "getHashAnalyticsPrevalence". + - Its command_type is CommandType.BUILTIN (not the legacy INTERNAL core command). + """ + from AggregatedCommandApiModule import CommandType + + sha256_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + batches = _capture_built_commands(mocker, [sha256_hash], is_platform=True) + + prevalence_cmds = [cmd for batch in batches for cmd in batch if cmd.name == "getHashAnalyticsPrevalence"] + assert len(prevalence_cmds) == 1 + assert prevalence_cmds[0].command_type == CommandType.BUILTIN + assert "core-get-hash-analytics-prevalence" not in [cmd.name for batch in batches for cmd in batch] diff --git a/Packs/AggregatedScripts/Scripts/GetEndpointData/GetEndpointData.py b/Packs/AggregatedScripts/Scripts/GetEndpointData/GetEndpointData.py index 01874b8c0460..036e97786c11 100644 --- a/Packs/AggregatedScripts/Scripts/GetEndpointData/GetEndpointData.py +++ b/Packs/AggregatedScripts/Scripts/GetEndpointData/GetEndpointData.py @@ -25,6 +25,7 @@ class Brands(StrEnum): CORTEX_CORE_IR = "Cortex Core - IR" FIREEYE_HX_V2 = "FireEyeHX v2" GENERIC_COMMAND = "Generic Command" + BUILTIN = "Builtin" @classmethod def get_all_values(cls) -> list[str]: @@ -134,6 +135,10 @@ def is_brand_available(self, command: Command) -> bool: bool: True if the brand is in both the list of brands to run and the set of enabled brands; False otherwise. """ + # Builtin commands are injected by the server on the unified platform, + # so they are always available there and are not tied to an installed integration brand. + if command.brand == Brands.BUILTIN: + return is_platform() return False if not self.is_brand_in_brands_to_run(command) else command.brand in self._enabled_brands def get_enabled_brands(self): @@ -346,7 +351,10 @@ def get_command_results( continue if entry_type == EntryType.ERROR or entry_type == EntryType.WARNING: - command_error_outputs.append(hr_to_command_results(command, args, contents, entry_type=entry_type)) # type: ignore[arg-type] + # Built-in commands may return no entries (None) on a not-found result + # guard against 'NoneType' object is not iterable + if error_result := hr_to_command_results(command, args, contents, entry_type=entry_type): # type: ignore[arg-type] + command_error_outputs.append(error_result) elif entry_type == EntryType.NOTE: command_context_outputs.append(entry.get("EntryContext", {})) human_readable_outputs.append(entry.get("HumanReadable") or "") @@ -509,8 +517,8 @@ def initialize_commands( not_found_checker="was not found", ), Command( - brand=Brands.CORTEX_CORE_IR, - name="core-list-risky-hosts", + brand=Brands.BUILTIN if is_platform() else Brands.CORTEX_CORE_IR, + name="getRiskyHosts" if is_platform() else "core-list-risky-hosts", output_keys=["Core.RiskyHost"], args_mapping={"host_id": "endpoint_hostname"}, output_mapping={"id": "Hostname", "risk_level": "RiskLevel"}, @@ -532,8 +540,8 @@ def initialize_commands( list_args_commands = [ Command( - brand=Brands.CORTEX_CORE_IR, - name="core-get-endpoints", + brand=Brands.BUILTIN if is_platform() else Brands.CORTEX_CORE_IR, + name="getEndpoints" if is_platform() else "core-get-endpoints", output_keys=["Core.Endpoint"], args_mapping={"endpoint_id_list": "endpoint_id", "ip_list": "endpoint_ip", "hostname": "endpoint_hostname"}, output_mapping={ @@ -614,7 +622,7 @@ def run_single_args_commands( ) if endpoint_output: - if command.brand in [Brands.CORTEX_XDR_IR, Brands.CORTEX_CORE_IR]: + if command.brand in [Brands.CORTEX_XDR_IR, Brands.CORTEX_CORE_IR, Brands.BUILTIN]: update_endpoint_in_mapping(endpoint_output, ir_mapping) else: endpoint_outputs_list.extend(endpoint_output) @@ -667,7 +675,7 @@ def run_list_args_commands( ) if endpoint_output: - if command.brand in [Brands.CORTEX_XDR_IR, Brands.CORTEX_CORE_IR]: + if command.brand in [Brands.CORTEX_XDR_IR, Brands.CORTEX_CORE_IR, Brands.BUILTIN]: add_endpoint_to_mapping(endpoint_output, ir_mapping) else: multiple_endpoint_outputs.extend(endpoint_output) diff --git a/Packs/AggregatedScripts/Scripts/GetUserData/GetUserData.py b/Packs/AggregatedScripts/Scripts/GetUserData/GetUserData.py index d09b2ef27ec1..0c1d43120377 100644 --- a/Packs/AggregatedScripts/Scripts/GetUserData/GetUserData.py +++ b/Packs/AggregatedScripts/Scripts/GetUserData/GetUserData.py @@ -69,6 +69,10 @@ def is_brand_available(self, command: Command) -> bool: Returns: bool: True if the brand is available and in the list of brands to run, False otherwise. """ + # Builtin commands are injected by the server on the unified platform, + # so they are always available there and are not tied to an installed integration brand. + if command.brand == "Builtin": + return is_platform() is_available = command.brand in self._enabled_brands if not is_available: demisto.debug(f"Skipping command '{command.name}' since the brand '{command.brand}' is not available.") @@ -292,7 +296,8 @@ def run_execute_command(command_name: str, args: dict[str, Any]) -> tuple[list[d errors_command_results = [] human_readable_list = [] entry_context_list = [] - for entry in res: + # Built-in commands may return no entries (None) on a not-found result, guard against 'NoneType' object is not iterable + for entry in res or []: entry_context_list.append((entry.get("EntryContext") or {}) | {"instance": entry.get("ModuleName")}) if is_error(entry): errors_command_results.extend(prepare_human_readable(command_name, args, get_error(entry), is_error=True)) @@ -1294,9 +1299,9 @@ def main(): ################################# readable_output, outputs = get_core_and_xdr_data( # type: ignore[assignment] modules=modules, - brand_name="Cortex Core - IR", - first_command="core-list-risky-users", - second_command="core-list-users", + brand_name="Builtin" if is_platform() else "Cortex Core - IR", + first_command="getRiskyUsers" if is_platform() else "core-list-risky-users", + second_command="getSystemUsers" if is_platform() else "core-list-users", user_names=users_names, additional_fields=additional_fields, list_non_risky_users=list_non_risky_users, diff --git a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.py b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.py index ac82d9a6aabf..8cf893ff2701 100644 --- a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.py +++ b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.py @@ -85,15 +85,26 @@ def ip_enrichment_script( if is_xsiam(): demisto.debug("Command Batch 2: Internal commands (for XSIAM)") - command_batch2.append( - Command( + # On platform, we use the built-in commands and brand Builtin, which is always enabled + # and does not require enabling the integration. + if is_platform(): + prevalence_command = Command( + name="getIPAnalyticsPrevalence", + args={"ip_address": valid_inputs}, + command_type=CommandType.BUILTIN, + brand="Builtin", + context_output_mapping={"Core.AnalyticsPrevalence.Ip": "Core.AnalyticsPrevalence.Ip"}, + ignore_using_brand=True, + ) + else: + prevalence_command = Command( name="core-get-IP-analytics-prevalence", args={"ip_address": valid_inputs}, command_type=CommandType.INTERNAL, brand="Cortex Core - IR", context_output_mapping={"Core.AnalyticsPrevalence.Ip": "Core.AnalyticsPrevalence.Ip"}, ) - ) + command_batch2.append(prevalence_command) demisto.debug("Command Batch 2: Enriching indicators") command_batch2.append( diff --git a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.yml b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.yml index 916d50bd432f..92d805d42ec9 100644 --- a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.yml +++ b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment.yml @@ -373,11 +373,11 @@ tags: timeout: '0' type: python subtype: python3 -dockerimage: demisto/python3:3.12.13.7444307 +dockerimage: demisto/python3:3.12.13.10404775 fromversion: 8.0.0 marketplaces: - xsoar_saas - marketplacev2 - platform tests: -- IPEnrichment-Test \ No newline at end of file +- No tests (auto formatted) diff --git a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment_test.py b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment_test.py index a89d644c0847..8bb3eeca775c 100644 --- a/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment_test.py +++ b/Packs/AggregatedScripts/Scripts/IPEnrichment/IPEnrichment_test.py @@ -1,5 +1,7 @@ import json + import demistomock as demisto +import pytest from IPEnrichment import ip_enrichment_script @@ -39,6 +41,7 @@ def test_ip_enrichment_script_end_to_end_with_batch_file(mocker): # is_xsiam mocker.patch("IPEnrichment.is_xsiam", return_value=True) + mocker.patch("IPEnrichment.is_platform", return_value=False) # extractIndicators -> validates input mocker.patch( "AggregatedCommandApiModule.execute_command", @@ -178,6 +181,7 @@ def test_ip_enrichment_script_with_internal_ip(mocker): ip_list = ["192.168.1.1"] mocker.patch.object(demisto, "args", return_value={"ip_list": ",".join(ip_list)}) mocker.patch("IPEnrichment.is_xsiam", return_value=True) + mocker.patch("IPEnrichment.is_platform", return_value=False) mocker.patch( "AggregatedCommandApiModule.execute_command", return_value=[{"EntryContext": {"ExtractedIndicators": {"IP": ip_list}}}], @@ -248,3 +252,55 @@ def _fake_execute_list_of_batches(self, list_of_batches, brands_to_run=None, ver assert len(endpoint_ctx) == 2 assert {e["Brand"] for e in endpoint_ctx} == {"Core"} assert {e["Hostname"] for e in endpoint_ctx} == {"host-1", "host-2"} + + +def test_ip_enrichment_uses_builtin_command_on_platform(mocker): + """ + Given: + - Running on the unified Cortex platform (is_xsiam and is_platform both True). + When: + - ip_enrichment_script builds its command batches. + Then: + - The prevalence command is the built-in "getIPAnalyticsPrevalence". + - Its command_type is CommandType.BUILTIN (not the legacy INTERNAL core command). + """ + from AggregatedCommandApiModule import CommandType + + ip_list = ["1.1.1.1"] + captured_batches: dict = {} + + class _StopAfterCapture(Exception): + pass + + def _capture_batches(self, list_of_batches, brands_to_run=None, verbose=False): + captured_batches["batches"] = list_of_batches + raise _StopAfterCapture + + mocker.patch.object(demisto, "args", return_value={"ip_list": ",".join(ip_list)}) + mocker.patch("IPEnrichment.is_xsiam", return_value=True) + mocker.patch("IPEnrichment.is_platform", return_value=True) + mocker.patch( + "AggregatedCommandApiModule.execute_command", + return_value=[{"EntryContext": {"ExtractedIndicators": {"IP": ip_list}}}], + ) + mocker.patch("AggregatedCommandApiModule.IndicatorsSearcher", return_value=iter([])) + mocker.patch.object( + demisto, + "getModules", + return_value={"coreir": {"state": "active", "brand": "Cortex Core - IR"}}, + ) + mocker.patch("AggregatedCommandApiModule.BatchExecutor.execute_list_of_batches", _capture_batches) + + with pytest.raises(_StopAfterCapture): + ip_enrichment_script( + ip_list=ip_list, + external_enrichment=True, + verbose=True, + enrichment_brands=["Cortex Core - IR"], + additional_fields=False, + ) + + b2_cmds = captured_batches["batches"][1] + prevalence_cmds = [c for c in b2_cmds if c.name == "getIPAnalyticsPrevalence"] + assert len(prevalence_cmds) == 1 + assert prevalence_cmds[0].command_type == CommandType.BUILTIN diff --git a/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint.py b/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint.py index 0f0cda141d97..02869375b3d3 100644 --- a/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint.py +++ b/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint.py @@ -14,6 +14,7 @@ class Brands(StrEnum): FIREEYE_HX_V2 = "FireEyeHX v2" CROWDSTRIKE_FALCON = "CrowdstrikeFalcon" CORTEX_CORE_IR = "Cortex Core - IR" + BUILTIN = "Builtin" MICROSOFT_DEFENDER_ADVANCED_THREAT_PROTECTION = "Microsoft Defender Advanced Threat Protection" @classmethod @@ -55,9 +56,9 @@ def initialize_commands() -> list: """ commands = [ Command( - # Can be used only in XSIAM - brand=Brands.CORTEX_CORE_IR, - name="core-isolate-endpoint", + # On platform, we use the built-in commands and brand Builtin + brand=Brands.BUILTIN if is_platform() else Brands.CORTEX_CORE_IR, + name="isolateEndpoint" if is_platform() else "core-isolate-endpoint", arg_mapping={"endpoint_id": "endpoint_id"}, ), Command( diff --git a/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint_test.py b/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint_test.py index 78a7f113c487..48bff3a60049 100644 --- a/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint_test.py +++ b/Packs/AggregatedScripts/Scripts/IsolateEndpoint/IsolateEndpoint_test.py @@ -7,7 +7,13 @@ def test_get_all_values_returns_expected_list(): """ Ensure get_all_values returns all brand values in the correct order. """ - expected_list = ["FireEyeHX v2", "CrowdstrikeFalcon", "Cortex Core - IR", "Microsoft Defender Advanced Threat Protection"] + expected_list = [ + "FireEyeHX v2", + "CrowdstrikeFalcon", + "Cortex Core - IR", + "Builtin", + "Microsoft Defender Advanced Threat Protection", + ] assert Brands.get_all_values() == expected_list diff --git a/Packs/AggregatedScripts/Scripts/QuarantineFile/QuarantineFile.py b/Packs/AggregatedScripts/Scripts/QuarantineFile/QuarantineFile.py index ba3b554b5094..bab5d258c238 100644 --- a/Packs/AggregatedScripts/Scripts/QuarantineFile/QuarantineFile.py +++ b/Packs/AggregatedScripts/Scripts/QuarantineFile/QuarantineFile.py @@ -26,6 +26,10 @@ def values(cls): def normalize(cls, value: str): _ALIASES = { "Microsoft Defender ATP": "Microsoft Defender Advanced Threat Protection", + # On the unified platform, get-endpoint-data reports Core endpoints under the + # built-in brand "Builtin". Map it to "Cortex Core - IR" so the correct handler + # is selected and the legacy Core quarantine commands are used. + "Builtin": cls.CORTEX_CORE_IR.value, } """Normalize a brand string (alias → canonical enum).""" canonical = _ALIASES.get(value, value) diff --git a/Packs/AggregatedScripts/pack_metadata.json b/Packs/AggregatedScripts/pack_metadata.json index 8f1ad4e7fc73..63c73ad0dd22 100644 --- a/Packs/AggregatedScripts/pack_metadata.json +++ b/Packs/AggregatedScripts/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Aggregated Scripts", "description": "A pack containing all aggregated scripts.", "support": "xsoar", - "currentVersion": "1.3.50", + "currentVersion": "1.3.52", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AlienVault_OTX/Integrations/AlienVault_OTX_v2/AlienVault_OTX_v2.yml b/Packs/AlienVault_OTX/Integrations/AlienVault_OTX_v2/AlienVault_OTX_v2.yml index ed5a984d92cc..a6fc2ea7a49a 100644 --- a/Packs/AlienVault_OTX/Integrations/AlienVault_OTX_v2/AlienVault_OTX_v2.yml +++ b/Packs/AlienVault_OTX/Integrations/AlienVault_OTX_v2/AlienVault_OTX_v2.yml @@ -554,7 +554,7 @@ script: - contextPath: URL.Relationships.EntityBType description: The type of the destination of the relationship. type: string - dockerimage: demisto/python3:3.12.13.9059085 + dockerimage: demisto/python3:3.12.13.10116658 runonce: false script: '-' type: python diff --git a/Packs/AlienVault_OTX/ReleaseNotes/1_1_53.md b/Packs/AlienVault_OTX/ReleaseNotes/1_1_53.md new file mode 100644 index 000000000000..9659a457a6f6 --- /dev/null +++ b/Packs/AlienVault_OTX/ReleaseNotes/1_1_53.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### AlienVault OTX v2 + +- Updated the Docker image to: *demisto/python3:3.12.13.10116658*. + diff --git a/Packs/AlienVault_OTX/pack_metadata.json b/Packs/AlienVault_OTX/pack_metadata.json index 57f9487a2251..7b116255a957 100644 --- a/Packs/AlienVault_OTX/pack_metadata.json +++ b/Packs/AlienVault_OTX/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AlienVault OTX", "description": "Query Indicators of Compromise in AlienVault OTX.", "support": "xsoar", - "currentVersion": "1.1.52", + "currentVersion": "1.1.53", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/ApiModules/ReleaseNotes/2_4_36.md b/Packs/ApiModules/ReleaseNotes/2_4_36.md new file mode 100644 index 000000000000..725559127f26 --- /dev/null +++ b/Packs/ApiModules/ReleaseNotes/2_4_36.md @@ -0,0 +1,6 @@ + +#### Scripts + +##### NGINXApiModule + +- Improved implementation of cache concurrency control to better handle a high volume of concurrent requests. diff --git a/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule.py b/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule.py index 47971bb59d95..4a8581665c4c 100644 --- a/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule.py +++ b/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule.py @@ -1,8 +1,9 @@ import os import subprocess import traceback +import uuid +from itertools import count from math import ceil -from multiprocessing import Process from pathlib import Path from signal import SIGUSR1 from string import Template @@ -13,7 +14,7 @@ import requests from CommonServerPython import * # noqa: F401 from flask.logging import default_handler -from gevent.pywsgi import WSGIServer +from gevent.pywsgi import WSGIHandler, WSGIServer from CommonServerUserPython import * @@ -21,19 +22,373 @@ class Handler: @staticmethod def write(msg: str): - demisto.info(msg) + # gevent's pywsgi writes one Common-Log-Format access line per request here. + # Tag it so it is easy to grep for and correlate with the nginx access log. + # Kept at debug: the structured "wsgi request:". + demisto.debug(f"wsgi access: {msg.rstrip()}") class ErrorHandler: @staticmethod def write(msg: str): - demisto.error(f"wsgi error: {msg}") + demisto.error(f"wsgi error: {msg.rstrip()}") DEMISTO_LOGGER: Handler = Handler() ERROR_LOGGER: ErrorHandler = ErrorHandler() +# --- Request lifecycle / concurrency instrumentation ------------------------- +# Monotonic sequence so every request can be correlated across the start line, +# the end line and the gevent access line. +_REQUEST_SEQ = count(1) +# The environ key under which we stash the per-request id so DemistoWSGIHandler +# can print the same id on the "wsgi access:" line. +REQUEST_ID_ENVIRON_KEY = "nginxapimodule.request_id" +# The environ key under which the middleware stashes the number of body bytes the +# APP produced, so the gevent handler can compare it against the bytes actually +# written to the socket and flag a discrepancy on the "wsgi access:" line. +APP_BYTES_ENVIRON_KEY = "nginxapimodule.app_bytes" +# When the app-produced bytes and the socket-sent bytes differ by MORE than this +# many bytes, the access line includes a short body_diff note (a 1-byte trailing +# newline difference is normal and must not be reported). +BODY_DIFF_THRESHOLD_BYTES = 1000 + + +def _new_request_id(environ: dict) -> str: + """Return a correlation id for the request. + + Prefers nginx's ``X-Request-ID`` (propagated via ``proxy_set_header``) so the + same id appears in the nginx access log, the ``wsgi request:`` line and the + ``wsgi access:`` line. Falls back to a fresh short uuid when nginx did not + forward one (e.g. direct upstream hit during tests). + """ + forwarded = environ.get(_header_env_key("X-Request-ID")) + return forwarded if forwarded else uuid.uuid4().hex[:12] + + +def _next_request_seq() -> int: + """Return the next monotonic per-request sequence number.""" + return next(_REQUEST_SEQ) + + +def _iso_now() -> str: + """Human-readable UTC timestamp for absolute received/responded times in logs.""" + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + +# Request headers worth capturing for client/cache/conditional diagnostics. +# Shared by both the gevent access log (DemistoWSGIHandler) and the per-request +# RequestLoggingMiddleware so the two log lines stay consistent. +LOGGED_REQUEST_HEADERS = ( + "X-Forwarded-For", + "X-Real-IP", + "X-Forwarded-Proto", + "X-Original-URI", + "Host", + "User-Agent", + "Range", + "If-None-Match", + "If-Modified-Since", + "Authorization", +) + + +def _header_env_key(header_name: str) -> str: + # WSGI exposes request headers as HTTP_ in the environ. + return "HTTP_" + header_name.upper().replace("-", "_") + + +def format_request_headers(environ: dict) -> str: + """Render the allow-listed request headers from a WSGI ``environ`` into a log string. + + Produces a space-separated ``Name="value"`` sequence for every header in + ``LOGGED_REQUEST_HEADERS`` that is present. The ``Authorization`` header is + reduced to ``present`` so credentials are never written to the logs. + + Args: + environ (dict): The WSGI request environment. + + Returns: + str: e.g. ``X-Forwarded-For="1.2.3.4" Host="server" Authorization="present"``. + """ + header_parts = [] + for header_name in LOGGED_REQUEST_HEADERS: + value = environ.get(_header_env_key(header_name)) + if value is None: + continue + if header_name == "Authorization": + value = "present" + header_parts.append(f'{header_name}="{value}"') + return " ".join(header_parts) + + +class DemistoWSGIHandler(WSGIHandler): + """gevent WSGI handler that enriches the access line with transfer diagnostics + headers. + + gevent's ``WSGIServer`` formats one Common-Log-Format access line per request + via ``WSGIHandler.format_request()`` and writes it to the configured ``log`` + (our :class:`Handler`, which prefixes it with ``wsgi access:``). The default + line only reports the body bytes gevent sent, which - when compared with the + ``content_length`` the app declared in :class:`RequestLoggingMiddleware` - can + silently hide truncated/aborted transfers (client disconnects mid-stream). + + To make that gap explicit on a single line, ``format_request`` appends data + that only gevent's handler knows: + * ``sent_bytes`` - bytes actually written to the socket (``self.response_length``). + * ``content_length`` - the ``Content-Length`` the app declared (``-`` if chunked/unset). + * ``truncated`` - ``true`` when a numeric ``Content-Length`` was declared but + fewer bytes reached the socket (i.e. the transfer was cut short). + * ``connection`` - ``close`` or ``keep-alive``, derived from ``self.close_connection``. + It also appends the allow-listed request headers (via :func:`format_request_headers`) + so the real client/conditional headers are visible alongside the byte accounting. + """ + + @staticmethod + def _coerce_int(value: Any) -> int | None: + try: + return int(value) + except (TypeError, ValueError): + return None + + def _transfer_diagnostics(self, environ: dict) -> str: + # Bytes gevent actually wrote to the socket for the response body. + sent_bytes = getattr(self, "response_length", None) + sent_bytes_str = str(sent_bytes) if sent_bytes is not None else "-" + + # The Content-Length the app declared, if any (chunked responses won't have one). + declared_length: int | None = None + for header_key, header_value in getattr(self, "response_headers", None) or []: + if header_key.lower() == "content-length": + declared_length = self._coerce_int(header_value) + break + content_length_str = str(declared_length) if declared_length is not None else "-" + + # The number of body bytes the APP produced (counted by RequestLoggingMiddleware + # and stashed in environ), so we can compare the two independent counts. + app_bytes = self._coerce_int(environ.get(APP_BYTES_ENVIRON_KEY)) + app_bytes_str = str(app_bytes) if app_bytes is not None else "-" + + # truncated=true only when we can prove fewer bytes reached the socket than promised. + if declared_length is not None and isinstance(sent_bytes, int): + truncated = "true" if sent_bytes < declared_length else "false" + else: + truncated = "unknown" + + # Whether the connection is being closed (vs reused for keep-alive). + connection = "close" if getattr(self, "close_connection", True) else "keep-alive" + + # client_disconnected mirrors the truncated detection but phrased from the + # connection's point of view: a proven short write means the peer went away + # (or nginx/the firewall dropped) before the full body was sent. + client_disconnected = "true" if truncated == "true" else "false" + + # Number of requests served on this (keep-alive) connection, if gevent tracks it. + requests_on_conn = getattr(self, "_requests_on_connection", None) + requests_on_conn_str = str(requests_on_conn) if requests_on_conn is not None else "-" + + diagnostics = ( + f"sent_bytes={sent_bytes_str} app_bytes={app_bytes_str} content_length={content_length_str} " + f"truncated={truncated} client_disconnected={client_disconnected} " + f"connection={connection} requests_on_conn={requests_on_conn_str}" + ) + + # When the app produced N bytes but a meaningfully different number reached + # the socket, surface a short, human-readable explanation right on the line. + diff_note = self._body_diff_note(app_bytes, sent_bytes, declared_length) + if diff_note: + diagnostics = f"{diagnostics} {diff_note}" + return diagnostics + + @staticmethod + def _body_diff_note(app_bytes: int | None, sent_bytes: Any, declared_length: int | None) -> str: + """Return a short 'body_diff=...' note when app vs socket bytes diverge. + + Only emitted when both counts are known and differ by MORE than + ``BODY_DIFF_THRESHOLD_BYTES`` (a normal trailing-newline 1-byte delta is + ignored). The note names the likely cause so a reader instantly knows + whether the body was truncated on the wire or grew/shrank unexpectedly. + """ + if app_bytes is None or not isinstance(sent_bytes, int): + return "" + delta = sent_bytes - app_bytes + if abs(delta) <= BODY_DIFF_THRESHOLD_BYTES: + return "" + if delta < 0: + # Fewer bytes on the wire than the app produced -> cut short. + missing = -delta + pct = (missing / app_bytes * 100) if app_bytes else 0.0 + cause = ( + "client/proxy closed the connection before the full body was sent" + if (declared_length is not None and sent_bytes < declared_length) + else "write stopped before the app finished streaming" + ) + detail = ( + f'body_diff="app produced {app_bytes} body bytes but only {sent_bytes} ' + f"reached the socket; {missing} bytes ({pct:.1f}%) were not sent - " + f'likely {cause}"' + ) + else: + extra = delta + detail = ( + f'body_diff="socket sent {sent_bytes} bytes, {extra} MORE than the ' + f"{app_bytes} body bytes the app counted - extra bytes are likely " + f'response framing/headers or a double-write"' + ) + return detail + + def format_request(self): + environ = self.environ or {} + base_line = super().format_request() + rid = environ.get(REQUEST_ID_ENVIRON_KEY, "-") + parts = [base_line, f"rid={rid}", self._transfer_diagnostics(environ)] + headers_str = format_request_headers(environ) + if headers_str: + parts.append(headers_str) + return " ".join(parts) + + +class RequestLoggingMiddleware: + """WSGI middleware that emits a detailed, structured log line per request. + + This is the Python-side counterpart to the nginx ``edl_detailed`` access log. + Because nginx serves cache HITs without ever reaching this upstream, a request + that appears in the nginx log with ``cache=HIT`` but is absent here was served + entirely from cache - making the cache-vs-upstream distinction explicit. + + For every request it logs: the real client (X-Forwarded-For / X-Real-IP) as + forwarded by nginx, the original URI, conditional/range headers, user-agent, + the cache status nginx attached (X-Proxy-Cache), the response status, the + number of body bytes written, and the wall-clock time spent in the app. + """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + # --- request received ------------------------------------------------ + # Wall-clock for absolute timestamps; perf_counter for accurate durations. + t_received = time.time() + received_iso = _iso_now() + start_perf = time.perf_counter() + + # Correlation id: reuse nginx's X-Request-ID when present, else generate. + # Stash it in environ so DemistoWSGIHandler prints the SAME id on the + # "wsgi access:" line, tying the two python log lines together. + rid = _new_request_id(environ) + environ[REQUEST_ID_ENVIRON_KEY] = rid + + method = environ.get("REQUEST_METHOD", "-") + path = environ.get("PATH_INFO", "-") + query = environ.get("QUERY_STRING", "") + full_path = f"{path}?{query}" if query else path + remote_addr = environ.get("REMOTE_ADDR", "-") + + # How long the request waited inside nginx (queueing / cache-lock / connect) + # BEFORE this app handler started. nginx forwards its forward-time as the + # X-Request-Start epoch header; the difference vs our receive time is the + # gap that explains why nginx's time_total_secs >> the app's elapsed time. + nginx_wait_str = "-" + request_start_header = environ.get(_header_env_key("X-Request-Start")) + if request_start_header: + try: + nginx_wait = t_received - float(request_start_header) + # Clamp tiny negative values from clock skew to 0. + nginx_wait_str = f"{max(0.0, nginx_wait):.3f}s" + except (TypeError, ValueError): + nginx_wait_str = "-" + + # Collect the interesting request headers (auth is reduced to presence only). + headers_str = format_request_headers(environ) + + # Monotonic sequence per request for correlation across log lines. + seq = _next_request_seq() + + # Emit a START line. A request that then hangs for 125s (or never finishes + # because the client vanished) is visible here when debug logging is on, + # even though its END line would only appear much later (or not at all). + # Kept at debug to reduce steady-state volume; the END line is authoritative. + demisto.debug( + f"wsgi request-start: rid={rid} seq={seq} received={received_iso} " + f"nginx_wait={nginx_wait_str} " + f'client={remote_addr} method={method} uri="{full_path}" {headers_str}' + ) + + # Capture the response status/headers via a wrapped start_response. + response_info: dict = {"status": "-", "cache": "-", "edl_size": "-", "content_length": "-"} + # When start_response is invoked = app produced its response headers + # (time-to-headers). Captured via closure so we can log it below. + timings: dict = {"t_headers": None, "t_first_byte": None} + + def logging_start_response(status, response_headers, exc_info=None): + if timings["t_headers"] is None: + timings["t_headers"] = time.perf_counter() + response_info["status"] = status.split(" ", 1)[0] if status else "-" + for header_key, header_value in response_headers: + lowered = header_key.lower() + if lowered == "x-proxy-cache": + response_info["cache"] = header_value + elif lowered == "x-edl-size": + response_info["edl_size"] = header_value + elif lowered == "content-length": + response_info["content_length"] = header_value + return start_response(status, response_headers, exc_info) + + bytes_sent = 0 + error_str = "-" + t_app_start = time.perf_counter() + try: + result = self.app(environ, logging_start_response) + # Count the bytes actually produced by the app so we can detect + # size-vs-bytes mismatches / truncated bodies on the Python side. + for chunk in result: + if chunk: + if timings["t_first_byte"] is None: + timings["t_first_byte"] = time.perf_counter() + bytes_sent += len(chunk) + # Continuously expose the app-produced byte count so the gevent + # handler (DemistoWSGIHandler) can compare it with the bytes that + # actually reached the socket - even if we are cut off mid-stream. + environ[APP_BYTES_ENVIRON_KEY] = bytes_sent + yield chunk + if hasattr(result, "close"): + result.close() + except Exception as exc: # noqa: BLE001 - we re-raise after logging + # A client disconnect / write error surfaces here; record it so the + # END line explains why a transfer stopped short. + error_str = type(exc).__name__ + demisto.debug(f"WSGI app raised {error_str} while streaming response; re-raising.\n{traceback.format_exc()}") + raise + finally: + end_perf = time.perf_counter() + responded_iso = _iso_now() + + # Phase breakdown so it's obvious WHERE the time went: + # time_to_headers : app entry -> start_response (the "thinking" time). + # ttfb : request received -> first body byte. + # stream_time : first byte -> last byte (the streaming cost). + # elapsed : total time inside the app. + elapsed = end_perf - start_perf + time_to_headers = (timings["t_headers"] - t_app_start) if timings["t_headers"] else None + ttfb = (timings["t_first_byte"] - start_perf) if timings["t_first_byte"] else None + stream_time = end_perf - timings["t_first_byte"] if timings["t_first_byte"] else None + + def _fmt(value): + return f"{value:.3f}s" if value is not None else "-" + + demisto.info( + f"wsgi request: rid={rid} seq={seq} " + f"received={received_iso} responded={responded_iso} " + f"client={remote_addr} method={method} uri=\"{full_path}\" " + f"status={response_info['status']} " + f"app_bytes={bytes_sent} content_length={response_info['content_length']} " + f'cache="{response_info["cache"]}" edl_size={response_info["edl_size"]} ' + f"nginx_wait={nginx_wait_str} time_to_headers={_fmt(time_to_headers)} ttfb={_fmt(ttfb)} " + f"stream_time={_fmt(stream_time)} elapsed={_fmt(elapsed)} " + f"error={error_str} {headers_str}" + ) + + # nginx server params NGINX_SERVER_ACCESS_LOG = "/var/log/nginx/access.log" NGINX_SERVER_ERROR_LOG = "/var/log/nginx/error.log" @@ -44,32 +399,158 @@ def write(msg: str): ssl_certificate {NGINX_SSL_CRT_FILE}; ssl_certificate_key {NGINX_SSL_KEY_FILE}; """ +# Detailed access log format with self-explanatory key names, grouped so the line +# reads top-to-bottom like the life of one request. Fields are ordered: +# +# 1. IDENTITY (first, so every line starts with "who/when/which request"): +# when_finished - ISO8601 timestamp of when nginx FINISHED the request +# and wrote this log line (i.e. request end). NOTE: stock +# nginx evaluates ALL log variables at write-time, so there +# is no built-in variable for the absolute arrival time. +# Derive it from the durations below: +# arrival = when_finished - time_total_secs +# sent_upstream = arrival + time_to_upstream_connect_secs +# request_id - unique id for THIS request; the SAME id is sent to +# the Python upstream (X-Request-ID) and printed on the +# "wsgi request:"/"wsgi access:" lines, so one request +# can be followed across nginx and Python. +# connection_id - id of the TCP connection (many requests can share one). +# requests_on_connection- how many requests have used this keep-alive connection +# (rising numbers = reuse; helps spot CLOSE_WAIT buildup). +# +# 2. WHO CONNECTED (client identity through the proxy chain): +# client_real_ip - the true client (the firewall), via X-Real-IP. +# client_forwarded_chain- full X-Forwarded-For chain of proxies in between. +# client_nearest_peer - the immediate TCP peer nginx saw (usually localhost/edge). +# client_user_agent - the client's User-Agent string. +# +# 3. WHAT WAS ASKED (the request itself): +# request_method - GET/HEAD/... +# request_uri - the full requested URI. +# request_host - the Host header. +# request_range - Range header (partial-content requests). +# request_if_none_match - ETag the client already has (conditional GET). +# request_if_modified_since - date the client already has (conditional GET). + +# +# 4. WHAT HAPPENED (response outcome + cache decision): +# response_status - HTTP status code returned. +# cache_status - HIT/MISS/BYPASS/EXPIRED/STALE/UPDATING/REVALIDATED. +# upstream_address - which upstream served it (empty on a cache HIT). +# response_etag - ETag we returned. +# +# 5. HOW BIG (payload accounting; mismatch = truncated/aborted transfer): +# response_body_bytes - body bytes sent to the client. +# response_total_bytes - total bytes sent (headers + body). +# edl_indicator_count - number of indicators in the EDL response. +# edl_origin_count - number of origin indicators before filtering. +# +# 6. HOW LONG (ALL timings grouped together at the end, in seconds). These are the +# source of truth for the timeline - use them to reconstruct WHEN nginx received +# the request from the client and WHEN it forwarded it to the upstream: +# time_total_secs - total time to serve the request (from when nginx +# read the first client bytes until the last byte was +# sent to the client). The client request therefore +# ARRIVED at: when_finished - time_total_secs. +# time_to_upstream_connect_secs - seconds AFTER arrival that nginx established the +# upstream connection. This marks WHEN nginx forwarded +# the request to the upstream: +# sent_to_upstream = (when_finished - time_total_secs) +# + time_to_upstream_connect_secs +# ("-" on a cache HIT, because no upstream request was made.) +# time_upstream_headers_secs - seconds after the upstream request until the upstream +# returned its response headers. +# time_upstream_response_secs- seconds after the upstream request until the upstream +# finished sending its response. +# time_edl_query_secs - time the EDL spent building the list (app side). +NGINX_LOG_FORMAT = """ +log_format edl_detailed + 'when_finished=$time_iso8601 request_id=$request_id ' + 'connection_id=$connection requests_on_connection=$connection_requests ' + 'client_real_ip="$http_x_real_ip" client_forwarded_chain="$http_x_forwarded_for" ' + 'client_nearest_peer=$remote_addr client_user_agent="$http_user_agent" ' + 'request_method=$request_method request_uri="$request_uri" request_host="$host" ' + 'request_range="$http_range" request_if_none_match="$http_if_none_match" ' + 'request_if_modified_since="$http_if_modified_since" ' + 'response_status=$status cache_status=$upstream_cache_status upstream_address="$upstream_addr" ' + 'response_etag="$sent_http_etag" ' + 'response_body_bytes=$body_bytes_sent response_total_bytes=$bytes_sent ' + 'edl_indicator_count="$sent_http_x_edl_size" edl_origin_count="$sent_http_x_edl_origin_size" ' + 'time_total_secs=$request_time time_to_upstream_connect_secs="$upstream_connect_time" ' + 'time_upstream_headers_secs="$upstream_header_time" time_upstream_response_secs="$upstream_response_time" ' + 'time_edl_query_secs="$sent_http_x_edl_query_time_secs"'; +""" NGINX_SERVER_CONF = """ +$log_format + +# Per-URI concurrency zone used to FAIL (not queue) concurrent cold-MISS requests +# for the same URI. Keyed on $request_uri so the limit is per-resource, not per-client. +limit_conn_zone $request_uri zone=concurrent_conn_zone:1m; server { listen $port default_server $ssl; $sslcerts + # Per-request detailed access log + verbose error log so cache decisions, + # real client IPs, timings and upstream warnings are all captured. + access_log $access_log_path edl_detailed; + error_log $error_log_path info; + + proxy_cache_key $scheme$proxy_host$request_uri$extra_cache_key; $proxy_set_range_header $extra_headers -# Thundering-herd protection -proxy_cache_lock on; -proxy_cache_lock_timeout $cache_lock_timeout; -proxy_cache_lock_age $cache_lock_age; +# Cache-vs-fetch policy (TWO-TIER, HIT-safe fail-fast on cold MISS) +# --------------------------------------------------------------------------- +# We want two behaviors that plain `proxy_cache_lock` cannot give together: +# * On a TRUE cold MISS (nothing cached to fall back on), concurrent requests +# for the SAME URI must FAIL FAST (429) instead of queueing behind a lock. +# * On STALE / UPDATING (a cached copy exists but is expired/being refreshed) +# clients must be served the STALE copy and must NEVER be rejected. +# +# `limit_conn` is the only mechanism that REJECTS (proxy_cache_lock only WAITS), +# but it runs in the preaccess phase - before the cache status is known - so +# applying it on the public cache location would ALSO count cache HITs and could +# 429 two simultaneous HITs of the same URI. nginx's `proxy_cache` cannot route +# "only on miss" to a different location within one server, so we use TWO server +# blocks: +# Tier 1 = public server on $port : does the cache read. HIT / STALE / UPDATING +# are answered here from the cache (see proxy_cache_use_stale + +# proxy_cache_background_update) and, on a MISS, proxy_pass to Tier 2. +# HIT/STALE/UPDATING never leave Tier 1, so they are never counted and +# never rejected. +# Tier 2 = internal server on localhost:$fetchport : reached ONLY when Tier 1's +# cache must populate a new entry (true MISS / expired-with-no-stale). +# It carries `limit_conn ... 1`, so the first fetch for a URI proceeds +# and every concurrent same-URI fetch is rejected immediately with 429. # Cache validity by status proxy_cache_valid 200 301 302 $cache_refresh_rate; # Optional: cache other responses briefly (helps absorb spikes) proxy_cache_valid 404 $cache_404_ttl; +# NEVER cache the fail-fast rejection: a 429 from the Tier-2 limiter is a +# transient "someone else is already building this" signal. Caching it (via the +# `any` rule below) would poison the URI and serve 429s even after the real +# content is ready. `0s` = do not cache; being status-specific it overrides `any`. +proxy_cache_valid 429 0s; +# NEVER cache upstream errors / timeouts either. A 504 means the build exceeded +# proxy_read_timeout and a 5xx is a transient upstream failure - caching them via +# the `any` rule below would poison the URI and keep serving the error (as a +# cache HIT) even after a later build would succeed, blocking recovery. `0s` = do +# not cache; being status-specific these override `any`. Note: this does NOT stop +# `proxy_cache_use_stale timeout http_50x` from serving a previously-cached GOOD +# copy on timeout - that stale-serving is desirable and is what we keep. +proxy_cache_valid 500 502 503 504 0s; proxy_cache_valid any $cache_default_ttl; # Revalidation (use conditional requests when expired) proxy_cache_revalidate on; -# Serve stale content in failure/update scenarios +# Serve stale content in failure/update scenarios. `updating` is what lets a +# STALE entry be served immediately to every waiting client while a single +# background refresh runs - so STALE/UPDATING never reach the Tier-2 limiter. proxy_cache_use_stale updating error @@ -80,7 +561,8 @@ def write(msg: str): http_503 http_504; -# Background refresh of expired cache +# Background refresh of expired cache: the refresh runs as a detached subrequest, +# so serving stale is always allowed and never fails. proxy_cache_background_update on; # Static test file @@ -89,10 +571,26 @@ def write(msg: str): default_type text/html; } - # Proxy everything to python + # ---- Tier 1: public cache front -------------------------------------- + # Serves HIT / STALE / UPDATING from the cache. On a MISS, the cache module + # fetches from Tier 2 (the internal fetch server) where the fail-fast limiter + # lives. HITs/STALE/UPDATING are served straight from cache and never reach + # Tier 2, so they are never counted by limit_conn and never rejected. location / { - proxy_pass http://localhost:$serverport/; - add_header X-Proxy-Cache $upstream_cache_status; + proxy_pass http://localhost:$fetchport/; + + # CRITICAL: the base flask-nginx image ENABLES the cache lock in the + # http{} block, and that setting is inherited here. While enabled, + # concurrent cold-MISS requests for the same URI WAIT for the first + # request to populate the cache (then get served HIT) - they never fall + # through to Tier 2, so the fail-fast limit_conn never fires. We MUST turn + # the inherited lock OFF here so misses proceed to Tier 2 and the 2nd+ + # concurrent miss is rejected with 429 instead of queued. + proxy_cache_lock off; + + # Surface the cache decision both to the client and (via the access log + # variable) to our logging: HIT/MISS/BYPASS/EXPIRED/STALE/UPDATING/REVALIDATED. + add_header X-Proxy-Cache $upstream_cache_status always; $extra_headers # allow bypassing the cache with an arg of nocache=1 ie http://server:7000/?nocache=1 proxy_cache_bypass $arg_nocache; @@ -100,6 +598,65 @@ def write(msg: str): proxy_connect_timeout 3600; proxy_send_timeout 3600; send_timeout 3600; + + # Forward the real client identity through the chain so the Python (gevent) + # upstream's WSGI middleware can log the actual firewall/client instead of + # 127.0.0.1. Tier 2 forwards these on to the app. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Original-URI $request_uri; + # Propagate nginx's per-request id to the upstream so the same request_id + # appears in the nginx access log AND the wsgi request/access lines, + # giving a single correlation id end-to-end. + proxy_set_header X-Request-ID $request_id; + # Forward, as an epoch (seconds.ms), the moment nginx is about to hand the + # request to the upstream. The WSGI middleware compares this with its own + # receive time and logs "nginx_wait" - the time the request spent inside + # nginx (queueing / cache-lock / connect) BEFORE the app started working. + proxy_set_header X-Request-Start $msec; + } + +} + +# ---- Tier 2: internal fetch server (cold-MISS path only) ----------------- +# Reached ONLY via Tier 1's cache fetch on a MISS. Because Tier 1 answers +# HIT/STALE/UPDATING from cache, only requests that actually need the upstream +# arrive here, so `limit_conn ... 1` counts ONLY cold-miss fetches: the first +# request for a URI builds the cache entry, and every concurrent same-URI fetch +# is rejected immediately with 429. Listens on loopback only, so it is never +# reachable directly by external clients. +server { + listen localhost:$fetchport; + + access_log $access_log_path edl_detailed; + error_log $error_log_path info; + + location / { + limit_conn concurrent_conn_zone 1; + limit_conn_status 429; + + # Tier 2 must NOT cache: caching is owned entirely by Tier 1 (the public + # server). If proxy_cache is inherited from the http{} block, disable it + # here so this tier is purely the rate-limited cold-MISS fetch path. + proxy_cache off; + + proxy_pass http://localhost:$serverport/; + + # Preserve the forwarded client identity headers set by Tier 1. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $http_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Original-URI $http_x_original_uri; + proxy_set_header X-Request-ID $http_x_request_id; + proxy_set_header X-Request-Start $http_x_request_start; + + proxy_read_timeout $timeout; + proxy_connect_timeout 3600; + proxy_send_timeout 3600; + send_timeout 3600; } } @@ -127,28 +684,21 @@ def create_nginx_server_conf(file_path: str, port: int, params: dict): timeout = _normalize_nginx_time(params.get("timeout"), default="3600", param_name="timeout") cache_refresh_rate = _normalize_nginx_time(params.get("cache_refresh_rate"), default=timeout, param_name="cache_refresh_rate") - # Ensure cache lock directives are at least as large as the upstream timeout. Otherwise, when an - # upstream request takes longer than the lock timeout/age, waiting clients bypass the cache lock - # and stampede the upstream (each waiter then produces an uncached response), defeating the purpose - # of `proxy_cache_lock on`. Defaults match `timeout`; explicit smaller values are bumped up. - cache_lock_timeout = _normalize_nginx_time(params.get("cache_lock_timeout"), default=timeout, param_name="cache_lock_timeout") - cache_lock_age = _normalize_nginx_time(params.get("cache_lock_age"), default=timeout, param_name="cache_lock_age") cache_404_ttl = _normalize_nginx_time(params.get("cache_404_ttl"), default="1m", param_name="cache_404_ttl") cache_default_ttl = _normalize_nginx_time(params.get("cache_default_ttl"), default="1m", param_name="cache_default_ttl") - # Ensure cache_refresh_rate is at least as large as timeout, and apply the same anti-stampede - # floor to the cache lock directives. All values are now guaranteed to end in "s" (the helper - # always returns `s`), so an O(1) integer compare on the prefix is safe. + # Ensure cache_refresh_rate is at least as large as timeout. All values are now guaranteed to + # end in "s" (the helper always returns `s`), so an O(1) integer compare on the prefix is safe. timeout_seconds = int(timeout[:-1]) if int(cache_refresh_rate[:-1]) < timeout_seconds: cache_refresh_rate = timeout - if int(cache_lock_timeout[:-1]) < timeout_seconds: - cache_lock_timeout = timeout - if int(cache_lock_age[:-1]) < timeout_seconds: - cache_lock_age = timeout ssl, extra_headers, sslcerts, proxy_set_range_header = "", "", "", "" serverport = port + 1 + # Internal loopback port for the Tier-2 cold-MISS fetch server. Tier 1 (public, + # $port) proxies cache misses to localhost:$fetchport, which applies the + # fail-fast `limit_conn` and then proxies on to the gevent app on $serverport. + fetchport = serverport + 1 extra_cache_keys = [] if (certificate and not private_key) or (private_key and not certificate): raise DemistoException("If using HTTPS connection, both certificate and private key should be provided.") @@ -173,19 +723,31 @@ def create_nginx_server_conf(file_path: str, port: int, params: dict): extra_cache_keys_str = "".join(extra_cache_keys) server_conf = Template(template_str).safe_substitute( + log_format=NGINX_LOG_FORMAT, port=port, serverport=serverport, + fetchport=fetchport, ssl=ssl, sslcerts=sslcerts, extra_cache_key=extra_cache_keys_str, proxy_set_range_header=proxy_set_range_header, timeout=timeout, cache_refresh_rate=cache_refresh_rate, - cache_lock_timeout=cache_lock_timeout, - cache_lock_age=cache_lock_age, cache_404_ttl=cache_404_ttl, cache_default_ttl=cache_default_ttl, extra_headers=extra_headers, + access_log_path=NGINX_SERVER_ACCESS_LOG, + error_log_path=NGINX_SERVER_ERROR_LOG, + ) + # Log the effective cache / timeout settings so each (re)start records exactly + # which values are active - essential for interpreting cache=HIT/STALE/UPDATING + # decisions and the upstream timing fields in the access logs. Kept at debug. + demisto.debug( + "edl: nginx effective settings -> " + f"listen_port={port} upstream_port={serverport} fetch_tier_port={fetchport} ssl={'on' if ssl else 'off'} " + f"timeout={timeout} cache_refresh_rate={cache_refresh_rate} " + f"cache_404_ttl={cache_404_ttl} cache_default_ttl={cache_default_ttl} " + f"extra_cache_keys=[{extra_cache_keys_str}]" ) with open(file_path, mode="w+") as f: f.write(server_conf) @@ -205,7 +767,9 @@ def start_nginx_server(port: int, params: dict = {}) -> subprocess.Popen: nginx_test_command.extend(directive_args) test_output = subprocess.check_output(nginx_test_command, stderr=subprocess.STDOUT, text=True) demisto.info(f"ngnix test passed. command: [{nginx_test_command}]") - demisto.debug(f"nginx test ouput:\n{test_output}") + # Promote the fully rendered config to info so the active log_format, cache, + # timeout and proxy_set_header directives are recorded on every (re)start. + demisto.info(f"nginx effective rendered config (nginx -T):\n{test_output}") except subprocess.CalledProcessError as err: raise ValueError(f"Failed testing nginx conf. Return code: {err.returncode}. Output: {err.output}") nginx_command = ["nginx"] @@ -251,7 +815,7 @@ def nginx_log_process(nginx_process: subprocess.Popen): start = 1 for lines in batch(f.readlines(), 100): end = start + len(lines) - demisto.info(f"nginx access log ({start}-{end-1}): " + "".join(lines)) + demisto.debug(f"nginx access log ({start}-{end-1}): " + "".join(lines)) start = end Path(old_access).unlink() if log_error: @@ -522,27 +1086,32 @@ def run_long_running(params: dict = None, is_test: bool = False): log_handler.setFormatter(logging.Formatter("flask log: [%(asctime)s] %(levelname)s in %(module)s: %(message)s")) APP.logger.addHandler(log_handler) # type: ignore[name-defined] # pylint: disable=E0602 demisto.debug("done setting demisto handler for logging") - server = WSGIServer( - ("0.0.0.0", server_port), - APP, # type: ignore[name-defined] # pylint: disable=E0602 - log=DEMISTO_LOGGER, # type: ignore[name-defined] # pylint: disable=E0602 - error_log=ERROR_LOGGER, - ) + demisto.info(f"edl: starting server on 0.0.0.0:{server_port}; nginx proxy on port {nginx_port}.") + if is_test: test_nginx_server(nginx_port, params) - server_process = Process(target=server.serve_forever) - server_process.start() + server = WSGIServer( + ("0.0.0.0", server_port), + APP, # type: ignore[name-defined] # pylint: disable=E0602 + log=DEMISTO_LOGGER, + error_log=ERROR_LOGGER, + ) + server.start() time.sleep(5) - try: - server_process.terminate() - server_process.join(1.0) - except Exception as ex: - demisto.error(f"failed stopping test wsgi server process: {ex}") + server.stop() else: nginx_process = start_nginx_server(nginx_port, params) test_nginx_web_server(nginx_port, params) nginx_log_monitor = gevent.spawn(nginx_log_monitor_loop, nginx_process) + wsgi_app = RequestLoggingMiddleware(APP) # type: ignore[name-defined] # pylint: disable=E0602 + server = WSGIServer( + ("0.0.0.0", server_port), + wsgi_app, + log=DEMISTO_LOGGER, # type: ignore[name-defined] # pylint: disable=E0602 + error_log=ERROR_LOGGER, + handler_class=DemistoWSGIHandler, + ) demisto.updateModuleHealth("") server.serve_forever() except Exception as e: diff --git a/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule_test.py b/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule_test.py index 342f389963f9..5b3afbb382e9 100644 --- a/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule_test.py +++ b/Packs/ApiModules/Scripts/NGINXApiModule/NGINXApiModule_test.py @@ -309,12 +309,20 @@ def test_nginx_log_process(nginx_cleanup, mocker: MockerFixture): sleep(0.5) # give nginx time to start # create a request to get a log line requests.get("http://localhost:12345/nginx-test?unit_testing") - sleep(0.2) - mocker.patch.object(demisto, "info") + # Poll until nginx has flushed the access-log line to the (test-redirected) + # log before invoking nginx_log_process. A fixed short sleep is racy: if the + # line isn't on disk yet, nginx_log_process sees an empty log, logs nothing, + # and demisto.debug.call_args is None. + for _ in range(50): + if Path(module.NGINX_SERVER_ACCESS_LOG).exists() and Path(module.NGINX_SERVER_ACCESS_LOG).stat().st_size: + break + sleep(0.1) + mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") module.nginx_log_process(NGINX_PROCESS) + # The nginx access log line is emitted via demisto.debug. # call_args is tuple (args list, kwargs). we only need the args - arg = demisto.info.call_args[0][0] + arg = demisto.debug.call_args[0][0] assert "nginx access log" in arg assert "unit_testing" in arg # make sure old file was removed @@ -559,12 +567,16 @@ def test_normalize_nginx_time_invalid_raises(value): def test_create_nginx_server_conf_renders_seconds_for_all_five_params(tmp_path: Path, mocker): """ Given: a params dict that mixes nginx-native, human-readable and bare-integer - forms across all five cache_* params (plus a "timeout" of 3600s that - should floor-bump the lock + refresh values). + forms across the cache_* time params (plus a "timeout" of 3600s that + should floor-bump cache_refresh_rate). When: create_nginx_server_conf renders the nginx server config. - Then: every rendered cache directive is in the safe "s" form, and no + Then: every rendered cache-time directive is in the safe "s" form, and no occurrence of any human-readable unit word or original input token survives into the rendered conf. + + Note: the two-tier design NO LONGER emits proxy_cache_lock_timeout / + proxy_cache_lock_age (concurrency is controlled by Tier-2 limit_conn, + not by a cache lock), so those directives must be ABSENT. """ from NGINXApiModule import create_nginx_server_conf @@ -576,8 +588,6 @@ def test_create_nginx_server_conf_renders_seconds_for_all_five_params(tmp_path: params={ "timeout": "3600", "cache_refresh_rate": "12 hours", - "cache_lock_timeout": "1h", - "cache_lock_age": "30m", "cache_404_ttl": "5 minutes", "cache_default_ttl": "300", }, @@ -587,22 +597,110 @@ def test_create_nginx_server_conf_renders_seconds_for_all_five_params(tmp_path: # cache_refresh_rate = "12 hours" -> 43200s (>= timeout, not floor-bumped) assert "proxy_cache_valid 200 301 302 43200s;" in conf - # cache_lock_timeout = "1h" = 3600s == timeout (not bumped, already at floor) - assert "proxy_cache_lock_timeout 3600s;" in conf - # cache_lock_age = "30m" = 1800s < timeout (3600s) -> floor-bumped to "3600s" - assert "proxy_cache_lock_age 3600s;" in conf # cache_404_ttl = "5 minutes" -> 300s assert "proxy_cache_valid 404 300s;" in conf # cache_default_ttl = "300" -> 300s assert "proxy_cache_valid any 300s;" in conf + # The cache-lock tuning directives are no longer part of the two-tier design. + assert "proxy_cache_lock_timeout" not in conf + assert "proxy_cache_lock_age" not in conf + # Hardening: none of the original unit words / non-normalized tokens may # survive into the rendered conf — those are exactly the failure surfaces # that crashed `nginx -T` in the documented incident. - for forbidden in ("hours", "minutes", "12h", "30m", "5 minutes"): + for forbidden in ("hours", "minutes", "12h", "5 minutes"): assert forbidden not in conf, f"Rendered conf must not contain non-normalized substring {forbidden!r}: {conf}" +def test_create_nginx_server_conf_renders_two_tier_fail_fast(tmp_path: Path, mocker): + """ + Given: a default params dict. + When: create_nginx_server_conf renders the nginx server config. + Then: the rendered conf implements the TWO-TIER fail-fast design: + * a per-URI limit_conn zone keyed on $request_uri, + * Tier 2 rejects concurrent same-URI cold-miss fetches (limit_conn 1 + + limit_conn_status 429) and does NOT cache, + * Tier 1 turns OFF the inherited cache lock so misses fall through to + Tier 2 instead of queueing. + """ + from NGINXApiModule import create_nginx_server_conf + + mocker.patch.object(demisto, "callingContext", return_value={"context": {}}) + conf_file = str(tmp_path / "nginx-test-server.conf") + create_nginx_server_conf(conf_file, 12345, params={}) + with open(conf_file) as f: + conf = f.read() + + # Per-URI concurrency zone (keyed on the resource, not the client). + assert "limit_conn_zone $request_uri zone=concurrent_conn_zone:1m;" in conf + # Tier 2 fail-fast: one build per URI, extras rejected with 429, no caching. + assert "limit_conn concurrent_conn_zone 1;" in conf + assert "limit_conn_status 429;" in conf + assert "proxy_cache off;" in conf + # Tier 1 must DISABLE the inherited cache lock so cold misses reach Tier 2 + # (the fail-fast limiter) instead of queueing behind proxy_cache_lock. + assert "proxy_cache_lock off;" in conf + # The queueing lock must NOT be enabled anywhere as a directive. + assert "proxy_cache_lock on;" not in conf + + +def test_create_nginx_server_conf_never_caches_transient_statuses(tmp_path: Path, mocker): + """ + Given: a default params dict. + When: create_nginx_server_conf renders the nginx server config. + Then: transient responses are excluded from caching so a slow/failed build + cannot poison the URI: the fail-fast 429 and the upstream 5xx/504 + family both get "0s" validity (overriding the `any` catch-all), while + proxy_cache_use_stale + background_update still allow serving a GOOD + stale copy on timeout. + """ + from NGINXApiModule import create_nginx_server_conf + + mocker.patch.object(demisto, "callingContext", return_value={"context": {}}) + conf_file = str(tmp_path / "nginx-test-server.conf") + create_nginx_server_conf(conf_file, 12345, params={}) + with open(conf_file) as f: + conf = f.read() + + # Never cache the fail-fast rejection or upstream errors/timeouts. + assert "proxy_cache_valid 429 0s;" in conf + assert "proxy_cache_valid 500 502 503 504 0s;" in conf + # Stale-serving of a previously-cached GOOD copy is still enabled. + assert "proxy_cache_use_stale" in conf + assert "proxy_cache_background_update on;" in conf + + +def test_create_nginx_server_conf_two_server_blocks_and_ports(tmp_path: Path, mocker): + """ + Given: a listening port of 12345. + When: create_nginx_server_conf renders the nginx server config. + Then: two server blocks are emitted — Tier 1 public on the given port, and + Tier 2 internal on loopback at fetchport (= serverport + 1 = port + 2) + — and Tier 2 proxies on to the gevent app at serverport (= port + 1). + """ + from NGINXApiModule import create_nginx_server_conf + + mocker.patch.object(demisto, "callingContext", return_value={"context": {}}) + conf_file = str(tmp_path / "nginx-test-server.conf") + port = 12345 + create_nginx_server_conf(conf_file, port, params={}) + with open(conf_file) as f: + conf = f.read() + + serverport = port + 1 # gevent app + fetchport = serverport + 1 # Tier-2 internal fetch server + + # Exactly two `server {` blocks (Tier 1 + Tier 2). + assert conf.count("server {") == 2 + # Tier 1 listens on the public port; Tier 2 on loopback:fetchport. + assert f"listen {port} default_server" in conf + assert f"listen localhost:{fetchport};" in conf + # Tier 1 proxies misses to Tier 2; Tier 2 proxies to the gevent app. + assert f"proxy_pass http://localhost:{fetchport}/;" in conf + assert f"proxy_pass http://localhost:{serverport}/;" in conf + + def test_create_nginx_server_conf_rejects_invalid_param(tmp_path: Path, mocker, monkeypatch): """ Given: a params dict with an unparseable value for cache_404_ttl. diff --git a/Packs/ApiModules/pack_metadata.json b/Packs/ApiModules/pack_metadata.json index b8dafe5495c4..e2ce09bcfe8f 100644 --- a/Packs/ApiModules/pack_metadata.json +++ b/Packs/ApiModules/pack_metadata.json @@ -2,7 +2,7 @@ "name": "ApiModules", "description": "API Modules", "support": "xsoar", - "currentVersion": "2.4.35", + "currentVersion": "2.4.36", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.py b/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.py index 1a33ac92d903..73f98c7340a0 100644 --- a/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.py +++ b/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.py @@ -792,7 +792,7 @@ def on_page(page: list[dict]) -> None: running_state["last_event_time"] = latest_time add_time_to_events(new_events, dataset) - product = f"{PRODUCT}_{event_type.type}" if event_type.type != EVENT_TYPE_ALERTS else PRODUCT + product = f"{PRODUCT}_{dataset}" if event_type.type != EVENT_TYPE_ALERTS else PRODUCT send_start = time.monotonic() send_events_to_xsiam(new_events, vendor=VENDOR, product=product) @@ -1253,6 +1253,13 @@ def fetch_events( if "Devices" in event_types_to_fetch and not should_run_device_fetch(last_run, device_fetch_interval, datetime.now()): safe_debug("Skipping Devices fetch - interval not reached") event_types_to_fetch.remove("Devices") + for device_state_key in ( + DEVICES_LAST_FETCH, + f"{EVENT_TYPE_DEVICES}_last_fetch_ids", + f"{EVENT_TYPE_DEVICES}_last_fetch_next_field", + ): + if device_state_key in last_run: + next_run[device_state_key] = last_run[device_state_key] safe_debug(f"Event types after filtering: {event_types_to_fetch}") @@ -1463,7 +1470,7 @@ def handle_fetched_events(events: dict[str, list[dict[str, Any]]], next_run: dic demisto.setLastRun(next_run) -def events_to_command_results(events: dict[str, list], event_type) -> CommandResults: +def events_to_command_results(events: dict[str, list], event_type: str) -> CommandResults: """Return a CommandResults object with a table of fetched events. Args: diff --git a/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector_test.py b/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector_test.py index 619c8edec74c..f50f0ac98cf3 100644 --- a/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector_test.py +++ b/Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector_test.py @@ -510,6 +510,43 @@ def test_handle_from_date_argument(self): from_date_datetime = handle_from_date_argument("2023-01-01T01:00:00") assert from_date_datetime == datetime(2023, 1, 1, 1, 0, 0) + @freeze_time("2023-01-01T01:00:00") + def test_devices_only_skip_preserves_state(self, mocker, dummy_client): + """ + Given: + - A Devices-only instance whose device-fetch interval has NOT elapsed + (last fetch 1 minute ago, interval 1 hour). + When: + - fetch_events runs and skips the device fetch. + Then: + - The returned next_run is NOT empty and retains devices_last_fetch_time, so the + persisted lastRun does not get wiped. + """ + from ArmisEventCollector import fetch_events + + fetch_start_time = arg_to_datetime("2023-01-01T01:00:00") + # Frozen now is 01:00:00; last_fetch 1 minute earlier -> interval (1h) not reached -> skip. + recent = "2023-01-01T00:59:00" + last_run = {"devices_last_fetch_time": recent, "devices_last_fetch_ids": ["dev1"], "devices_last_fetch_next_field": 0} + device_fetch_interval = timedelta(hours=1) + + events, next_run = fetch_events( + client=dummy_client, + max_fetch=1000, + devices_max_fetch=1000, + last_run=last_run, + fetch_start_time=fetch_start_time, + event_types_to_fetch=["Devices"], + device_fetch_interval=device_fetch_interval, + use_multithreading=False, + context_manager=None, + ) + + assert events == {} + assert next_run.get("devices_last_fetch_time") == recent + assert next_run.get("devices_last_fetch_ids") == ["dev1"] + assert next_run.get("devices_last_fetch_next_field") == 0 + class TestFetchFlow: fetch_start_time = arg_to_datetime("2023-01-01T01:00:00") @@ -1667,6 +1704,24 @@ def test_devices_product_routing(self, mocker): assert mock_send.call_args.kwargs["product"] == f"{PRODUCT}_devices" + def test_activities_product_routing(self, mocker): + """ + Given: The Activities event type (whose ``type`` is singular "activity" but whose + ``dataset_name`` is plural "activities"). + When: Callback ships a page. + Then: send_events_to_xsiam is called with product == "_activities" + (plural), so events land in ``armis_security_activities_raw``. + """ + from ArmisEventCollector import PRODUCT + + mock_send = mocker.patch("ArmisEventCollector.send_events_to_xsiam") + state = self._running_state() + on_page = _stream_page_to_xsiam(EVENT_TYPES["Activities"], state) + + on_page([{"activityUUID": "1", "time": "2023-01-01T01:00:10.000000+00:00"}]) + + assert mock_send.call_args.kwargs["product"] == f"{PRODUCT}_activities" + class TestFetchByAqlQueryWithCallback: """Tests for fetch_by_aql_query in streaming mode (on_page provided).""" diff --git a/Packs/Armis/ReleaseNotes/1_3_1.md b/Packs/Armis/ReleaseNotes/1_3_1.md new file mode 100644 index 000000000000..8f3d70dde27a --- /dev/null +++ b/Packs/Armis/ReleaseNotes/1_3_1.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Armis Event Collector + +- Fixed an issue where *Activities* events were ingested into the *armis_security_activity_raw* dataset instead of the *armis_security_activities_raw* dataset. +- Fixed an issue where an instance configured to fetch only *Devices* stopped ingesting device events, because the device fetch state was not preserved between fetch cycles. diff --git a/Packs/Armis/pack_metadata.json b/Packs/Armis/pack_metadata.json index c43154e19bcf..cae3a9a170a6 100644 --- a/Packs/Armis/pack_metadata.json +++ b/Packs/Armis/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Armis", "description": "Agentless and passive security platform that sees, identifies, and classifies every device, tracks behavior, identifies threats, and takes action automatically to protect critical information and systems", "support": "partner", - "currentVersion": "1.3.0", + "currentVersion": "1.3.1", "author": "Armis Corporation", "url": "https://support.armis.com/", "email": "support@armis.com", @@ -12,7 +12,9 @@ "tags": [], "created": "2021-01-02T18:00:53Z", "useCases": [], - "keywords": [], + "keywords": [ + "Armis" + ], "dependencies": {}, "marketplaces": [ "xsoar", diff --git a/Packs/Azure/.secrets-ignore b/Packs/Azure/.secrets-ignore index 5c74a8faaf7a..963e04621acf 100644 --- a/Packs/Azure/.secrets-ignore +++ b/Packs/Azure/.secrets-ignore @@ -1,4 +1,5 @@ Microsoft orLo vm_response -https://corpvault.vault.azure.net \ No newline at end of file +https://corpvault.vault.azure.net +https://portal.azure.com \ No newline at end of file diff --git a/Packs/Azure/Integrations/Azure/Azure.py b/Packs/Azure/Integrations/Azure/Azure.py index f018180f1940..a7819e1feaba 100644 --- a/Packs/Azure/Integrations/Azure/Azure.py +++ b/Packs/Azure/Integrations/Azure/Azure.py @@ -7,6 +7,7 @@ from requests.exceptions import ConnectionError, Timeout import datetime as dt import defusedxml.ElementTree as defused_ET +import urllib.parse from urllib.parse import parse_qs, urlparse, urlencode, urlunparse from datetime import UTC @@ -37,6 +38,7 @@ DEFAULT_RESOURCE = "https://management.azure.com/" STORAGE_SCOPE = "https://storage.azure.com/.default" STORAGE_RESOURCE = "https://storage.azure.com/" +DEFAULT_AZURE_AD_ENDPOINT = "https://login.microsoftonline.com" PERMISSIONS_TO_COMMANDS = { "Microsoft.Network/networkSecurityGroups/read": ["azure-nsg-security-groups-list", "azure-vn-security-groups-list"], @@ -603,20 +605,71 @@ def __init__( resource: str | None = None, scope: str | None = None, headers: dict | None = {}, + connection_type: str = "Client Credentials", + azure_ad_endpoint: str = "https://login.microsoftonline.com", + auth_code: str | None = None, + redirect_uri: str | None = None, + managed_identities_client_id: str | None = None, ): + self.connection_type = connection_type if not headers: + is_device_code = "Device Code" in connection_type + # Managed Identities authenticate against the IMDS endpoint, not an AAD token endpoint, + # so they must not receive a token_retrieval_url. + is_managed_identities = "Managed Identities" in connection_type + token_retrieval_url: str | None + ms_scope: str | None + ms_resource: str | None + if is_device_code: + # Use the configured tenant when available so single-tenant apps get a tenant-scoped + # authority. Fall back to the "organizations" endpoint only for multi-tenant apps with + # no configured tenant; otherwise Microsoft returns "No tenant-identifying information + # found in either the request or implied by any provided credentials". + tenant_segment = tenant_id or "organizations" + token_retrieval_url = urllib.parse.urljoin(azure_ad_endpoint, f"{tenant_segment}/oauth2/v2.0/token") + # Use the per-command resource (management or storage) as the single source of truth, + # then derive the matching Device Code delegated scope from it (space-delimited form, + # unlike the `.default` form used by the other flows). This keeps storage-container + # commands storage-scoped under Device Code auth instead of always management-scoped. + ms_resource = (resource or DEFAULT_RESOURCE).rstrip("/") + ms_scope = f"{ms_resource}/user_impersonation offline_access user.read" + else: + # Build the token authority from the configured Azure AD endpoint so that Client Credentials + # and Authorization Code flows target the correct cloud (e.g. US Gov login.microsoftonline.us). + # Without this, MicrosoftClient falls back to the commercial login.microsoftonline.com authority + # while the scope/resource point to a national cloud, which Microsoft rejects with + # "Confidential Client is not supported in Cross Cloud request". + token_retrieval_url = ( + None + if is_managed_identities + else urllib.parse.urljoin(azure_ad_endpoint, f"{tenant_id or ''}/oauth2/v2.0/token") + ) + ms_scope = scope + # The Client Credentials and Authorization Code flows use the v2.0 token endpoint, which + # authorizes via the `.default` scope and rejects the v1.0 `resource` parameter. Sending both + # results in "invalid_target: The resource parameter provided in the request doesn't match with + # the requested scopes". Only Device Code (v1.0-style) uses `resource`. + ms_resource = None ms_client_args = assign_params( self_deployed=True, auth_id=app_id, - token_retrieval_url=None, - grant_type=GRANT_BY_CONNECTION.get("Client Credentials"), + token_retrieval_url=token_retrieval_url, + grant_type=GRANT_BY_CONNECTION.get(connection_type), base_url=f"{PREFIX_URL_AZURE}", verify=verify, proxy=proxy, - resource=resource, - scope=scope, + resource=ms_resource, + scope=ms_scope, + azure_ad_endpoint=azure_ad_endpoint, tenant_id=tenant_id, enc_key=enc_key, + auth_code=auth_code, + redirect_uri=redirect_uri, + managed_identities_client_id=managed_identities_client_id, + managed_identities_resource_uri=(resource or DEFAULT_RESOURCE).rstrip("/") + if is_managed_identities + else (ms_resource or Resources.management_azure), + command_prefix="azure", ok_codes=(200, 201, 202, 204), ) self.ms_client = MicrosoftClient(**ms_client_args) @@ -5353,26 +5406,110 @@ def parse_forecast_table_to_dict(response: dict) -> list[dict]: def test_module(client: AzureClient) -> str: - """Tests API connectivity and authentication' + """Tests API connectivity and authentication. + Returning 'ok' indicates that the integration works like it is supposed to. - Connection to the service is successful. Raises exceptions if something goes wrong. + :type AzureClient: ``Client`` :param Client: client to use :return: 'ok' if test passed. :rtype: ``str`` """ - try: - client.http_request( - method="GET", - full_url=f"{PREFIX_URL_AZURE}{client.subscription_id}/providers/Microsoft.Authorization/roleAssignments", - params={"api-version": PERMISSIONS_VERSION}, + if "Device Code" in client.connection_type: + raise DemistoException( + "When using the Device Code authentication type, the Test button cannot validate the " + "connection. Save the instance, run the `!azure-auth-start` and `!azure-auth-complete` " + "commands to log in, and then run the `!azure-auth-test` command to validate the connection. " + "For more details, see the Detailed Instructions (?) Help section in the integration configuration." ) + try: + if "Azure Managed Identities" in client.connection_type: + # The roleAssignments call requires the Microsoft.Authorization/roleAssignments/read + # permission, which a Managed Identity often lacks even when it can run other commands. + # Validate the connection with a lightweight resource-groups list call, which only requires + # the baseline Microsoft.Resources/subscriptions/resourceGroups/read permission. + client.http_request( + method="GET", + full_url=f"{PREFIX_URL_AZURE}{client.subscription_id}/resourcegroups", + params={"$top": "1"}, + ) + else: + client.http_request( + method="GET", + full_url=f"{PREFIX_URL_AZURE}{client.subscription_id}/providers/Microsoft.Authorization/roleAssignments", + params={"api-version": PERMISSIONS_VERSION}, + ) except (ConnectionError, Timeout) as conn_err: raise Exception("Connectivity Error: Cannot reach Azure endpoint") from conn_err return "ok" +def _get_ms_client(client: AzureClient) -> "MicrosoftClient": + """Return the MicrosoftClient used for the marketplace auth flows. + + The MicrosoftClient only exists on the Cortex XSOAR / Cortex XSIAM (marketplace) path. On the + Cortex Platform (COOC) path authentication is handled automatically via the cloud connector, so + the auth helper commands are not applicable there. + + Raises: + DemistoException: If called on the Cortex Platform path (no MicrosoftClient available). + """ + ms_client = getattr(client, "ms_client", None) + if ms_client is None: + raise DemistoException( + "This command is supported only on Cortex XSOAR and Cortex XSIAM. On the Cortex Platform, " + "authentication is handled automatically and does not require these auth commands." + ) + return ms_client + + +def test_connection(client: AzureClient) -> str: + """Validate the Azure connection by requesting an access token (marketplace flows). + + Args: + client (AzureClient): The Azure client used to obtain the underlying Microsoft client. + + Returns: + str: "Success!" if an access token was retrieved successfully. + + Raises: + Exception: Propagated from MicrosoftApiModule if authentication fails. + """ + _get_ms_client(client).get_access_token() # If fails, MicrosoftApiModule raises an error + return "Success!" + + +def start_auth(client: AzureClient) -> CommandResults: + """Start the interactive (Device Code) authorization process (marketplace flows). + + Args: + client (AzureClient): The Azure client used to obtain the underlying Microsoft client. + + Returns: + CommandResults: A result containing the device-code authorization instructions for the user, + who must then run the `!azure-auth-complete` command to finish the flow. + """ + result = _get_ms_client(client).start_auth("!azure-auth-complete") + return CommandResults(readable_output=result) + + +def complete_auth(client: AzureClient) -> str: + """Complete the interactive (Device Code) authorization process (marketplace flows). + + Args: + client (AzureClient): The Azure client used to obtain the underlying Microsoft client. + + Returns: + str: A confirmation message indicating the authorization completed successfully. + + Raises: + Exception: Propagated from MicrosoftApiModule if the access token cannot be retrieved. + """ + _get_ms_client(client).get_access_token() + return "Authorization completed successfully." + + def health_check(shared_creds: dict, subscription_id: str, connector_id: str) -> HealthCheckError | None: # pragma: no cover """ Tests connectivity to Azure and checks for required permissions. @@ -5418,13 +5555,91 @@ def health_check(shared_creds: dict, subscription_id: str, connector_id: str) -> return None -def get_azure_client(params: dict, args: dict, command: str): +# Auth-helper commands establish or reset authentication and therefore must not require the +# Authorization code, which is only obtained by first running azure-generate-login-url. Requiring +# it here would create a chicken-and-egg block on the command whose purpose is to produce the code. +AUTH_HELPER_COMMANDS = { + "azure-generate-login-url", + "azure-auth-start", + "azure-auth-complete", + "azure-auth-reset", + "azure-auth-test", +} + + +def validate_auth_params(params: dict, connection_type: str, command: str = "") -> None: + """Validate that all mandatory parameters for the selected authentication type are configured. + + This runs only on the Cortex XSOAR / Cortex XSIAM (marketplace) path. + + The mandatory parameters per authentication type are: + - Client Credentials: Application ID, Tenant ID, Client Secret, Default Subscription ID + - Device Code: Application ID, Default Subscription ID + - Authorization Code: Application ID, Application redirect URI, Authorization code, + Default Subscription ID + - Azure Managed Identities: Azure Managed Identities Client ID, Default Subscription ID + + The Authorization code is not required for auth-helper commands (e.g. azure-generate-login-url), + since those commands are used to obtain/establish authentication before a code exists. + + Raises: + DemistoException: If one or more mandatory parameters for the selected auth type are missing. + """ + app_id = params.get("app_id") + subscription_id = params.get("subscription_id") + tenant_id = params.get("tenant_id") + client_secret = (params.get("credentials") or {}).get("password") + auth_code = (params.get("auth_code") or {}).get("password") + redirect_uri = params.get("redirect_uri") + managed_identities_client_id = get_azure_managed_identities_client_id(params) + + authorization_code_required: dict[str, Any] = { + "Application ID": app_id, + "Application redirect URI": redirect_uri, + "Default Subscription ID": subscription_id, + } + # Only demand the Authorization code itself when the caller is an actual data command, not an + # auth-helper command such as azure-generate-login-url that is run to obtain the code. + if command not in AUTH_HELPER_COMMANDS: + authorization_code_required["Authorization code"] = auth_code + + required_by_auth_type: dict[str, dict[str, Any]] = { + "Client Credentials": { + "Application ID": app_id, + "Tenant ID": tenant_id, + "Client Secret": client_secret, + "Default Subscription ID": subscription_id, + }, + "Device Code": { + "Application ID": app_id, + "Default Subscription ID": subscription_id, + }, + "Authorization Code": authorization_code_required, + "Azure Managed Identities": { + "Azure Managed Identities Client ID": managed_identities_client_id, + "Default Subscription ID": subscription_id, + }, + } + + required_params = required_by_auth_type.get(connection_type, {}) + missing = [display_name for display_name, value in required_params.items() if not value] + if missing: + raise DemistoException( + f"Missing required parameter(s) for the '{connection_type}' authentication type: " + f"{', '.join(missing)}. Configure these in the integration instance and try again." + ) + + +def get_azure_client(params: dict, args: dict, command: str, azure_ad_endpoint: str = DEFAULT_AZURE_AD_ENDPOINT): headers = {} client_scope, token_scopes = get_command_and_token_scopes(command) - demisto.debug(f"Got {client_scope=} and {token_scopes=}") resource = get_command_resource(command) - demisto.debug(f"Got {resource=}") - if not params.get("credentials", {}).get("password"): + connection_type = params.get("auth_type") or "Client Credentials" + # The Cortex Platform (COOC) path is selected when a connector ID is present. There, the CTS + # token flow (get_cloud_credentials -> demisto._platformAPICall) handles authentication. + # On Cortex XSOAR / Cortex XSIAM (version < 3.0) there is no connector, so authentication is done + # by MicrosoftClient using the configured auth_type. + if get_connector_id(): credentials = get_cloud_credentials( CloudTypes.AZURE.value, get_from_args_or_params(params=params, args=args, key="subscription_id"), @@ -5435,6 +5650,10 @@ def get_azure_client(params: dict, args: dict, command: str): raise DemistoException("Failed to retrieve AZURE access token - token is missing from credentials") headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json"} demisto.debug("Using CTS.") + else: + # Marketplace path: ensure all mandatory parameters for the selected auth type are configured + # before attempting any API call. + validate_auth_params(params, connection_type, command) client = AzureClient( app_id=params.get("app_id", ""), subscription_id=params.get("subscription_id", ""), @@ -5442,10 +5661,15 @@ def get_azure_client(params: dict, args: dict, command: str): verify=not params.get("insecure", False), proxy=params.get("proxy", False), tenant_id=params.get("tenant_id"), - enc_key=params.get("credentials", {}).get("password"), + enc_key=(params.get("credentials") or {}).get("password"), resource=resource, scope=client_scope, headers=headers, + connection_type=connection_type, + azure_ad_endpoint=azure_ad_endpoint, + auth_code=(params.get("auth_code", {}) or {}).get("password"), + redirect_uri=params.get("redirect_uri"), + managed_identities_client_id=get_azure_managed_identities_client_id(params), ) return client @@ -5624,18 +5848,39 @@ def main(): # pragma: no cover "azure-postgres-config-set-statement-logging-quick-action": set_postgres_config_command, "azure-postgres-server-update-ssl-enforcement-quick-action": postgres_server_update_command, } - if command == "test-module" and connector_id: - if is_gov_account(connector_id): # type: ignore + + azure_ad_endpoint = params.get("azure_ad_endpoint") or DEFAULT_AZURE_AD_ENDPOINT + + if connector_id: + if command == "test-module": + if is_gov_account(connector_id): # type: ignore + switch_to_gov_account() + demisto.debug(f"Running health check for connector ID: {connector_id}") + return return_results(run_health_check_for_accounts(connector_id, CloudTypes.AZURE.value, health_check)) + + account_id = get_from_args_or_params(params=params, args=args, key="subscription_id") + if is_gov_account(connector_id, account_id): # type: ignore switch_to_gov_account() - demisto.debug(f"Running health check for connector ID: {connector_id}") - return return_results(run_health_check_for_accounts(connector_id, CloudTypes.AZURE.value, health_check)) + else: + if azure_ad_endpoint.rstrip("/") != DEFAULT_AZURE_AD_ENDPOINT: + demisto.debug(f"Non-commercial Azure AD endpoint configured ({azure_ad_endpoint}); switching to Gov account.") + switch_to_gov_account() + + if command == "azure-auth-reset": + return return_results(reset_auth()) + + client = get_azure_client(params, args, command, azure_ad_endpoint) - account_id = get_from_args_or_params(params=params, args=args, key="subscription_id") - if is_gov_account(connector_id, account_id): # type: ignore - switch_to_gov_account() - client = get_azure_client(params, args, command) if command == "test-module": return_results(test_module(client)) + elif command == "azure-auth-start": + return_results(start_auth(client)) + elif command == "azure-auth-complete": + return_results(complete_auth(client)) + elif command == "azure-auth-test": + return_results(test_connection(client)) + elif command == "azure-generate-login-url": + return_results(generate_login_url(_get_ms_client(client), azure_ad_endpoint)) elif command in commands_with_params_and_args: return_results(commands_with_params_and_args[command](client=client, params=params, args=args)) else: diff --git a/Packs/Azure/Integrations/Azure/Azure.yml b/Packs/Azure/Integrations/Azure/Azure.yml index 4802fe978bb3..676a04e2a5fa 100644 --- a/Packs/Azure/Integrations/Azure/Azure.yml +++ b/Packs/Azure/Integrations/Azure/Azure.yml @@ -15,6 +15,8 @@ configuration: name: subscription_id type: 0 section: Connect + required: true + required:platform: false additionalinfo: You can set the value either in the configuration or directly within the commands. If you set it in both places, the value in the command will override the configuration setting. - display: Default Resource Group Name name: resource_group_name @@ -34,6 +36,18 @@ configuration: section: Connect advanced: true required: false +- name: auth_type + display: Authentication Type + defaultvalue: Client Credentials + type: 15 + additionalinfo: The type of authentication. Can be Client Credentials (default authentication type), Device Code, Authorization Code, or Azure Managed Identities. + options: + - Client Credentials + - Device Code + - Authorization Code + - Azure Managed Identities + section: Connect + required: false - display: Trust any certificate (not secure) name: insecure type: 8 @@ -50,28 +64,77 @@ configuration: display: Tenant ID defaultvalue: type: 0 - additionalinfo: "" + additionalinfo: The tenant ID of the Azure app registration. section: Connect required: false - name: credentials display: Client Secret defaultvalue: type: 9 - additionalinfo: "" + additionalinfo: The client secret of the Azure app registration. Required when using the Client Credentials flow. displaypassword: Client Secret hiddenusername: true section: Connect required: false +- name: redirect_uri + display: Application redirect URI + defaultvalue: + type: 0 + additionalinfo: The application redirect URI for the Authorization Code flow. + section: Connect + advanced: true + required: false +- name: auth_code + display: Authorization code + defaultvalue: + type: 9 + additionalinfo: The authorization code for the Authorization Code flow, received from the authorization step. See the Detailed Instructions (?) Help section. + displaypassword: Authorization code + hiddenusername: true + section: Connect + advanced: true + required: false +- name: managed_identities_client_id + display: Azure Managed Identities Client ID + defaultvalue: + type: 9 + additionalinfo: The managed identities client ID for authentication. Relevant only when the integration is running on an Azure VM. + displaypassword: Azure Managed Identities Client ID + hiddenusername: true + section: Connect + advanced: true + required: false description: Cloud integrations are installed from the **Data Sources** page. To configure a cloud integration, go to Settings > Data Sources and click "Add Data Source", select Azure, then in Advanced Settings > Security Capabilities, enable "Automation". display: Azure name: Azure script: commands: + - name: azure-auth-start + description: Starts the authorization process for the Device Code / Authorization Code flows. Follow the instructions in the command results. + hidden: + - platform + - name: azure-auth-complete + description: Completes the authorization process for the Device Code flow. Should be used after running the azure-auth-start command. + hidden: + - platform + - name: azure-auth-test + description: Tests the connectivity to Azure. Relevant for the Device Code and Authorization Code authentication flows (used instead of the Test button for these interactive flows). + hidden: + - platform + - name: azure-auth-reset + description: Resets the authentication process. Relevant for the Device Code and Authorization Code authentication flows. + hidden: + - platform + - name: azure-generate-login-url + description: Generates the login URL used for authorization. Relevant only for the Authorization Code authentication flow. + hidden: + - platform - arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: The name of the resource group. name: resource_group_name type: String @@ -172,10 +235,11 @@ script: - name: azure-vn-security-rule-update description: "Update a security rule. If one does not exist, it will be created. Required permissions: Microsoft.Network/networkSecurityGroups/securityRules/read, Microsoft.Network/networkSecurityGroups/securityRules/write." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: The name of the resource group. name: resource_group_name type: String @@ -268,9 +332,10 @@ script: - description: The resource group name. name: resource_group_name required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the storage account. name: account_name required: true @@ -501,9 +566,10 @@ script: - description: The resource group name. name: resource_group_name required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the storage account. name: account_name required: true @@ -554,9 +620,10 @@ script: - description: The resource group name. name: resource_group_name required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the storage account. name: account_name required: true @@ -636,9 +703,10 @@ script: description: An array of predefined, supported blob types. The only supported value is BlockBlob. type: String arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group name. name: resource_group_name required: true @@ -648,9 +716,10 @@ script: - name: azure-storage-blob-service-property-get description: "Run this command to get the blob service properties of a specific account storage. Required permissions: Microsoft.Storage/storageAccounts/blobServices/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group name. name: resource_group_name required: true @@ -724,9 +793,10 @@ script: description: The hasLegalHold property is set to true by SRP if at least one legal hold tag exists, and false if all tags are cleared. type: Boolean arguments: - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -755,9 +825,10 @@ script: - name: azure-storage-blob-container-update description: "Updates container properties as specified in request body. Properties not mentioned in the request will be unchanged. Update fails if the specified container doesn't already exist. Required permissions: Microsoft.Storage/storageAccounts/blobServices/containers/write." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group name. name: resource_group_name required: true @@ -809,9 +880,10 @@ script: - description: The name of the Container. name: container_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -849,9 +921,10 @@ script: https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata name: container_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -865,9 +938,10 @@ script: - description: The name of the Container to delete. name: container_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -887,9 +961,10 @@ script: required: true - description: The name of the Blob to create. Default is the file name. name: blob_name - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -903,9 +978,10 @@ script: - name: azure-storage-blob-create description: "Create a new Blob under the specified Container. Required permissions: Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write." arguments: - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -928,9 +1004,10 @@ script: - description: The name of the Blob to retrieve. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -975,9 +1052,10 @@ script: - name: azure-storage-blob-get description: "Retrieve a Blob from a Container. Required permissions: Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read." arguments: - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1029,9 +1107,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1063,9 +1142,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1096,9 +1176,10 @@ script: - description: 'Tags fields in JSON format: {"tag-name-1": "tag-value-1", "tag-name-2": "tag-value-2"}. The tags fields may contain at most 10 tags.' name: tags required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1128,9 +1209,10 @@ script: - description: 'Tags fields in JSON format: {"tag-name-1": "tag-value-1", "tag-name-2": "tag-value-2"}. The tags fields may contain at most 10 tags.' name: tags required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1138,7 +1220,7 @@ script: name: account_name required: true - auto: PREDEFINED - description: Whether to append tags to the existing tags or not. Default is false. + description: Whether to append tags to the existing tags or not. name: append defaultValue: 'false' predefined: @@ -1152,9 +1234,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1204,9 +1287,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1282,9 +1366,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1334,9 +1419,10 @@ script: - description: The name of the blob. name: blob_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1379,9 +1465,10 @@ script: - description: Name of a container. name: container_name required: true - - description: "The subscription ID." + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: "The resource group name." name: resource_group_name required: true @@ -1396,8 +1483,9 @@ script: required: true description: Name of the policy assignment. - name: subscription_id - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: scope type: string description: The scope of the policy assignment. @@ -1480,9 +1568,10 @@ script: required: true description: The configuration setting name. - name: subscription_id - required: true + required: false + required:platform: true type: string - description: Subscription ID. + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: source type: string description: Source of the configuration. @@ -1500,8 +1589,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name type: string required: true @@ -1632,8 +1722,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name type: string required: true @@ -1755,8 +1846,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name type: string required: true @@ -1878,8 +1970,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name type: string required: true @@ -1996,8 +2089,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: server_name type: string required: true @@ -2024,8 +2118,9 @@ script: type: String - name: subscription_id type: string - required: true - description: subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: location description: Resource location. - name: retention_policy_days @@ -2081,8 +2176,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: disk_name type: string required: true @@ -2226,8 +2322,9 @@ script: description: The resource group name. - name: subscription_id type: string - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: disk_name type: string required: true @@ -2367,8 +2464,9 @@ script: description: The resource group name. - name: subscription_id type: String - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name type: String required: true @@ -2605,8 +2703,9 @@ script: required: true description: The resource group name. - name: subscription_id - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: name required: true description: Name of the Web App. @@ -2709,8 +2808,9 @@ script: description: The resource group name. - name: subscription_id type: String - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: registry_name type: String required: true @@ -2823,8 +2923,9 @@ script: description: The resource group name. - name: subscription_id type: String - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: registry_name type: String required: true @@ -2932,8 +3033,9 @@ script: description: The resource group name. - name: subscription_id type: String - required: true - description: Subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. - name: server_name type: String required: true @@ -2953,8 +3055,9 @@ script: name: vault_name required: true - name: subscription_id - required: true - description: The subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. type: String - name: resource_group_name required: true @@ -3054,8 +3157,9 @@ script: name: vault_name required: true - name: subscription_id - required: true - description: The subscription ID. + required: false + required:platform: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. type: String - name: resource_group_name required: true @@ -3141,9 +3245,10 @@ script: predefined: - 'true' - 'false' - - description: Subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the resource group. name: resource_group_name required: true @@ -3213,9 +3318,10 @@ script: predefined: - 'true' - 'false' - - description: Subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the resource group. name: resource_group_name required: true @@ -3282,9 +3388,10 @@ script: - Disabled - Enabled required: true - - description: Subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the resource group. name: resource_group_name required: true @@ -3302,9 +3409,10 @@ script: - 'true' - 'false' type: bool - - description: Subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the resource group. required: true name: resource_group_name @@ -3455,9 +3563,10 @@ script: predefined: - 'true' - 'false' - - description: Subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The name of the resource group. required: true name: resource_group_name @@ -3633,9 +3742,10 @@ script: type: List arguments: - name: subscription_id - description: The subscription ID. + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. type: String - required: true + required: false + required:platform: true - name: resource_group_name description: "The resource group name." type: String @@ -3644,8 +3754,9 @@ script: description: "Creates a network security group. Required Permissions: Microsoft.Network/networkSecurityGroups/write." arguments: - name: subscription_id - description: The subscription ID. - required: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. + required: false + required:platform: true - name: resource_group_name description: The resource group name. required: true @@ -3773,9 +3884,10 @@ script: description: "List all network security groups. Required permissions: Microsoft.Network/networkSecurityGroups/read." arguments: - name: subscription_id - description: The subscription ID. + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. type: String - required: true + required: false + required:platform: true - name: resource_group_name description: "The resource group name." type: String @@ -3868,10 +3980,11 @@ script: description: The rule's direction, which can be "Inbound" or "Outbound". type: String arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -3886,10 +3999,11 @@ script: - name: azure-vn-security-rule-get description: "Get a specific rule. Required permissions: Microsoft.Network/networkSecurityGroups/securityRules/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -3955,10 +4069,11 @@ script: description: "Retrieves all security rules in a network security group. Deprecated. Use the azure-vn-security-rules-list command instead. Required permission: Microsoft.Network/networkSecurityGroups/securityRules/read." deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4004,10 +4119,11 @@ script: - name: azure-vn-security-rules-list description: "Gets all security rules in a network security group. Required permissions: Microsoft.Network/networkSecurityGroups/securityRules/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4100,10 +4216,11 @@ script: description: The rule's direction, which can be "Inbound" or "Outbound". type: String arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4166,10 +4283,11 @@ script: - name: azure-vn-security-rule-create description: "Create a security rule. Required permissions: Microsoft.Network/networkSecurityGroups/securityRules/read, Microsoft.Network/networkSecurityGroups/securityRules/write." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4280,10 +4398,11 @@ script: name: azure-nsg-security-rule-delete deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4297,10 +4416,11 @@ script: - description: "Delete a security rule. Required permissions: Microsoft.Network/networkSecurityGroups/securityRules/delete." name: azure-vn-security-rule-delete arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - description: Resource group name. name: resource_group_name type: String @@ -4315,10 +4435,11 @@ script: name: azure-nsg-resource-group-list deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: Limit on the number of resource groups to return. defaultValue: 50 @@ -4366,10 +4487,11 @@ script: - description: "Gets all resource groups for a subscription. Required Permissions: Microsoft.Resources/subscriptions/resourceGroups/read." name: azure-rm-resource-groups-list arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: Limit on the number of resource groups to return. defaultValue: 50 @@ -4467,10 +4589,11 @@ script: name: resource_group_name type: String required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: The maximum number of records to return. defaultValue: 50 @@ -4489,10 +4612,11 @@ script: name: resource_group_name type: String required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: The maximum number of records to return. defaultValue: 50 @@ -4596,10 +4720,11 @@ script: name: resource_group_name type: String required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: The maximum number of records to return. defaultValue: 50 @@ -4618,10 +4743,11 @@ script: name: resource_group_name type: String required: true - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id type: String - required: true + required: false + required:platform: true - name: limit description: The maximum number of records to return. defaultValue: 50 @@ -4682,9 +4808,10 @@ script: execution: true deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: | Resource Group to which the virtual machine belongs. To see all the resource groups associated with your subscription, run the `azure-list-resource-groups` command. @@ -4707,9 +4834,10 @@ script: description: "Powers on a given virtual machine. Required Permissions: Microsoft.Compute/virtualMachines/start/action." execution: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: Resource Group to which the virtual machine belongs. name: resource_group_name required: true @@ -4731,9 +4859,10 @@ script: execution: true deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: | The resource group to which the virtual machine belongs. To see all the resource groups associated with your subscription, run the `azure-list-resource-groups` command. @@ -4763,9 +4892,10 @@ script: description: "Powers off a given virtual machine. Required Permissions: Microsoft.Compute/virtualMachines/poweroff/action." execution: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group to which the virtual machine belongs. To see all the resource groups associated with your subscription, run the `azure-rm-resource-groups-list` command. name: resource_group_name required: true @@ -4793,9 +4923,10 @@ script: description: "Retrieves the properties of a specific virtual machine. Deprecated. Use the azure-compute-vm-get command instead. Required permission: Microsoft.Compute/virtualMachines/read." deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: | The resource group to which the virtual machine belongs. To see all the resource groups associated with your subscription, run the `azure-list-resource-groups` command. @@ -4864,9 +4995,10 @@ script: - name: azure-compute-vm-get description: "Gets the properties of a given virtual machine. Required Permissions: Microsoft.Compute/virtualMachines/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group to which the virtual machine belongs. name: resource_group_name required: true @@ -4925,9 +5057,10 @@ script: description: "Retrieves the properties of a specific network interface. Deprecated. Use the azure-vn-network-interface-get command instead. Required permission: Microsoft.Network/networkInterfaces/read." deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: | The resource group to which the network interface belongs. To see all the resource groups associated with your subscription, run the `azure-list-resource-groups` command. @@ -4982,9 +5115,10 @@ script: - name: azure-vn-network-interface-get description: "Gets the properties of a given network interface. Required permissions: Microsoft.Network/networkInterfaces/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group to which the network interface belongs. name: resource_group_name required: true @@ -5034,9 +5168,10 @@ script: - name: azure-vn-network-interface-update description: "Updates a network interface. Required Permissions: Microsoft.Network/networkInterfaces/read, Microsoft.Network/networkInterfaces/write, Microsoft.Network/virtualNetworks/subnets/join/action, Microsoft.Network/publicIPAddresses/join/action, Microsoft.Network/networkSecurityGroups/join/action, Microsoft.Network/loadBalancers/backendAddressPools/join/action." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true type: String - description: The resource group to which the network interface belongs. name: resource_group_name @@ -5270,9 +5405,10 @@ script: description: "Retrieves the properties of a specific public IP address. Deprecated. Use the azure-vn-public-ip-address-get command instead. Required permission: Microsoft.Network/publicIPAddresses/read." deprecated: true arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: | The resource group to which the IP address belongs. To see all the resource groups associated with your subscription, run the `azure-list-resource-groups` command. @@ -5310,7 +5446,7 @@ script: description: The version of the public IP address (IPv4 or IPv6). type: String - contextPath: Azure.Network.IPConfigurations.properties.publicIPAllocationMethod - description: Defines how the public IP address is allocated — Static or Dynamic. + description: The method used to allocate the public IP address (Static or Dynamic). type: String - contextPath: Azure.Network.IPConfigurations.properties.resourceGuid description: The unique Azure resource GUID for the IP configuration. @@ -5321,9 +5457,10 @@ script: - name: azure-vn-public-ip-address-get description: "Gets the properties of a given public IP address. Permissions required: Microsoft.Network/publicIPAddresses/read." arguments: - - description: The subscription ID. + - description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. name: subscription_id - required: true + required: false + required:platform: true - description: The resource group to which the IP address belongs. name: resource_group_name required: true @@ -7017,8 +7154,9 @@ script: description: "Returns actual usage and cost details for a given time period, optionally filtered by service name. Required permission: Microsoft.Consumption/usageDetails/read." arguments: - name: subscription_id - description: The Azure subscription ID on which to run the command. It will be sent as a scope parameter - /subscriptions/{subscriptionId}/. - required: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. + required: false + required:platform: true - name: expand_result description: Whether to expand the result. Default - false. default: false @@ -7221,8 +7359,9 @@ script: description: "Returns cost forecast for a subscription over a given time range. Required permission: Microsoft.CostManagement/forecast/read." arguments: - name: subscription_id - description: Azure subscription ID (required). - required: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. + required: false + required:platform: true type: String - name: type description: Forecast type (required). @@ -7258,7 +7397,7 @@ script: required: false type: String - name: end_date - description: End date for the forecast. Default value 7 days from current time(UTC). e.g. 2022-01-01T00:00:00Z. + description: End date for the forecast. Default value 7 days from current time (UTC). e.g. 2022-01-01T00:00:00Z. required: false type: String - name: filter @@ -7304,8 +7443,9 @@ script: description: "Lists configured budgets at the subscription or resource group level. Required permission: Microsoft.Consumption/budgets/read." arguments: - name: subscription_id - description: The Azure subscription ID on which to run the command. Will be sent as a scope parameter - /subscriptions/{subscriptionId}/. - required: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. + required: false + required:platform: true - name: budget_name description: Name of the budget. Optional; if provided, returns a specific budget. required: false @@ -10712,8 +10852,9 @@ script: description: "Lists all the virtual machines in the specified subscription. Use the next_token argument to view the next page of results. Required Permissions: Microsoft.Compute/virtualMachines/read." arguments: - name: subscription_id - description: "The subscription ID." - required: true + description: The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. + required: false + required:platform: true - name: resource_group_name description: "The name of the resource group." required: true diff --git a/Packs/Azure/Integrations/Azure/Azure_description.md b/Packs/Azure/Integrations/Azure/Azure_description.md index 161e6482b5a1..2cd4e95dfcb5 100644 --- a/Packs/Azure/Integrations/Azure/Azure_description.md +++ b/Packs/Azure/Integrations/Azure/Azure_description.md @@ -1,9 +1,13 @@ -To connect to the Azure integration, do the following. +Connect to Azure using one of the following authentication methods: +- Client Credentials flow (recommended). +- Authorization Code flow. +- Device Code flow. +- Azure Managed Identities flow. ## Self-Deployed Azure App -To use a self-configured Azure application, you need to add a new Azure App Registration in the Azure Portal. +To use a self-configured Azure application, add a new Azure App Registration in the Azure Portal. To add the registration, refer to the following [Microsoft article](https://learn.microsoft.com/en-us/defender-xdr/api-create-app-web?view=o365-worldwide) steps 1-8. @@ -21,15 +25,62 @@ To add a permission: ### Authentication Using the Client Credentials Flow -1. Assign Azure roles using the Azure portal [Microsoft article](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) +Configure an instance that uses this flow with a self-deployed Azure application. + + +1. Assign Azure roles in the Azure portal. For more information, see the [Microsoft article](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal). *Note:* In the *Select members* section, assign the application you created earlier. -2. To configure a Microsoft integration that uses this authorization flow with a self-deployed Azure application: - a. In the *Application ID* field, enter your Client/Application ID. - b. In the *Subscription ID* field, enter your Subscription ID. - c. In the *Resource Group Name* field, enter you Resource Group Name. - d. In the *Tenant ID* field, enter your Tenant ID . - e. In the *Client Secret* field, enter your Client Secret. - f. Click **Test** to validate the URLs, token, and connection - g. Save the instance. +2. In the integration instance settings, configure the following: + 1. In the **Authentication Type** field, select the *Client Credentials* option. + 2. In the **Application ID** field, enter your client/application ID. + 3. In the **Default Subscription ID** field, enter your subscription ID. + 4. In the **Default Resource Group Name** field, enter your resource group name. + 5. In the **Tenant ID** field, enter your tenant ID. + 6. In the **Client Secret** field, enter your client secret. + 7. Click **Test** to validate the URLs, token, and connection. + 8. Save the instance. + +### Authentication Using the Authorization Code Flow + +1. In the **Authentication Type** field, select the *Authorization Code* option. +2. In the **Application ID** field, enter your client/application ID. +3. In the **Client Secret** field, enter your client secret. +4. In the **Tenant ID** field, enter your tenant ID. +5. In the **Application redirect URI** field, enter your application redirect URI. +6. Save the instance. +7. Run the ***!azure-generate-login-url*** command in the War Room and follow the instructions to obtain the authorization code. +8. Paste the value you received in the *Authorization code* parameter and save the instance again. +9. Click **Test** to validate the connection. + +### Authentication Using the Device Code Flow + +Use the [device authorization grant flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code). + +1. In the **Authentication Type** field, select the *Device Code* option. +2. Fill in the **Application ID**, **Tenant ID**, **Default Subscription ID**, and **Default Resource Group Name** fields. +3. Save the instance. +4. Run the ***!azure-auth-start*** command. +5. Follow the instructions that appear. +6. Run the ***!azure-auth-complete*** command. +7. At the end of the process you will see a message that you have logged in successfully. +8. Run the ***!azure-auth-test*** command to validate the connection. (instead of the **Test** button.) + +### Azure Managed Identities Authentication + +**Note:** This option is relevant only if the integration is running on an Azure VM. Authenticate using either a system-assigned managed identity or a user-assigned managed identity. + +For authentication using a system-assigned managed identity: + +1. In the **Authentication Type** drop-down list, select *Azure Managed Identities* and leave the **Azure Managed Identities Client ID** field empty. +2. Click **Test** to validate the connection. + +For authentication using a user-assigned managed identity: + +1. Go to the [**Azure Portal**](https://portal.azure.com/) > **Managed Identities**. +2. Select your user-assigned managed identity, then copy the client ID and paste it in the **Azure Managed Identities Client ID** field in the instance configuration. +3. In the **Authentication Type** drop-down list, select *Azure Managed Identities*. +4. Click **Test** to validate the connection. + +For more information, see [Microsoft Integrations - Authentication Flows](https://xsoar.pan.dev/docs/reference/articles/microsoft-integrations---authentication#authentication-flows). \ No newline at end of file diff --git a/Packs/Azure/Integrations/Azure/Azure_test.py b/Packs/Azure/Integrations/Azure/Azure_test.py index cc98d43ec8ce..c46e10057f2e 100644 --- a/Packs/Azure/Integrations/Azure/Azure_test.py +++ b/Packs/Azure/Integrations/Azure/Azure_test.py @@ -1089,14 +1089,15 @@ def test_remove_member_from_group_command(mocker, client): def test_get_azure_client_no_token(mocker, mock_params): """ - Given: Parameters without credentials and no token from cloud credentials. + Given: Platform (connector) path where cloud credentials return no token. When: The get_azure_client function is called. - Then: The function should raise an exception. + Then: The function should raise an exception about the missing token. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" + mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path mocker.patch("Azure.get_from_args_or_params", return_value="mocked_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={}) # No token @@ -1111,17 +1112,40 @@ def test_get_azure_client_no_token(mocker, mock_params): assert "Failed to retrieve AZURE access token" in str(excinfo.value) +def test_get_azure_client_marketplace_missing_secret(mocker, mock_params): + """ + Given: Marketplace path (no connector) with the Client Credentials flow and no Client Secret. + When: The get_azure_client function is called. + Then: The function should raise a clear DemistoException about the missing Client Secret. + """ + args = {"subscription_id": "arg_subscription_id"} + command = "command" + + mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path + + params = mock_params.copy() + params["credentials"] = {} + params["auth_type"] = "Client Credentials" + + with pytest.raises(DemistoException) as excinfo: + get_azure_client(params, args, command) + + assert "Client Secret" in str(excinfo.value) + + def test_get_azure_client_with_stored_credentials(mocker, mock_params): """ - Given: Parameters with stored credentials, arguments, and an Azure command. + Given: Marketplace path with a Client Secret configured (Client Credentials flow). When: The get_azure_client function is called. - Then: The function should return an initialized Azure client using stored credentials without cloud authentication. + Then: The function should return an initialized Azure client using the secret without cloud (CTS) authentication. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mock_client = mocker.Mock() + mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path + mock_get_managed = mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) # Test with credentials (stored credentials path) @@ -1146,12 +1170,17 @@ def test_get_azure_client_with_stored_credentials(mocker, mock_params): resource=Resources.management_azure, scope=SCOPE_BY_CONNECTION.get("Client Credentials"), headers={}, + connection_type="Client Credentials", + azure_ad_endpoint="https://login.microsoftonline.com", + auth_code=None, + redirect_uri=None, + managed_identities_client_id=mock_get_managed.return_value, ) def test_get_azure_client_with_cloud_credentials_azure_command(mocker, mock_params): """ - Given: Parameters without stored credentials, arguments, and an Azure command. + Given: Platform (connector) path without stored credentials. When: The get_azure_client function is called. Then: The function should retrieve cloud credentials and return a client with proper headers and scope. """ @@ -1161,6 +1190,8 @@ def test_get_azure_client_with_cloud_credentials_azure_command(mocker, mock_para mock_client = mocker.Mock() mock_token = "mock_access_token" + mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path + mock_get_managed = mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mocker.patch("Azure.get_from_args_or_params", return_value="test_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={"access_token": mock_token}) mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) @@ -1191,18 +1222,24 @@ def test_get_azure_client_with_cloud_credentials_azure_command(mocker, mock_para resource=Resources.management_azure, scope=SCOPE_BY_CONNECTION.get("Client Credentials"), headers=expected_headers, + connection_type="Client Credentials", + azure_ad_endpoint="https://login.microsoftonline.com", + auth_code=None, + redirect_uri=None, + managed_identities_client_id=mock_get_managed.return_value, ) def test_get_azure_client_no_token_raises_exception(mocker, mock_params): """ - Given: Parameters without stored credentials and cloud credentials that return no token. + Given: Platform (connector) path with cloud credentials that return no token. When: The get_azure_client function is called. Then: The function should raise a DemistoException about missing token. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" + mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path mocker.patch("Azure.get_from_args_or_params", return_value="test_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={}) # No access_token @@ -1250,19 +1287,26 @@ def test_get_azure_client_insecure_and_proxy_settings(mocker, mock_params): def test_get_azure_client_missing_optional_params(mocker): """ - Given: Parameters with missing optional fields. + Given: The mandatory Client Credentials params are provided, but the truly optional fields + (resource_group_name, insecure, proxy) are omitted. When: The get_azure_client function is called. - Then: The function should handle missing parameters gracefully with default values. + Then: The function builds the client using default values for the omitted optional fields. """ # Setup mocks args = {} mock_client = mocker.Mock() command = "command" + mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) - # Test with minimal parameters - params = {"credentials": {"password": "test_password"}} + # Mandatory params for Client Credentials present; optional fields omitted. + params = { + "app_id": "test_app_id", + "subscription_id": "test_subscription_id", + "tenant_id": "test_tenant_id", + "credentials": {"password": "test_password"}, + } # Call the function result = get_azure_client(params, args, command) @@ -1270,14 +1314,11 @@ def test_get_azure_client_missing_optional_params(mocker): # Verify results assert result == mock_client - # Verify default values were used + # Verify default values were used for the optional fields call_args = mock_azure_client_constructor.call_args - assert call_args[1]["app_id"] == "" - assert call_args[1]["subscription_id"] == "" assert call_args[1]["resource_group_name"] == "" assert call_args[1]["verify"] is True # Default for insecure=False assert call_args[1]["proxy"] is False # Default - assert call_args[1]["tenant_id"] is None def test_format_rule_dict_input(mocker): @@ -3909,11 +3950,20 @@ def test_get_command_resource_storage(command): class TestGetAzureClient: """Tests for the get_azure_client function.""" + @patch("Azure.get_connector_id", return_value="connector-123") + @patch("Azure.get_azure_managed_identities_client_id", return_value=None) @patch("Azure.get_from_args_or_params") @patch("Azure.get_cloud_credentials") @patch("Azure.AzureClient") - def test_with_cloud_credentials(self, mock_azure_client, mock_get_cloud_credentials, mock_get_from_args_or_params): - """Test get_azure_client with cloud credentials.""" + def test_with_cloud_credentials( + self, + mock_azure_client, + mock_get_cloud_credentials, + mock_get_from_args_or_params, + mock_get_managed, + mock_get_connector_id, + ): + """Test get_azure_client with cloud credentials (Platform path).""" # Setup mocks mock_get_from_args_or_params.return_value = "test-subscription-id" mock_get_cloud_credentials.return_value = {"access_token": "test-token"} @@ -3952,13 +4002,27 @@ def test_with_cloud_credentials(self, mock_azure_client, mock_get_cloud_credenti resource=DEFAULT_RESOURCE, scope=DEFAULT_SCOPE, headers={"Authorization": "Bearer test-token", "Content-Type": "application/json", "Accept": "application/json"}, + connection_type="Client Credentials", + azure_ad_endpoint="https://login.microsoftonline.com", + auth_code=None, + redirect_uri=None, + managed_identities_client_id=None, ) + @patch("Azure.get_connector_id", return_value="connector-123") + @patch("Azure.get_azure_managed_identities_client_id", return_value=None) @patch("Azure.get_from_args_or_params") @patch("Azure.get_cloud_credentials") @patch("Azure.AzureClient") - def test_with_storage_command(self, mock_azure_client, mock_get_cloud_credentials, mock_get_from_args_or_params): - """Test get_azure_client with a storage command.""" + def test_with_storage_command( + self, + mock_azure_client, + mock_get_cloud_credentials, + mock_get_from_args_or_params, + mock_get_managed, + mock_get_connector_id, + ): + """Test get_azure_client with a storage command (Platform path).""" # Setup mocks mock_get_from_args_or_params.return_value = "test-subscription-id" mock_get_cloud_credentials.return_value = {"access_token": "test-token"} @@ -3997,6 +4061,11 @@ def test_with_storage_command(self, mock_azure_client, mock_get_cloud_credential resource=STORAGE_RESOURCE, scope=STORAGE_SCOPE, headers={"Authorization": "Bearer test-token", "Content-Type": "application/json", "Accept": "application/json"}, + connection_type="Client Credentials", + azure_ad_endpoint="https://login.microsoftonline.com", + auth_code=None, + redirect_uri=None, + managed_identities_client_id=None, ) @@ -5598,3 +5667,559 @@ def test_network_interface_update_command_conflict_nsg(mocker): DemistoException, match="The remove_network_security_group option cannot be used with network_security_group_name." ): network_interface_update_command(client, params, args) + + +def test_test_module_device_code_flow(mocker): + """ + Given: A client configured with the Device Code authentication type. + When: test_module is called (Test button). + Then: A DemistoException is raised, because the Device Code token lives in the saved instance + context and the Test button cannot validate it. The user is directed to `!azure-auth-test`. + No API call is made. + """ + client = AzureClient(app_id="test_app_id", connection_type="Device Code") + mock_http = mocker.patch.object(client, "http_request", return_value={}) + + with pytest.raises(DemistoException) as excinfo: + Azure.test_module(client) + + assert "azure-auth-test" in str(excinfo.value) + mock_http.assert_not_called() + + +def test_test_module_authorization_code_flow(mocker): + """ + Given: A client configured with the Authorization Code authentication type. + When: test_module is called (Test button). + Then: It validates directly via the roleAssignments call (the authorization code is in the + instance parameters, so a token can be obtained on demand) and returns "ok". + """ + client = AzureClient(app_id="test_app_id", connection_type="Authorization Code") + mock_http = mocker.patch.object(client, "http_request", return_value={}) + + assert Azure.test_module(client) == "ok" + mock_http.assert_called_once() + + +def test_test_module_client_credentials_ok(mocker, client): + """ + Given: A client configured with the Client Credentials flow (default) and a successful API call. + When: test_module is called. + Then: It returns "ok". + """ + mocker.patch.object(client, "connection_type", "Client Credentials") + mocker.patch.object(client, "http_request", return_value={}) + + assert Azure.test_module(client) == "ok" + + +def test_test_module_managed_identities_uses_resource_groups(mocker): + """ + Given: A client configured with the Azure Managed Identities authentication type. + When: test_module is called (Test button). + Then: It validates via the lightweight resource-groups list call (not the roleAssignments call), + because a Managed Identity often lacks the roleAssignments/read permission while still being + able to run other commands. It returns "ok". + """ + client = AzureClient(app_id="test_app_id", connection_type="Azure Managed Identities") + client.subscription_id = "sub-123" + mock_http = mocker.patch.object(client, "http_request", return_value={}) + + assert Azure.test_module(client) == "ok" + + mock_http.assert_called_once() + _, kwargs = mock_http.call_args + assert kwargs["full_url"].endswith("/subscriptions/sub-123/resourcegroups") + assert "roleAssignments" not in kwargs["full_url"] + + +def test_test_connection_success(mocker, client): + """ + Given: A client whose MicrosoftClient can fetch an access token. + When: test_connection is called. + Then: A success message is returned. + """ + mocker.patch.object(client.ms_client, "get_access_token") + + assert "Success" in Azure.test_connection(client) + + +def test_start_auth(mocker, client): + """ + Given: A client. + When: start_auth is called. + Then: It returns CommandResults wrapping the MicrosoftClient start_auth output. + """ + mocker.patch.object(client.ms_client, "start_auth", return_value="follow these steps") + + result = Azure.start_auth(client) + + assert result.readable_output == "follow these steps" + + +def test_complete_auth(mocker, client): + """ + Given: A client. + When: complete_auth is called. + Then: It fetches the access token and returns a success message. + """ + mock_get_token = mocker.patch.object(client.ms_client, "get_access_token") + + result = Azure.complete_auth(client) + + mock_get_token.assert_called_once() + assert "completed successfully" in result + + +def test_get_azure_client_device_code_no_secret(mocker, mock_params): + """ + Given: Marketplace path (no connector) with the Device Code flow and no Client Secret. + When: get_azure_client is called. + Then: It does NOT raise the missing-secret error and builds a client with the Device Code connection type. + """ + mocker.patch("Azure.get_connector_id", return_value=None) + mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) + mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) + + params = mock_params.copy() + params["credentials"] = {} + params["auth_type"] = "Device Code" + + get_azure_client(params, {}, "command") + + # The connection_type must be propagated to the client (no missing-secret exception raised). + _, kwargs = mock_azure_client.call_args + assert kwargs["connection_type"] == "Device Code" + + +def test_get_azure_client_marketplace_storage_scope(mocker, mock_params): + """ + Given: Marketplace path (no connector), Client Credentials, and a storage-container command. + When: get_azure_client is called. + Then: The AzureClient is built with the STORAGE scope and STORAGE resource (not the management + scope), so the storage-scoped token is requested. Regression test for the scope-override bug. + """ + from Azure import STORAGE_SCOPE, STORAGE_RESOURCE + + mocker.patch("Azure.get_connector_id", return_value=None) + mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) + mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) + + params = mock_params.copy() + params["credentials"] = {"password": "secret"} + params["auth_type"] = "Client Credentials" + + get_azure_client(params, {}, "azure-storage-container-create") + + _, kwargs = mock_azure_client.call_args + assert kwargs["scope"] == STORAGE_SCOPE + assert kwargs["resource"] == STORAGE_RESOURCE + assert kwargs["connection_type"] == "Client Credentials" + + +def test_azure_client_client_credentials_does_not_send_resource_to_v2_endpoint(mocker): + """ + Given: A Client Credentials client (uses the v2.0 token endpoint with a `.default` scope). + When: AzureClient builds the MicrosoftClient. + Then: No `resource` is forwarded to MicrosoftClient, so the token request sends only `scope`. + Sending both `scope` and `resource` to the v2.0 endpoint causes Microsoft to return + "invalid_target: The resource parameter provided in the request doesn't match with the + requested scopes". Only Device Code (v1.0-style) uses `resource`. + """ + from Azure import DEFAULT_SCOPE, DEFAULT_RESOURCE + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Client Credentials", + tenant_id="my-tenant", + scope=DEFAULT_SCOPE, + resource=DEFAULT_RESOURCE, + ) + + assert captured["scope"] == DEFAULT_SCOPE + # resource must not be forwarded for the v2.0 client-credentials flow. + assert captured.get("resource") is None + + +def test_azure_client_device_code_default_resource_derives_management_scope(mocker): + """ + Given: A Device Code client for a management (default) command, i.e. resource=DEFAULT_RESOURCE. + When: AzureClient builds the MicrosoftClient. + Then: The MicrosoftClient receives the management resource (no trailing slash) and a Device Code + delegated scope derived from it. Regression test: the branch derives scope/resource from + the per-command resource instead of hardcoding management-only values. + """ + from Azure import DEFAULT_RESOURCE + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient(app_id="app", connection_type="Device Code", resource=DEFAULT_RESOURCE) + + expected_resource = DEFAULT_RESOURCE.rstrip("/") + assert captured["resource"] == expected_resource + assert captured["scope"] == f"{expected_resource}/user_impersonation offline_access user.read" + assert captured["token_retrieval_url"] is not None + + +def test_azure_client_device_code_storage_resource_derives_storage_scope(mocker): + """ + Given: A Device Code client for a storage-container command, i.e. resource=STORAGE_RESOURCE. + When: AzureClient builds the MicrosoftClient. + Then: The MicrosoftClient receives the storage resource (no trailing slash) and a Device Code + delegated scope derived from it, so storage commands are storage-scoped under Device Code + auth instead of always management-scoped. + """ + from Azure import STORAGE_RESOURCE + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient(app_id="app", connection_type="Device Code", resource=STORAGE_RESOURCE) + + expected_resource = STORAGE_RESOURCE.rstrip("/") + assert captured["resource"] == expected_resource + assert captured["scope"] == f"{expected_resource}/user_impersonation offline_access user.read" + assert captured["token_retrieval_url"] is not None + + +def test_azure_client_client_credentials_gov_endpoint_builds_gov_token_url(mocker): + """ + Given: A Client Credentials client configured with a US Gov Azure AD endpoint + (https://login.microsoftonline.us). + When: AzureClient builds the MicrosoftClient. + Then: The token_retrieval_url points to the same (gov) authority so the confidential-client token + request is not sent cross-cloud. Regression test for the Microsoft error + "Confidential Client is not supported in Cross Cloud request". + """ + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Client Credentials", + tenant_id="my-tenant", + azure_ad_endpoint="https://login.microsoftonline.us", + ) + + assert captured["token_retrieval_url"] == "https://login.microsoftonline.us/my-tenant/oauth2/v2.0/token" + + +def test_azure_client_client_credentials_default_endpoint_builds_commercial_token_url(mocker): + """ + Given: A Client Credentials client using the default (commercial) Azure AD endpoint. + When: AzureClient builds the MicrosoftClient. + Then: The token_retrieval_url points to the commercial login.microsoftonline.com authority. + """ + from Azure import DEFAULT_AZURE_AD_ENDPOINT + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Client Credentials", + tenant_id="my-tenant", + azure_ad_endpoint=DEFAULT_AZURE_AD_ENDPOINT, + ) + + assert captured["token_retrieval_url"] == "https://login.microsoftonline.com/my-tenant/oauth2/v2.0/token" + + +def test_azure_client_managed_identities_passes_mi_args_to_ms_client(mocker): + """ + Given: A client constructed with the Azure Managed Identities flow and a user-assigned client ID. + When: AzureClient builds the MicrosoftClient. + Then: The MicrosoftClient receives the managed_identities_client_id and the management Azure + resource URI, matching the reference Azure packs (e.g. AzureNetworkSecurityGroups). The + grant_type is None (the managed-identities path is selected by managed_identities_client_id, + not by grant_type). No device-code token URL is set. + """ + from Azure import DEFAULT_RESOURCE, DEFAULT_SCOPE + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Azure Managed Identities", + managed_identities_client_id="my-mi-client-id", + scope=DEFAULT_SCOPE, + resource=DEFAULT_RESOURCE, + ) + + assert captured["managed_identities_client_id"] == "my-mi-client-id" + # The MI resource URI is the management Azure resource with the trailing slash stripped + # (the MI branch derives it from `(resource or DEFAULT_RESOURCE).rstrip("/")`). + assert captured["managed_identities_resource_uri"] == DEFAULT_RESOURCE.rstrip("/") + assert captured["managed_identities_resource_uri"] == "https://management.azure.com" + # Managed Identities is not a grant_type flow; the path is chosen by managed_identities_client_id. + assert captured.get("grant_type") is None + # Device-code-only token retrieval URL must not be set for the MI flow. + assert captured.get("token_retrieval_url") is None + + +def test_azure_client_client_credentials_empty_tenant_builds_token_url_without_none(mocker): + """ + Given: A non-device-code client (Client Credentials) with no tenant_id configured. + When: AzureClient builds the MicrosoftClient. + Then: The token_retrieval_url is built with an empty tenant segment (no literal "None" in the URL). + Regression test for None stringification in the token URL. + """ + from Azure import DEFAULT_AZURE_AD_ENDPOINT + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Client Credentials", + tenant_id=None, + azure_ad_endpoint=DEFAULT_AZURE_AD_ENDPOINT, + ) + + assert "None" not in captured["token_retrieval_url"] + # urljoin collapses the empty tenant segment, so the authority host is followed by a single slash. + assert captured["token_retrieval_url"] == "https://login.microsoftonline.com/oauth2/v2.0/token" + + +def test_azure_client_managed_identities_storage_resource_derives_storage_uri(mocker): + """ + Given: A Managed Identities client for a storage-container command (per-command resource is the + storage resource). + When: AzureClient builds the MicrosoftClient. + Then: managed_identities_resource_uri is the storage resource (not the management default), so the + MI token is storage-scoped. Regression test for storage commands failing with 401/403 under + MI auth because the token was always management-scoped. + """ + from Azure import STORAGE_RESOURCE + + captured = {} + + def fake_ms_client(**kwargs): + captured.update(kwargs) + return mocker.Mock() + + mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) + AzureClient( + app_id="app", + connection_type="Azure Managed Identities", + managed_identities_client_id="my-mi-client-id", + resource=STORAGE_RESOURCE, + ) + + assert captured["managed_identities_resource_uri"] == STORAGE_RESOURCE.rstrip("/") + assert captured["managed_identities_resource_uri"] == "https://storage.azure.com" + + +def test_get_azure_client_managed_identities_resolves_client_id(mocker, mock_params): + """ + Given: Marketplace path (no connector) with auth_type "Azure Managed Identities" and a configured + managed_identities_client_id credential. + When: get_azure_client is called. + Then: get_azure_managed_identities_client_id resolves the client ID from params and it is passed to + AzureClient. The Client Credentials missing-secret guard is NOT triggered (no Client Secret + required for the MI flow). + """ + mocker.patch("Azure.get_connector_id", return_value=None) + mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) + + params = mock_params.copy() + params["credentials"] = {} # no client secret configured + params["auth_type"] = "Azure Managed Identities" + params["managed_identities_client_id"] = {"password": "resolved-mi-id"} + + get_azure_client(params, {}, "command") + + _, kwargs = mock_azure_client.call_args + assert kwargs["connection_type"] == "Azure Managed Identities" + assert kwargs["managed_identities_client_id"] == "resolved-mi-id" + + +def test_get_azure_client_managed_identities_system_assigned(mocker, mock_params): + """ + Given: Marketplace path with auth_type "Azure Managed Identities" and no client ID configured. + When: get_azure_client is called. + Then: The system-assigned managed identity sentinel is resolved and passed to AzureClient. + """ + from MicrosoftApiModule import MANAGED_IDENTITIES_SYSTEM_ASSIGNED + + mocker.patch("Azure.get_connector_id", return_value=None) + mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) + + params = mock_params.copy() + params["credentials"] = {} + params["auth_type"] = "Azure Managed Identities" + params["managed_identities_client_id"] = {} # no client id -> system assigned + + get_azure_client(params, {}, "command") + + _, kwargs = mock_azure_client.call_args + assert kwargs["managed_identities_client_id"] == MANAGED_IDENTITIES_SYSTEM_ASSIGNED + + +def test_get_azure_client_credentials_none(mocker, mock_params): + """ + Given: Marketplace path, Client Credentials flow, and credentials explicitly set to None. + When: get_azure_client is called. + Then: It raises a missing-parameter DemistoException (listing the Client Secret) without an + AttributeError. + """ + mocker.patch("Azure.get_connector_id", return_value=None) + + params = mock_params.copy() + params["credentials"] = None + params["auth_type"] = "Client Credentials" + + with pytest.raises(DemistoException) as excinfo: + get_azure_client(params, {}, "command") + + assert "Client Secret" in str(excinfo.value) + assert "Client Credentials" in str(excinfo.value) + + +@pytest.mark.parametrize( + "auth_type, missing_key, expected_in_message", + [ + ("Client Credentials", "app_id", "Application ID"), + ("Client Credentials", "tenant_id", "Tenant ID"), + ("Client Credentials", "credentials", "Client Secret"), + ("Client Credentials", "subscription_id", "Default Subscription ID"), + ("Device Code", "app_id", "Application ID"), + ("Device Code", "subscription_id", "Default Subscription ID"), + ("Authorization Code", "app_id", "Application ID"), + ("Authorization Code", "redirect_uri", "Application redirect URI"), + ("Authorization Code", "auth_code", "Authorization code"), + ("Authorization Code", "subscription_id", "Default Subscription ID"), + ("Azure Managed Identities", "subscription_id", "Default Subscription ID"), + ], +) +def test_validate_auth_params_missing(auth_type, missing_key, expected_in_message): + """ + Given: A full set of params for an auth type, with exactly one mandatory param removed. + When: validate_auth_params is called. + Then: It raises a DemistoException naming the missing parameter and the auth type. + """ + from Azure import validate_auth_params + + full_params = { + "app_id": "app", + "subscription_id": "sub", + "tenant_id": "tenant", + "credentials": {"password": "secret"}, + "auth_code": {"password": "code"}, + "redirect_uri": "redirect-uri", + "managed_identities_client_id": {"password": "mi-id"}, + "auth_type": auth_type, + } + full_params.pop(missing_key) + + with pytest.raises(DemistoException) as excinfo: + validate_auth_params(full_params, auth_type) + + assert expected_in_message in str(excinfo.value) + assert auth_type in str(excinfo.value) + + +@pytest.mark.parametrize( + "auth_type, params", + [ + ( + "Client Credentials", + {"app_id": "app", "subscription_id": "sub", "tenant_id": "t", "credentials": {"password": "s"}}, + ), + ("Device Code", {"app_id": "app", "subscription_id": "sub"}), + ( + "Authorization Code", + { + "app_id": "app", + "subscription_id": "sub", + "redirect_uri": "redirect-uri", + "auth_code": {"password": "c"}, + }, + ), + ( + "Azure Managed Identities", + {"subscription_id": "sub", "managed_identities_client_id": {"password": "mi-id"}}, + ), + ], +) +def test_validate_auth_params_valid(auth_type, params): + """ + Given: A complete set of mandatory params for an auth type. + When: validate_auth_params is called. + Then: It does not raise. + """ + from Azure import validate_auth_params + + params = {**params, "auth_type": auth_type} + validate_auth_params(params, auth_type) # Should not raise + + +def test_validate_auth_params_managed_identities_system_assigned(): + """ + Given: Azure Managed Identities with no explicit client ID (system-assigned) and a subscription. + When: validate_auth_params is called. + Then: It does not raise, because the system-assigned identity resolves to a sentinel client ID. + """ + from Azure import validate_auth_params + + params = { + "auth_type": "Azure Managed Identities", + "subscription_id": "sub", + "managed_identities_client_id": {}, # no password -> system assigned + } + validate_auth_params(params, "Azure Managed Identities") # Should not raise + + +def test_main_auth_reset(mocker): + """ + Given: The azure-auth-reset command on the marketplace path. + When: main is called. + Then: reset_auth is invoked and the client is not built. + """ + from Azure import main + + mocker.patch.object(demisto, "command", return_value="azure-auth-reset") + mocker.patch.object(demisto, "params", return_value={}) + mocker.patch.object(demisto, "args", return_value={}) + mocker.patch("Azure.get_connector_id", return_value=None) + mock_reset = mocker.patch("Azure.reset_auth", return_value="reset done") + mock_get_client = mocker.patch("Azure.get_azure_client") + mocker.patch("Azure.return_results") + + main() + + mock_reset.assert_called_once() + mock_get_client.assert_not_called() diff --git a/Packs/Azure/Integrations/Azure/README.md b/Packs/Azure/Integrations/Azure/README.md index 7a46d705fe3b..d192ecc04015 100644 --- a/Packs/Azure/Integrations/Azure/README.md +++ b/Packs/Azure/Integrations/Azure/README.md @@ -7,13 +7,17 @@ Click Connect, then click Show advanced settings, and under Additional Security | **Parameter** | **Description** | **Required** | | --- | --- | --- | | Application ID | | | -| Default Subscription ID | You can set the value either in the configuration or directly within the commands. If you set it in both places, the value in the command will override the configuration setting. | | +| Default Subscription ID | You can set the value either in the configuration or directly within the commands. If you set it in both places, the value in the command will override the configuration setting. | True | | Default Resource Group Name | You can set the value either in the configuration or directly within the commands. If you set it in both places, the value in the command will override the configuration setting. | | | Azure AD endpoint | Azure AD endpoint associated with a national cloud. | False | +| Authentication Type | The type of authentication. Can be Client Credentials (default authentication type), Device Code, Authorization Code, or Azure Managed Identities. | False | | Trust any certificate (not secure) | | False | | Use system proxy settings | | False | -| Tenant ID | | False | -| Client Secret | | False | +| Tenant ID | The tenant ID of the Azure app registration. | False | +| Client Secret | The client secret of the Azure app registration. Required when using the Client Credentials flow. | False | +| Application redirect URI | The application redirect URI for the Authorization Code flow. | False | +| Authorization code | The authorization code for the Authorization Code flow, received from the authorization step. See the Detailed Instructions (?) section. | False | +| Azure Managed Identities Client ID | The managed identities client ID for authentication. Relevant only when the integration is running on an Azure VM. | False | ## Commands @@ -71,7 +75,7 @@ Update a security rule. If one does not exist, it will be created. The command i ### azure-storage-account-update *** -Updates a specific account storage. Required Permissions: Microsoft.Storage/storageAccounts/read, Microsoft.Storage/storageAccounts/write. +Updates a specific account storage. Required permissions: Microsoft.Storage/storageAccounts/read, Microsoft.Storage/storageAccounts/write. #### Base Command @@ -82,7 +86,7 @@ Updates a specific account storage. Required Permissions: Microsoft.Storage/stor | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | account_name | The name of the storage account. | Required | | sku | Gets or sets the SKU name, Required for account creation; optional for update. Possible values are: Premium_LRS, Premium_ZRS, Standard_GRS, Standard_GZRS, Standard_LRS, Standard_RAGRS, Standard_RAGZRS, Standard_ZRS. | Optional | | kind | Indicates the type of storage account, Required for account creation; optional for update. Possible values are: Storage, StorageV2, BlobStorage, FileStorage, BlockBlobStorage. | Optional | @@ -173,7 +177,7 @@ Sets properties for the blob service in a specific account storage. The command ### azure-policy-assignment-create *** -Creates a policy assignment. Required Permissions: Microsoft.Authorization/policyAssignments/read, Microsoft.Authorization/policyAssignments/write. +Creates a policy assignment. Required permissions: Microsoft.Authorization/policyAssignments/read, Microsoft.Authorization/policyAssignments/write. #### Base Command @@ -184,7 +188,7 @@ Creates a policy assignment. Required Permissions: Microsoft.Authorization/polic | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | name | Name of the policy assignment. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | scope | The scope of the policy assignment. | Required | | policy_definition_id | The ID of the policy definition or policy set definition being assigned. | Optional | | display_name | The assignment display name. | Optional | @@ -351,7 +355,7 @@ Updates the authentication and authorization settings of an existing Azure Web A ### azure-mysql-flexible-server-param-set *** -Updates a configuration of a server. Required Permissions: Microsoft.DBforMySQL/flexibleServers/configurations/read, Microsoft.DBforMySQL/flexibleServers/configurations/write. +Updates a server configuration. Required permissions: Microsoft.DBforMySQL/flexibleServers/configurations/read, Microsoft.DBforMySQL/flexibleServers/configurations/write. #### Base Command @@ -361,8 +365,8 @@ Updates a configuration of a server. Required Permissions: Microsoft.DBforMySQL/ | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| resource_group_name | The resource group name. | Optional | -| subscription_id | Subscription ID. | Optional | +| resource_group_name | The resource group name. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | server_name | Name of the MySQL flexible server. | Required | | configuration_name | The name of the server configuration. | Required | | source | Source of the configuration. | Optional | @@ -375,7 +379,7 @@ There is no context output for this command. ### azure-monitor-log-profile-update *** -Updates a log profile in Azure Monitoring REST API. Required Permissions: Microsoft.Insights/logprofiles/read, Microsoft.Insights/logprofiles/write. +Updates a log profile in Azure Monitoring REST API. Required permissions: Microsoft.Insights/logprofiles/read, Microsoft.Insights/logprofiles/write. #### Base Command @@ -386,7 +390,7 @@ Updates a log profile in Azure Monitoring REST API. Required Permissions: Micros | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | log_profile_name | The name of the log profile to update. | Required | -| subscription_id | subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | location | Resource location. | Optional | | retention_policy_days | Number of days to retain logs. | Optional | | retention_policy_enabled | Whether to enable the retention policy. Possible values are: true, false. | Optional | @@ -733,7 +737,7 @@ Updates the database's threat detection policy. The command is deprecated, pleas ### azure-sql-db-transparent-data-encryption-set *** -Updates a logical database's transparent data encryption configuration. Required Permissions: Microsoft.Sql/servers/databases/transparentDataEncryption/read, Microsoft.Sql/servers/databases/transparentDataEncryption/write. +Updates a logical database's transparent data encryption configuration. Required permissions: Microsoft.Sql/servers/databases/transparentDataEncryption/read, Microsoft.Sql/servers/databases/transparentDataEncryption/write. #### Base Command @@ -746,80 +750,13 @@ Updates a logical database's transparent data encryption configuration. Required | server_name | Server name. | Required | | db_name | Database name. | Required | | state | The state of the transparent data encryption. Possible values are: Disabled, Enabled. | Required | -| subscription_id | Subscription ID. | Optional | -| resource_group_name | The name of the resource group. | Optional | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | +| resource_group_name | The name of the resource group. | Required | #### Context Output There is no context output for this command. -### azure-cosmos-db-update - -*** -Updates the properties of an existing Azure Cosmos DB database account. The command is deprecated, please use azure-cosmosdb-db-account-update. Required Permissions: Microsoft.DocumentDB/databaseAccounts/read, Microsoft.DocumentDB/databaseAccounts/write. - -#### Base Command - -`azure-cosmos-db-update` - -#### Input - -| **Argument Name** | **Description** | **Required** | -| --- | --- | --- | -| account_name | Cosmos DB database account name. | Required | -| disable_key_based_metadata_write_access | Whether to disable write operations on metadata resources via account keys. Possible values are: true, false. | Optional | -| subscription_id | Subscription ID. | Optional | -| resource_group_name | The name of the resource group. | Optional | - -#### Context Output - -| **Path** | **Type** | **Description** | -| --- | --- | --- | -| Azure.CosmosDB.id | String | The unique resource identifier of the ARM resource. | -| Azure.CosmosDB.name | String | The name of the ARM resource. | -| Azure.CosmosDB.location | String | The location of the resource group to which the resource belongs. | -| Azure.CosmosDB.kind | String | The database account type. | -| Azure.CosmosDB.identity.type | String | The type of identity used for the resource. | -| Azure.CosmosDB.identity.userAssignedIdentities | Dictionary | The list of user identities associated with the resource. | -| Azure.CosmosDB.properties.analyticalStorageConfiguration.schemaType | String | The analytical storage schema types. | -| Azure.CosmosDB.properties.apiProperties | Dictionary | API specific properties. | -| Azure.CosmosDB.properties.backupPolicy | unknown | The policy for taking backups on an account. | -| Azure.CosmosDB.properties.capabilities | List | List of Cosmos DB capabilities for the account. | -| Azure.CosmosDB.properties.capacity | Integer | Properties related to capacity enforcement on an account. | -| Azure.CosmosDB.properties.connectorOffer | String | The Cassandra connector offer type for the Cosmos DB database account. | -| Azure.CosmosDB.properties.consistencyPolicy | String | The consistency policy for the Cosmos DB database account. | -| Azure.CosmosDB.properties.cors | List | The CORS policy for the Cosmos DB database account. | -| Azure.CosmosDB.properties.createMode | String | The mode of account creation. | -| Azure.CosmosDB.properties.customerManagedKeyStatus | String | Status of the Customer Managed Key feature on the account. | -| Azure.CosmosDB.properties.databaseAccountOfferType | String | The offer type for the Cosmos DB database account. | -| Azure.CosmosDB.properties.defaultIdentity | String | The default identity for accessing key vault used in features like customer managed keys. | -| Azure.CosmosDB.properties.disableKeyBasedMetadataWriteAccess | Boolean | Whether write operations on metadata resources via account keys is disabled. | -| Azure.CosmosDB.properties.disableLocalAuth | Boolean | Whether local authentication is disabled. | -| Azure.CosmosDB.properties.documentEndpoint | String | The connection endpoint for the Cosmos DB database account. | -| Azure.CosmosDB.properties.enableAnalyticalStorage | Boolean | Whether storage analytics are enabled. | -| Azure.CosmosDB.properties.enableAutomaticFailover | Boolean | Enables automatic failover of the write region. | -| Azure.CosmosDB.properties.enableBurstCapacity | Boolean | Whether Burst Capacity is enabled. | -| Azure.CosmosDB.properties.enableCassandraConnector | Boolean | Enables the Cassandra connector on the Cosmos DB account. | -| Azure.CosmosDB.properties.enableFreeTier | Boolean | Whether Free Tier is enabled. | -| Azure.CosmosDB.properties.enableMultipleWriteLocations | Boolean | Enables the account to write in multiple locations. | -| Azure.CosmosDB.properties.enablePartitionMerge | Boolean | Whether Partition Merge is enabled. | -| Azure.CosmosDB.properties.enablePerRegionPerPartitionAutoscale | Boolean | Whether PerRegionPerPartitionAutoscale is enabled. | -| Azure.CosmosDB.properties.failoverPolicies | List | An array that contains the regions ordered by their failover priorities. | -| Azure.CosmosDB.properties.instanceId | String | A unique identifier assigned to the database account. | -| Azure.CosmosDB.properties.ipRules | List | List of IP rules. | -| Azure.CosmosDB.properties.isVirtualNetworkFilterEnabled | Boolean | Whether the Virtual Network ACL rules are enabled. | -| Azure.CosmosDB.properties.keyVaultKeyUri | String | The URI of the key vault. | -| Azure.CosmosDB.properties.keysMetadata | Dictionary | Metadata related to each access key for the given Cosmos DB database account. | -| Azure.CosmosDB.properties.locations | List | An array that contains all of the locations enabled for the Cosmos DB account. | -| Azure.CosmosDB.properties.minimalTlsVersion | String | The minimum allowed TLS version. | -| Azure.CosmosDB.properties.networkAclBypass | String | Which services are allowed to bypass firewall checks. | -| Azure.CosmosDB.properties.networkAclBypassResourceIds | List | List of resource IDs that are allowed to bypass firewall checks. | -| Azure.CosmosDB.properties.privateEndpointConnections | List | List of private endpoint connections. | -| Azure.CosmosDB.properties.provisioningState | String | The status of the Cosmos DB account at the time the operation was called. | -| Azure.CosmosDB.properties.readLocations | List | An array that contains the read locations enabled for the Cosmos DB account. | -| Azure.CosmosDB.properties.virtualNetworkRules | List | List of Virtual Network ACL rules. | -| Azure.CosmosDB.properties.writeLocations | List | An array that contains the write locations enabled for the Cosmos DB account. | - ### azure-vm-instance-details-get *** @@ -1295,7 +1232,7 @@ Powers off a given virtual machine. The command is deprecated, please use azure- ### azure-billing-usage-list *** -Returns actual usage and cost details for a given time period, optionally filtered by service name. Required Permissions: Microsoft.Consumption/usageDetails/read. +Returns actual usage and cost details for a given time period, optionally filtered by service name. Required permission: Microsoft.Consumption/usageDetails/read. #### Base Command @@ -1305,11 +1242,11 @@ Returns actual usage and cost details for a given time period, optionally filter | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The Azure subscription ID on which to run the command. It will be sent as a scope parameter - /subscriptions/{subscriptionId}/. | Required | -| expand_result | Whether to expand the result. Default - false. | Optional | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | +| expand_result | Whether to expand the result. Default - false. Possible values are: false, true. | Optional | | filter | Optional filter for the API. API argument - $filter. | Optional | | metric | The metric to retrieve. API values [actualcost, amortizedcost, usage]. Possible values are: Actual Cost, Amortized Cost, Usage. | Optional | -| max_results | Maximum results to return. Default - 50, Max - 1000. | Optional | +| max_results | Maximum results to return. Default - 50, Max - 1000. Default is 50. | Optional | | next_page_token | Next page token for pagination. Use value from Azure.Billing.UsageNextToken. | Optional | #### Context Output @@ -1377,7 +1314,7 @@ Returns actual usage and cost details for a given time period, optionally filter ### azure-billing-forecast-list *** -Returns cost forecast for a subscription over a given time range. Required Permissions: Microsoft.CostManagement/forecast/read. +Returns cost forecast for a subscription over a given time range. Required permission: Microsoft.CostManagement/forecast/read. #### Base Command @@ -1385,18 +1322,18 @@ Returns cost forecast for a subscription over a given time range. Required Permi #### Input -| **Argument Name** | **Description** | **Required** | -| --- | --- | --- | -| subscription_id | Azure subscription ID (required). | Required | -| type | Forecast type (required). Possible values are: Usage, ActualCost, AmortizedCost. | Required | -| aggregation_function_name | The name of the column to aggregate (required). Possible values are: PreTaxCostUSD, Cost, CostUSD, PreTaxCost. | Required | -| aggregation_function_type | The name of the aggregation function to use. Default is Sum. | Optional | -| granularity | The granularity of the forecast. Default is Daily. | Optional | -| start_date | Start date for the forecast. Default value current time. | Optional | -| end_date | End date for the forecast. Default value 7 days from current time. | Optional | -| filter | A URL parameter to filter forecasts by properties/usageDate (Utc time), properties/chargeType or properties/grain. The filter supports 'eq', 'lt', 'gt', 'le', 'ge', and 'and'. It does not currently support 'ne', 'or', or 'not'. | Optional | -| include_actual_cost | Include actual cost data. Possible values are: true, false. Default is false. | Optional | -| include_fresh_partial_cost | Include fresh partial cost data. Possible values are: true, false. Default is false. | Optional | +| **Argument Name** | **Description** | **Required** | +| --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | +| type | Forecast type (required). Possible values are: Usage, ActualCost, AmortizedCost. | Required | +| aggregation_function_name | The name of the column to aggregate (required). Possible values are: PreTaxCostUSD, Cost, CostUSD, PreTaxCost. | Required | +| aggregation_function_type | The name of the aggregation function to use. Default is Sum. | Optional | +| granularity | The granularity of the forecast. Default is Daily. | Optional | +| start_date | Start date for the forecast. Default value current time (UTC). e.g. 2022-01-01T00:00:00Z. | Optional | +| end_date | End date for the forecast. Default value 7 days from current time (UTC). e.g. 2022-01-01T00:00:00Z. | Optional | +| filter | A URL parameter to filter forecasts by properties/usageDate (Utc time), properties/chargeType or properties/grain. The filter supports 'eq', 'lt', 'gt', 'le', 'ge', and 'and'. It does not currently support 'ne', 'or', or 'not'. | Optional | +| include_actual_cost | Include actual cost data. Possible values are: true, false. Default is false. | Optional | +| include_fresh_partial_cost | Include fresh partial cost data. Possible values are: true, false. Default is false. | Optional | #### Context Output @@ -1412,7 +1349,7 @@ Returns cost forecast for a subscription over a given time range. Required Permi ### azure-billing-budgets-list *** -Lists configured budgets at the subscription or resource group scope. Required Permissions: Microsoft.Consumption/budgets/read. +Lists configured budgets at the subscription or resource group level. Required permission: Microsoft.Consumption/budgets/read. #### Base Command @@ -1422,7 +1359,7 @@ Lists configured budgets at the subscription or resource group scope. Required P | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The Azure subscription ID on which to run the command. Will be sent as a scope parameter - /subscriptions/{subscriptionId}/. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | budget_name | Name of the budget. Optional; if provided, returns a specific budget. | Optional | #### Context Output @@ -1461,7 +1398,7 @@ Lists configured budgets at the subscription or resource group scope. Required P ### azure-storage-container-property-get *** -Retrieve properties for the specified Container. Required Permissions: Microsoft.Storage/storageAccounts/blobServices/containers/read. +Retrieves properties for a specific Container. Required permission: Microsoft.Storage/storageAccounts/blobServices/containers/read. #### Base Command @@ -1472,7 +1409,7 @@ Retrieve properties for the specified Container. Required Permissions: Microsoft | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | container_name | The name of the Container. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1491,7 +1428,7 @@ Retrieve properties for the specified Container. Required Permissions: Microsoft ### azure-storage-container-create *** -Create a new Container under the specified account. Required Permissions: Microsoft.Storage/storageAccounts/blobServices/containers/write. +Creates a new Container under the specified account. Required permission: Microsoft.Storage/storageAccounts/blobServices/containers/write. #### Base Command @@ -1502,7 +1439,7 @@ Create a new Container under the specified account. Required Permissions: Micros | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | container_name | The name of the Container to create. Rules for naming containers can be found here:
https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata
. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1513,7 +1450,7 @@ There is no context output for this command. ### azure-storage-container-delete *** -Marks the specified Container for deletion. The Container and any Blobs contained within it, will be deleted during garbage collection. Required Permissions: Microsoft.Storage/storageAccounts/blobServices/containers/delete. +Marks a specific Container for deletion. The Container and any Blobs contained within it will be deleted during garbage collection. Required permission: Microsoft.Storage/storageAccounts/blobServices/containers/delete. #### Base Command @@ -1524,7 +1461,7 @@ Marks the specified Container for deletion. The Container and any Blobs containe | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | container_name | The name of the Container to delete. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1710,7 +1647,7 @@ There is no context output for this command. ### azure-storage-container-public-access-block *** -Block public access to a container. Required Permissions: Microsoft.Storage/storageAccounts/blobServices/containers/setAcl/action. +Blocks public access to a container. Required permission: Microsoft.Storage/storageAccounts/blobServices/containers/setAcl/action. #### Base Command @@ -1721,7 +1658,7 @@ Block public access to a container. Required Permissions: Microsoft.Storage/stor | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | container_name | Name of a container. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1729,39 +1666,6 @@ Block public access to a container. Required Permissions: Microsoft.Storage/stor There is no context output for this command. -### azure-nsg-security-rules-list - -*** -Gets all security rules in a network security group. The command is deprecated, please use azure-vn-security-rules-list. Required Permissions: Microsoft.Network/networkSecurityGroups/securityRules/read. - -#### Base Command - -`azure-nsg-security-rules-list` - -#### Input - -| **Argument Name** | **Description** | **Required** | -| --- | --- | --- | -| subscription_id | The subscription ID. | Required | -| resource_group_name | Resource group name. | Required | -| network_security_group_name | The name of the network security group. | Required | - -#### Context Output - -| **Path** | **Type** | **Description** | -| --- | --- | --- | -| Azure.NSGRule.name | String | The rule's name. | -| Azure.NSGRule.id | String | The rule's ID. | -| Azure.NSGRule.properties.provisioningState | String | The rule's provisioning state. | -| Azure.NSGRule.properties.protocol | String | The protocol, which can be "TCP", "UDP", "ICMP", or "\*". | -| Azure.NSGRule.properties.sourcePortRange | String | For a single port, the source port or a range of ports. | -| Azure.NSGRule.properties.destinationPortRange | String | For a single port, the destination port or range of ports. | -| Azure.NSGRule.properties.sourceAddressPrefix | String | The source address. | -| Azure.NSGRule.properties.destinationAddressPrefix | String | The destination address. | -| Azure.NSGRule.properties.access | String | The rule's access. Can be "Allow" or "Deny". | -| Azure.NSGRule.properties.priority | Number | The rule’s priority, ranging from 100 to 4096. | -| Azure.NSGRule.properties.direction | String | The rule's direction, which can be "Inbound" or "Outbound". | - ### azure-storage-blob-property-get *** @@ -1777,7 +1681,7 @@ Retrieve Blob properties. Required Permissions: Microsoft.Storage/storageAccount | --- | --- | --- | | container_name | The name of the Blob Container. | Required | | blob_name | The name of the blob. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1821,7 +1725,7 @@ Retrieve the tags of the specified Blob. Required Permissions: Microsoft.Storage | --- | --- | --- | | container_name | The name of the Blob Container. | Required | | blob_name | The name of the blob. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -1847,7 +1751,7 @@ Lists all the virtual machines in the specified subscription. Use the next_token | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The name of the resource group. | Required | | next_token | The URI to fetch the next page of results. | Optional | @@ -1927,7 +1831,7 @@ Gets the properties of a given virtual machine. Required Permissions: Microsoft. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group to which the virtual machine belongs. | Required | | virtual_machine_name | The name of the virtual machine you want to view the details of. To see all the virtual machines with their associated names for a specific resource group, run the `azure-compute-vm-list` command. | Required | | expand | The expand expression to apply on the operation. 'InstanceView' retrieves a snapshot of the runtime properties of the virtual machine that is managed by the platform and can change outside of control plane operations. 'UserData' retrieves the UserData property as part of the VM model view that was provided by the user during the VM Create/Update operation. Possible values are: instanceView, userData, resiliencyView. Default is instanceView. | Optional | @@ -1963,7 +1867,7 @@ Creates a network security group. Required Permissions: Microsoft.Network/networ | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | security_group_name | The name of the network security group. | Required | | location | The location of the network security group. Possible values are: australiacentral, australiacentral2, australiaeast, australiasoutheast, brazilsouth, brazilsoutheast, brazilus, canadacentral, canadaeast, centralindia, centralus, centraluseuap, eastasia, eastus, eastus2, eastus2euap, francecentral, francesouth, germanynorth, germanywestcentral, israelcentral, italynorth, japaneast, japanwest, jioindiacentral, jioindiawest, koreacentral, koreasouth, malaysiasouth, mexicocentral, newzealandnorth, northcentralus, northeurope, norwayeast, norwaywest, polandcentral, qatarcentral, southafricanorth, southafricawest, southcentralus, southeastasia, southindia, spaincentral, swedencentral, swedensouth, switzerlandnorth, switzerlandwest, uaecentral, uaenorth, uksouth, ukwest, westcentralus, westeurope, westindia, westus, westus2, westus3, australiaeastfoundational, austriaeast, chilecentral, eastusslv, indonesiacentral, israelnorthwest, malaysiawest, southcentralus2, southeastus, southeastus3, southwestus, usgovarizona, usgovtexas, usgovvirginia. | Required | @@ -1999,7 +1903,7 @@ Powers off a given virtual machine. Required Permissions: Microsoft.Compute/virt | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group to which the virtual machine belongs. To see all the resource groups associated with your subscription, run the `azure-rm-resource-groups-list` command. | Required | | virtual_machine_name | The name of the virtual machine to power off. To see all virtual machines with their associated names for a specific resource group, run the `azure-compute-vm-list` command. | Required | | skip_shutdown | Set to True to request non-graceful VM shutdown. Possible values are: true, false. Default is false. | Optional | @@ -2025,7 +1929,7 @@ Powers on a given virtual machine. Required Permissions: Microsoft.Compute/virtu | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | Resource Group to which the virtual machine belongs. | Required | | virtual_machine_name | Name of the virtual machine to power on. To see all virtual machines and their associated names for a specific resource group, run the `azure-compute-vm-list` command. | Required | @@ -2052,7 +1956,7 @@ Updates the properties of an existing Azure Cosmos DB database account. The requ | --- | --- | --- | | account_name | Cosmos DB database account name. | Required | | disable_key_based_metadata_write_access | Whether to disable write operations on metadata resources via account keys. Possible values are: true, false. | Optional | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The name of the resource group. | Required | #### Context Output @@ -2067,7 +1971,7 @@ Updates the properties of an existing Azure Cosmos DB database account. The requ | Azure.CosmosDB.DBAccounts.identity.userAssignedIdentities | Unknown | The list of user identities associated with the resource. | | Azure.CosmosDB.DBAccounts.properties.analyticalStorageConfiguration | Unknown | Analytical storage specific properties. | | Azure.CosmosDB.DBAccounts.properties.apiProperties | Unknown | API specific properties. | -| Azure.CosmosDB.DBAccounts.properties.backupPolicy | unknown | The policy for taking backups on an account. | +| Azure.CosmosDB.DBAccounts.properties.backupPolicy | Unknown | The policy for taking backups on an account. | | Azure.CosmosDB.DBAccounts.properties.capabilities | Unknown | List of Cosmos DB capabilities for the account. | | Azure.CosmosDB.DBAccounts.properties.capacity | Number | Properties related to capacity enforcement on an account. | | Azure.CosmosDB.DBAccounts.properties.connectorOffer | String | The Cassandra connector offer type for the Cosmos DB database account. | @@ -2118,7 +2022,7 @@ Updates a key vault in the specified subscription. The required permissions are: | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | vault_name | Key Vault name. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The name of the resource group. | Required | | enable_purge_protection | Whether protection against purge is enabled for this vault. This functionality is always enabled, it cannot be disabled. Possible values are: true, false. | Optional | | enable_soft_delete | Whether soft delete is enabled for this key vault. This functionality is always enabled, it cannot be disabled. Possible values are: true, false. | Optional | @@ -2159,7 +2063,7 @@ Gets all resource groups for a subscription. Required Permissions: Microsoft.Res | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | limit | Limit on the number of resource groups to return. Default is 50. | Optional | | tag | A single tag in the form of `{"Tag Name":"Tag Value"}` to filter the list by. | Optional | @@ -2196,7 +2100,7 @@ Updates a database's security alert policy. The required permissions are: Micros | server_name | Server name. | Required | | db_name | Database name. | Required | | email_account_admins_enabled | Whether the alert is sent to the account administrators. Possible values are: true, false. | Optional | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The name of the resource group. | Required | #### Context Output @@ -2234,7 +2138,7 @@ Gets network interfaces in a resource group. Required permissions: Microsoft.Net | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | limit | The maximum number of records to return. Default is 50. | Optional | | all_results | Whether to retrieve all results by overriding the default limit. Possible values are: false, true. Default is false. | Optional | @@ -2269,9 +2173,9 @@ Gets public IP addresses in a resource group. Required permissions: Microsoft.Ne | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | limit | The maximum number of records to return. Default is 50. | Optional | -| all_results | Whether to retrieve all results by overriding the default limit. Possible values are: false, true. | Optional | +| all_results | Whether to retrieve all results by overriding the default limit. Possible values are: false, true. Default is false. | Optional | #### Context Output @@ -2305,7 +2209,7 @@ List all network security groups. Required permissions: Microsoft.Network/networ | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | #### Context Output @@ -2337,7 +2241,7 @@ Create a security rule. Required permissions: Microsoft.Network/networkSecurityG | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | Resource group name. | Required | | security_group_name | The name of the security group. | Required | | security_rule_name | The name of the rule to create. | Required | @@ -2346,9 +2250,9 @@ Create a security rule. Required permissions: Microsoft.Network/networkSecurityG | protocol | The protocol on which to apply the rule. Possible values are: Any, TCP, UDP, ICMP. Default is Any. | Optional | | source | The source IP address range from which incoming traffic is allowed or denied by this rule. Possible values are "Any", an IP address range, an application security group, or a default tag. Default is Any. | Optional | | priority | The priority of the rule, with a value between 100 and 4096. Each rule in the collection must have a unique priority number. Lower numbers indicate higher priority. | Required | -| source_ports | The source ports from which traffic is allowed or denied by this rule. Provide a single port, such as 80; a port range, such as 1024-65535; or a comma-separated list of single ports and/or port ranges, such as 80,1024-65535. Use an asterisk (*) to allow traffic on any port. Default is`*`. | Optional | +| source_ports | The source ports from which traffic is allowed or denied by this rule. Provide a single port, such as 80; a port range, such as 1024-65535; or a comma-separated list of single ports and/or port ranges, such as 80,1024-65535. Use an asterisk (*) to allow traffic on any port. Default is `*`. | Optional | | destination | The destination IP address range for outgoing traffic that is allowed or denied by this rule. The destination filter can be "Any", an IP address range, an application security group, or a default tag. Default is Any. | Optional | -| destination_ports | The destination ports from which traffic is allowed or denied by this rule. Provide a single port, such as 80; a port range, such as 1024-65535; or a comma-separated list of single ports and/or port ranges, such as 80,1024-65535. Use an asterisk (*) to allow traffic on any port. Default is`*`. | Optional | +| destination_ports | The destination ports from which traffic is allowed or denied by this rule. Provide a single port, such as 80; a port range, such as 1024-65535; or a comma-separated list of single ports and/or port ranges, such as 80,1024-65535. Use an asterisk (*) to allow traffic on any port. Default is*. | Optional | | description | A description to add to the rule. | Optional | #### Context Output @@ -2384,7 +2288,7 @@ Updates a network interface. Required Permissions: Microsoft.Network/networkInte | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group to which the network interface belongs. | Required | | network_interface_name | The name of the network interface to update. | Required | | location | The location of the network security group. Possible values are: australiacentral, australiacentral2, australiaeast, australiasoutheast, brazilsouth, brazilsoutheast, brazilus, canadacentral, canadaeast, centralindia, centralus, centraluseuap, eastasia, eastus, eastus2, eastus2euap, francecentral, francesouth, germanynorth, germanywestcentral, israelcentral, italynorth, japaneast, japanwest, jioindiacentral, jioindiawest, koreacentral, koreasouth, malaysiasouth, mexicocentral, newzealandnorth, northcentralus, northeurope, norwayeast, norwaywest, polandcentral, qatarcentral, southafricanorth, southafricawest, southcentralus, southeastasia, southindia, spaincentral, swedencentral, swedensouth, switzerlandnorth, switzerlandwest, uaecentral, uaenorth, uksouth, ukwest, westcentralus, westeurope, westindia, westus, westus2, westus3, australiaeastfoundational, austriaeast, chilecentral, eastusslv, indonesiacentral, israelnorthwest, malaysiawest, southcentralus2, southeastus, southeastus3, southwestus, usgovarizona, usgovtexas, usgovvirginia. | Optional | @@ -2446,7 +2350,7 @@ Delete a security rule. Required permissions: Microsoft.Network/networkSecurityG | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | Resource group name. | Required | | security_group_name | The name of the security group. | Required | | security_rule_name | The name of the rule to delete. | Required | @@ -2468,7 +2372,7 @@ Get a specific rule. Required permissions: Microsoft.Network/networkSecurityGrou | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | Resource group name. | Required | | security_group_name | The name of the security group. | Required | | security_rule_name | The name of the rule to get. | Required | @@ -2507,7 +2411,7 @@ Update a security rule. If one does not exist, it will be created. Required perm | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The name of the resource group. | Required | | security_group_name | The name of the security group. | Required | | security_rule_name | The name of the rule to be updated. | Required | @@ -2554,7 +2458,7 @@ Gets all security rules in a network security group. Required permissions: Micro | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | Resource group name. | Required | | network_security_group_name | The name of the network security group. | Required | @@ -2574,6 +2478,8 @@ Gets all security rules in a network security group. Required permissions: Micro | Azure.VirtualNetworks.SecurityRules.properties.priority | Number | The rule’s priority, ranging from 100 to 4096. | | Azure.VirtualNetworks.SecurityRules.properties.direction | String | The rule's direction, which can be "Inbound" or "Outbound". | +#### Base Command + ### azure-vn-network-interface-get *** @@ -2587,7 +2493,7 @@ Gets the properties of a given network interface. Required permissions: Microsof | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group to which the network interface belongs. | Required | | network_interface_name | The name of the network interface you want to view the details of. | Required | @@ -2600,7 +2506,6 @@ Gets the properties of a given network interface. Required permissions: Microsof | Azure.VirtualNetworks.NetworkInterfaces.name | String | The interface name. | | Azure.VirtualNetworks.NetworkInterfaces.location | String | The interface location. | | Azure.VirtualNetworks.NetworkInterfaces.id | String | The interface ID. | -| Azure.VirtualNetworks.NetworkInterfaces.location | String | The Azure region where the network interface is deployed. | | Azure.VirtualNetworks.NetworkInterfaces.etag | String | A unique read-only string that changes whenever the network interface resource is updated. | | Azure.VirtualNetworks.NetworkInterfaces.kind | String | The type of the network interface resource, used for Azure internal classification. | | Azure.VirtualNetworks.NetworkInterfaces.properties.tapConfigurations | String | The list of tap configurations attached to the network interface for traffic mirroring. | @@ -2623,7 +2528,7 @@ Gets the properties of a given public IP address. Permissions required: Microsof | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group to which the IP address belongs. | Required | | address_name | The IP address name. | Required | @@ -2659,7 +2564,7 @@ Updates container properties as specified in request body. Properties not mentio | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | | container_name | The container name. | Required | @@ -2692,7 +2597,7 @@ Create a new Blob under the specified Container. Required permissions: Microsoft | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | | container_name | The name of the Blob Container. | Required | @@ -2716,7 +2621,7 @@ Retrieve a Blob from a Container. Required permissions: Microsoft.Storage/storag | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | | container_name | The name of the Blob Container. | Required | @@ -2734,7 +2639,7 @@ Retrieve a Blob from a Container. Required permissions: Microsoft.Storage/storag | File.EntryID | String | The entry ID of the file. | | File.Info | String | File information. | | File.Type | String | The file type. | -| File.MD5 | Unknown | The MD5 hash of the file. | +| File.MD5 | String | The MD5 hash of the file. | | File.Extension | String | The file extension. | ### azure-storage-blob-tag-set @@ -2753,7 +2658,7 @@ Updates the tags for the specified Blob. The command can replace the entire tags | container_name | The name of the Blob Container. | Required | | blob_name | The name of the blob. | Required | | tags | Tags fields in JSON format: {"tag-name-1": "tag-value-1", "tag-name-2": "tag-value-2"}. The tags fields may contain at most 10 tags. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | | append | Whether to append tags to the existing tags or not. Default is false. Possible values are: true, false. | Optional | @@ -2777,7 +2682,7 @@ Set Blob properties. Required permissions: Microsoft.Storage/storageAccounts/blo | --- | --- | --- | | container_name | The name of the Blob Container. | Required | | blob_name | The name of the blob. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | | content_type | Blob content type. Indicates the media type of the blob. | Optional | @@ -2807,7 +2712,7 @@ Updates the configuration settings of an existing Azure Web App. Required permis | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | name | Name of the Web App. | Required | | http20_enabled | Configures a web site to allow clients to connect over http2.0. Possible values are: true, false. | Optional | | remote_debugging_enabled | True if remote debugging is enabled; otherwise, false. Possible values are: true, false. | Optional | @@ -2860,7 +2765,7 @@ Updates the authentication and authorization settings of an existing Azure Web A | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | name | Name of the Web App. | Required | | enabled | True if the Authentication / Authorization feature is enabled for the current app; otherwise, false. Possible values are: true, false. | Optional | @@ -2915,7 +2820,7 @@ Updates a disk. Required permissions: Microsoft.Compute/disks/read, Microsoft.Co | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | disk_name | The name of the managed disk that is being created. Supported characters for the name are a-z, A-Z, 0-9, _ and -. The maximum name length is 80 characters. | Required | | public_network_access | Policy for controlling export on the disk. Possible values are: Disabled, Enabled. | Optional | | network_access_policy | Policy for accessing the disk via network. Possible values are: AllowAll, AllowPrivate, DenyAll. | Optional | @@ -2979,7 +2884,7 @@ Updates an Azure Web App. The required permissions are: Microsoft.Web/sites/read | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | name | Name of the Web App. | Required | | identity_type | Managed service identity type. Possible values are: None, SystemAssigned. | Optional | | https_only | Configures the web site to accept only https requests. Possible values are: true, false. | Optional | @@ -3028,7 +2933,7 @@ Updates a container registry. Required permissions: Microsoft.ContainerRegistry/ | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | Subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | registry_name | The name of the container registry. | Required | | allow_exports | Whether artifacts can be exported. Possible values are: disabled, enabled. | Optional | | public_network_access | Whether public network access is allowed for the container registry. Possible values are: disabled, enabled. | Optional | @@ -3076,7 +2981,7 @@ Run this command to get the blob service properties of a specific account storag | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | resource_group_name | The resource group name. | Required | | account_name | The storage account name. | Required | @@ -3112,7 +3017,7 @@ Sets properties for the blob service in a specific account storage. Required per | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | resource_group_name | The resource group name. | Required | -| subscription_id | The subscription ID. | Required | +| subscription_id | The Azure subscription ID. Required for Cortex Platform (which includes Cortex XSIAM version >=3.0 and Cortex Cloud). Optional for Cortex XSOAR and Cortex XSIAM version < 3.0, where it can be retrieved from the integration configuration. | Optional | | account_name | The name of the storage account. | Required | | delete_retention_policy_enabled | Whether DeleteRetentionPolicy is enabled. Possible values are: true, false. | Optional | | delete_retention_policy_days | The number of days the deleted item should be retained. | Optional | @@ -3134,3 +3039,88 @@ Sets properties for the blob service in a specific account storage. Required per | Azure.Storage.BlobServices.properties.restorePolicy | Unknown | The blob service properties for blob restore policy. | | Azure.Storage.BlobServices.sku.name | String | The SKU name. | | Azure.Storage.BlobServices.sku.tier | String | The SKU tier. | + +### azure-auth-test + +*** +Tests the connectivity to Azure. Relevant for the Device Code and Authorization Code authentication flows (used instead of the Test button for these interactive flows). + +#### Base Command + +`azure-auth-test` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +There is no context output for this command. + +### azure-generate-login-url + +*** +Generates the login URL used for authorization. Relevant only for the Authorization Code authentication flow. + +#### Base Command + +`azure-generate-login-url` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +There is no context output for this command. + +### azure-auth-complete + +*** +Completes the authorization process for the Device Code flow. Should be used after running the azure-auth-start command. + +#### Base Command + +`azure-auth-complete` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +There is no context output for this command. + +### azure-auth-reset + +*** +Resets the authentication process. Relevant for the Device Code and Authorization Code authentication flows. + +#### Base Command + +`azure-auth-reset` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +There is no context output for this command. + +### azure-auth-start + +*** +Starts the authorization process for the Device Code / Authorization Code flows. Follow the instructions in the command results. + +#### Base Command + +`azure-auth-start` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +There is no context output for this command. diff --git a/Packs/Azure/ReleaseNotes/1_2_0.md b/Packs/Azure/ReleaseNotes/1_2_0.md new file mode 100644 index 000000000000..d45b9a24e7c0 --- /dev/null +++ b/Packs/Azure/ReleaseNotes/1_2_0.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Azure + +- Added support for installing the **Azure** content pack in the **Cortex XSOAR** and **Cortex XSIAM** Marketplaces (with version < 3.0), including a connection validation on **Test** for the Client Credentials, Authorization Code, and Azure Managed Identities authentication types (for Authorization Code, provide the Authorization code parameter before clicking Test; for Azure Managed Identities, the connection is validated using a lightweight resource groups request). diff --git a/Packs/Azure/pack_metadata.json b/Packs/Azure/pack_metadata.json index ed4ec56ce533..1812447c1261 100644 --- a/Packs/Azure/pack_metadata.json +++ b/Packs/Azure/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Azure", "description": "Microsoft Azure", "support": "xsoar", - "currentVersion": "1.1.23", + "currentVersion": "1.2.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", @@ -13,7 +13,9 @@ "useCases": [], "keywords": [], "marketplaces": [ - "platform" + "platform", + "xsoar", + "marketplacev2" ], "supportedModules": [ "cloud_posture", diff --git a/Packs/AzureDataExplorer/Integrations/AzureDataExplorer/AzureDataExplorer.yml b/Packs/AzureDataExplorer/Integrations/AzureDataExplorer/AzureDataExplorer.yml index fb2da3832e37..fda87131760f 100644 --- a/Packs/AzureDataExplorer/Integrations/AzureDataExplorer/AzureDataExplorer.yml +++ b/Packs/AzureDataExplorer/Integrations/AzureDataExplorer/AzureDataExplorer.yml @@ -448,7 +448,7 @@ script: - description: Generate the login url used for Authorization code flow. name: azure-data-explorer-generate-login-url arguments: [] - dockerimage: demisto/auth-utils:1.0.0.3562326 + dockerimage: demisto/auth-utils:1.0.0.10133006 runonce: false script: "-" subtype: python3 diff --git a/Packs/AzureDataExplorer/ReleaseNotes/1_3_18.md b/Packs/AzureDataExplorer/ReleaseNotes/1_3_18.md new file mode 100644 index 000000000000..d7cd8c64626c --- /dev/null +++ b/Packs/AzureDataExplorer/ReleaseNotes/1_3_18.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Azure Data Explorer + +- Updated the Docker image to: *demisto/auth-utils:1.0.0.10133006*. + diff --git a/Packs/AzureDataExplorer/pack_metadata.json b/Packs/AzureDataExplorer/pack_metadata.json index 1c2143a66d00..6dd98d510852 100644 --- a/Packs/AzureDataExplorer/pack_metadata.json +++ b/Packs/AzureDataExplorer/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Azure Data Explorer", "description": "Use Azure Data Explorer integration to collect and analyze data inside clusters of Azure Data Explorer and manage search queries.", "support": "xsoar", - "currentVersion": "1.3.17", + "currentVersion": "1.3.18", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/AzureSecurityCenter/Integrations/AzureSecurityCenter_v2/AzureSecurityCenter_v2.yml b/Packs/AzureSecurityCenter/Integrations/AzureSecurityCenter_v2/AzureSecurityCenter_v2.yml index cbcb18e34267..23a9e4f11840 100644 --- a/Packs/AzureSecurityCenter/Integrations/AzureSecurityCenter_v2/AzureSecurityCenter_v2.yml +++ b/Packs/AzureSecurityCenter/Integrations/AzureSecurityCenter_v2/AzureSecurityCenter_v2.yml @@ -454,7 +454,7 @@ script: type: Unknown - contextPath: Azure.ResourceGroupName.properties.provisioningState description: Resource group provisioning state. - dockerimage: demisto/crypto:1.0.0.5490413 + dockerimage: demisto/crypto:1.0.0.10120494 runonce: false script: '-' type: python diff --git a/Packs/AzureSecurityCenter/ReleaseNotes/2_0_58.md b/Packs/AzureSecurityCenter/ReleaseNotes/2_0_58.md new file mode 100644 index 000000000000..f0124be19603 --- /dev/null +++ b/Packs/AzureSecurityCenter/ReleaseNotes/2_0_58.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Microsoft Defender for Cloud + +- Updated the Docker image to: *demisto/crypto:1.0.0.10120494*. + diff --git a/Packs/AzureSecurityCenter/pack_metadata.json b/Packs/AzureSecurityCenter/pack_metadata.json index d826cd1a293f..4b3d1a79fd72 100644 --- a/Packs/AzureSecurityCenter/pack_metadata.json +++ b/Packs/AzureSecurityCenter/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Microsoft Defender for Cloud", "description": "Unified security management and advanced threat protection across hybrid cloud workloads.", "support": "xsoar", - "currentVersion": "2.0.57", + "currentVersion": "2.0.58", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/Base/ReleaseNotes/1_42_10.md b/Packs/Base/ReleaseNotes/1_42_10.md new file mode 100644 index 000000000000..335a65b7e346 --- /dev/null +++ b/Packs/Base/ReleaseNotes/1_42_10.md @@ -0,0 +1,19 @@ + +#### Scripts + +##### DrawRelatedIncidentsCanvas + +- Updated the Docker image to: *demisto/sklearn:1.0.0.10866354*. + +##### DBotFindSimilarIncidents + +- Updated the Docker image to: *demisto/sklearn:1.0.0.10866354*. + +##### DBotFindSimilarIncidentsByIndicators + +- Updated the Docker image to: *demisto/sklearn:1.0.0.10866354*. + +##### FindSimilarIncidentsByText + +- Updated the Docker image to: *demisto/sklearn:1.0.0.10866354*. + diff --git a/Packs/Base/ReleaseNotes/1_42_11.md b/Packs/Base/ReleaseNotes/1_42_11.md new file mode 100644 index 000000000000..f9d1a02c7913 --- /dev/null +++ b/Packs/Base/ReleaseNotes/1_42_11.md @@ -0,0 +1,7 @@ + +#### Scripts + + +##### GetIndicatorsByQuery + +- Updated the Docker image to: *demisto/python3:3.12.13.10116658*. diff --git a/Packs/Base/ReleaseNotes/1_42_12.md b/Packs/Base/ReleaseNotes/1_42_12.md new file mode 100644 index 000000000000..b109e198b6f4 --- /dev/null +++ b/Packs/Base/ReleaseNotes/1_42_12.md @@ -0,0 +1,3 @@ +## Base + +Documentation and metadata improvements. diff --git a/Packs/Base/ReleaseNotes/1_42_9.md b/Packs/Base/ReleaseNotes/1_42_9.md new file mode 100644 index 000000000000..7b6a8abe0991 --- /dev/null +++ b/Packs/Base/ReleaseNotes/1_42_9.md @@ -0,0 +1,7 @@ + +#### Scripts + +##### CheckDockerImageAvailable + +- Updated the Docker image to: *demisto/python3:3.12.13.10404775*. + diff --git a/Packs/Base/Scripts/CheckDockerImageAvailable/CheckDockerImageAvailable.yml b/Packs/Base/Scripts/CheckDockerImageAvailable/CheckDockerImageAvailable.yml index e89376841202..c268746dde6b 100644 --- a/Packs/Base/Scripts/CheckDockerImageAvailable/CheckDockerImageAvailable.yml +++ b/Packs/Base/Scripts/CheckDockerImageAvailable/CheckDockerImageAvailable.yml @@ -31,4 +31,4 @@ tests: - CheckDockerImageAvailableTest runas: DBotWeakRole fromversion: 5.0.0 -dockerimage: demisto/python3:3.12.13.9059085 +dockerimage: demisto/python3:3.12.13.10404775 diff --git a/Packs/Base/Scripts/DBotFindSimilarIncidents/DBotFindSimilarIncidents.yml b/Packs/Base/Scripts/DBotFindSimilarIncidents/DBotFindSimilarIncidents.yml index f8135181e2c1..1f5e44bfe05f 100644 --- a/Packs/Base/Scripts/DBotFindSimilarIncidents/DBotFindSimilarIncidents.yml +++ b/Packs/Base/Scripts/DBotFindSimilarIncidents/DBotFindSimilarIncidents.yml @@ -86,7 +86,7 @@ script: '-' subtype: python3 timeout: '0' type: python -dockerimage: demisto/sklearn:1.0.0.10120494 +dockerimage: demisto/sklearn:1.0.0.10866354 runas: DBotWeakRole tests: - DBotFindSimilarIncidents-test diff --git a/Packs/Base/Scripts/DBotFindSimilarIncidentsByIndicators/DBotFindSimilarIncidentsByIndicators.yml b/Packs/Base/Scripts/DBotFindSimilarIncidentsByIndicators/DBotFindSimilarIncidentsByIndicators.yml index a42620546d17..760182ef0d85 100644 --- a/Packs/Base/Scripts/DBotFindSimilarIncidentsByIndicators/DBotFindSimilarIncidentsByIndicators.yml +++ b/Packs/Base/Scripts/DBotFindSimilarIncidentsByIndicators/DBotFindSimilarIncidentsByIndicators.yml @@ -42,7 +42,7 @@ script: '-' subtype: python3 timeout: '0' type: python -dockerimage: demisto/sklearn:1.0.0.8425368 +dockerimage: demisto/sklearn:1.0.0.10866354 runas: DBotWeakRole tests: - DBotFindSimilarIncidentsByIndicators - Test diff --git a/Packs/Base/Scripts/DrawRelatedIncidentsCanvas/DrawRelatedIncidentsCanvas.yml b/Packs/Base/Scripts/DrawRelatedIncidentsCanvas/DrawRelatedIncidentsCanvas.yml index 0a6f7dea8e6a..f5d86da2ec36 100644 --- a/Packs/Base/Scripts/DrawRelatedIncidentsCanvas/DrawRelatedIncidentsCanvas.yml +++ b/Packs/Base/Scripts/DrawRelatedIncidentsCanvas/DrawRelatedIncidentsCanvas.yml @@ -34,7 +34,7 @@ script: '-' subtype: python3 timeout: '0' type: python -dockerimage: demisto/sklearn:1.0.0.10120494 +dockerimage: demisto/sklearn:1.0.0.10866354 runas: DBotWeakRole tests: - No tests (auto formatted) diff --git a/Packs/Base/Scripts/FindSimilarIncidentsByText/FindSimilarIncidentsByText.yml b/Packs/Base/Scripts/FindSimilarIncidentsByText/FindSimilarIncidentsByText.yml index f1a63d068315..f7489c558c37 100644 --- a/Packs/Base/Scripts/FindSimilarIncidentsByText/FindSimilarIncidentsByText.yml +++ b/Packs/Base/Scripts/FindSimilarIncidentsByText/FindSimilarIncidentsByText.yml @@ -78,7 +78,7 @@ tags: - incidents timeout: '0' type: python -dockerimage: demisto/sklearn:1.0.0.49796 +dockerimage: demisto/sklearn:1.0.0.10866354 runonce: true tests: - No test diff --git a/Packs/Base/Scripts/GetIndicatorsByQuery/GetIndicatorsByQuery.yml b/Packs/Base/Scripts/GetIndicatorsByQuery/GetIndicatorsByQuery.yml index a8d04b1e0c6e..60c4d0f28c8e 100644 --- a/Packs/Base/Scripts/GetIndicatorsByQuery/GetIndicatorsByQuery.yml +++ b/Packs/Base/Scripts/GetIndicatorsByQuery/GetIndicatorsByQuery.yml @@ -29,7 +29,7 @@ tags: - ml timeout: '0' type: python -dockerimage: demisto/python3:3.12.13.8428455 +dockerimage: demisto/python3:3.12.13.10116658 runas: DBotWeakRole fromversion: 5.5.0 tests: diff --git a/Packs/Base/pack_metadata.json b/Packs/Base/pack_metadata.json index 17ab0daa1ee8..9fa618f272ff 100644 --- a/Packs/Base/pack_metadata.json +++ b/Packs/Base/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Base", "description": "The base pack for Cortex XSOAR.", "support": "xsoar", - "currentVersion": "1.42.8", + "currentVersion": "1.42.12", "author": "Cortex XSOAR", "serverMinVersion": "6.0.0", "url": "https://www.paloaltonetworks.com/cortex", diff --git a/Packs/BeyondTrust_Password_Safe/ModelingRules/BeyondTrust_Password_Safe/BeyondTrust_Password_Safe.xif b/Packs/BeyondTrust_Password_Safe/ModelingRules/BeyondTrust_Password_Safe/BeyondTrust_Password_Safe.xif index 060e8fee5fa0..3151421c3c23 100644 --- a/Packs/BeyondTrust_Password_Safe/ModelingRules/BeyondTrust_Password_Safe/BeyondTrust_Password_Safe.xif +++ b/Packs/BeyondTrust_Password_Safe/ModelingRules/BeyondTrust_Password_Safe/BeyondTrust_Password_Safe.xif @@ -80,10 +80,12 @@ alter // Extract raw data (https://www.beyondtrust.com/docs/beyondinsight-passwo request_id = arrayindex(regextract(details, "(?:Request \#|ReleaseRequestId=)(\w+)"), 0), target_application = arrayindex(regextract(details, "Application=(\w+)"), 0), target_account = coalesce( - arrayindex(regextract(event_target, "Account\:(\S+)"), 0), + arrayindex(regextract(event_target, "Account[:=](\S+)"), 0), arrayindex(regextract(_raw_log, "Username:\s\"?(\w.*?)\"?(?:,|\s{4}|\t)"), 0), arrayindex(regextract(_raw_log, "Account\s?Name:\s\"?(\w.*?)\"?(?:,|\s{4}|\t)"), 0)), - target_asset = arrayindex(regextract(event_target, "Asset(?:\=|\:)(\S+)"), 0), + target_asset = coalesce( + arrayindex(regextract(event_target, "Asset(?:\=|\:)(\S+)"), 0), + arrayindex(regextract(event_target, "ManagedSystem\=(\S+)"),0)), target_netbios_name = arrayindex(regextract(message, "NetBiosName=([^,]+)"), 0), user_domain = coalesce(arrayindex(regextract(user, "(.+)\\.+"), 0), arrayindex(split(user, "@"), 1), arrayindex(split(email, "@"), 1)), user_name = coalesce(arrayindex(regextract(user, "\\(.+)"), 0), arrayindex(regextract(user, "(.+)\@"), 0), user), diff --git a/Packs/BeyondTrust_Password_Safe/ReleaseNotes/1_1_14.md b/Packs/BeyondTrust_Password_Safe/ReleaseNotes/1_1_14.md new file mode 100644 index 000000000000..2837b614da3c --- /dev/null +++ b/Packs/BeyondTrust_Password_Safe/ReleaseNotes/1_1_14.md @@ -0,0 +1,6 @@ + +#### Modeling Rules + +##### BeyondTrust Password Safe Modeling Rule + +Improved the target account and target asset extraction to support additional value formats. diff --git a/Packs/BeyondTrust_Password_Safe/pack_metadata.json b/Packs/BeyondTrust_Password_Safe/pack_metadata.json index a4f4ca4f6a84..fec510796ceb 100644 --- a/Packs/BeyondTrust_Password_Safe/pack_metadata.json +++ b/Packs/BeyondTrust_Password_Safe/pack_metadata.json @@ -2,7 +2,7 @@ "name": "BeyondTrust Password Safe", "description": "Unified password and session management for seamless accountability and control over privileged accounts.", "support": "xsoar", - "currentVersion": "1.1.13", + "currentVersion": "1.1.14", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/BloodHoundEnterprise/Integrations/BloodHoundEnterprise/BloodHoundEnterprise.yml b/Packs/BloodHoundEnterprise/Integrations/BloodHoundEnterprise/BloodHoundEnterprise.yml index 9c02ea1bdf85..f44a74b75496 100644 --- a/Packs/BloodHoundEnterprise/Integrations/BloodHoundEnterprise/BloodHoundEnterprise.yml +++ b/Packs/BloodHoundEnterprise/Integrations/BloodHoundEnterprise/BloodHoundEnterprise.yml @@ -1,3 +1,5 @@ +name: BloodHoundEnterprise +display: BloodHoundEnterprise category: Analytics & SIEM provider: Specter Ops Inc sectionorder: @@ -39,9 +41,8 @@ configuration: type: 8 section: Connect advanced: true -description: Use this integration to fetch audit logs from BloodHound Enterprise as events in Cortex XSIAM. -display: BloodHoundEnterprise -name: BloodHoundEnterprise +description: Deprecated. Use the SpecterOps BloodHound Enterprise integration instead. Use this integration to fetch audit logs from BloodHound Enterprise as events in Cortex XSIAM. +deprecated: true script: commands: - arguments: diff --git a/Packs/BloodHoundEnterprise/README.md b/Packs/BloodHoundEnterprise/README.md index 18d58ff50711..f8fd4f2e5695 100644 --- a/Packs/BloodHoundEnterprise/README.md +++ b/Packs/BloodHoundEnterprise/README.md @@ -1,3 +1,7 @@ +**Note: This pack is deprecated. Use the *SpecterOps BloodHound Enterprise* pack instead.** + +--- + Note: Support for this Pack will be moved to Partner starting October 20, 2025. # BloodHound Enterprise diff --git a/Packs/BloodHoundEnterprise/ReleaseNotes/1_0_11.md b/Packs/BloodHoundEnterprise/ReleaseNotes/1_0_11.md new file mode 100644 index 000000000000..41503e332403 --- /dev/null +++ b/Packs/BloodHoundEnterprise/ReleaseNotes/1_0_11.md @@ -0,0 +1,4 @@ + +#### Integrations +##### BloodHoundEnterprise +- Deprecated. Use the *SpecterOps BloodHound Enterprise* integration instead. diff --git a/Packs/BloodHoundEnterprise/pack_metadata.json b/Packs/BloodHoundEnterprise/pack_metadata.json index 66bae0560eb0..15971f943dab 100644 --- a/Packs/BloodHoundEnterprise/pack_metadata.json +++ b/Packs/BloodHoundEnterprise/pack_metadata.json @@ -1,8 +1,9 @@ { - "name": "BloodHound Enterprise", - "description": "BloodHound Enterprise identifies and remediates Active Directory vulnerabilities to enhance security posture.", + "name": "BloodHound Enterprise (Deprecated)", + "hidden": true, + "description": "Deprecated. Use the SpecterOps BloodHound Enterprise pack instead. BloodHound Enterprise identifies and remediates Active Directory vulnerabilities to enhance security posture.", "support": "partner", - "currentVersion": "1.0.10", + "currentVersion": "1.0.11", "author": "SpecterOps", "url": "https://bloodhound.specterops.io/", "email": "sales@specterops.io", @@ -11,7 +12,11 @@ ], "tags": [], "useCases": [], - "keywords": [], + "created": "2026-07-17T00:00:00Z", + "keywords": [ + "SpecterOps", + "BloodHound" + ], "marketplaces": [ "marketplacev2", "platform" diff --git a/Packs/Box/Integrations/BoxEventsCollector/BoxEventsCollector.yml b/Packs/Box/Integrations/BoxEventsCollector/BoxEventsCollector.yml index 5d51bea7a591..c6501a0c540c 100644 --- a/Packs/Box/Integrations/BoxEventsCollector/BoxEventsCollector.yml +++ b/Packs/Box/Integrations/BoxEventsCollector/BoxEventsCollector.yml @@ -58,7 +58,7 @@ script: defaultValue: 3 days description: Get events. name: box-get-events - dockerimage: demisto/auth-utils:1.0.0.3562326 + dockerimage: demisto/auth-utils:1.0.0.10133006 runonce: false script: '-' subtype: python3 diff --git a/Packs/Box/Integrations/BoxV2/BoxV2.yml b/Packs/Box/Integrations/BoxV2/BoxV2.yml index 8cba2612bd4e..7d035661a856 100644 --- a/Packs/Box/Integrations/BoxV2/BoxV2.yml +++ b/Packs/Box/Integrations/BoxV2/BoxV2.yml @@ -2512,7 +2512,7 @@ script: - contextPath: Box.Folder.item_status description: The status of the parent of the item. type: String - dockerimage: demisto/auth-utils:1.0.0.3609876 + dockerimage: demisto/auth-utils:1.0.0.10133006 isfetch: true runonce: false script: '-' diff --git a/Packs/Box/ReleaseNotes/3_2_20.md b/Packs/Box/ReleaseNotes/3_2_20.md new file mode 100644 index 000000000000..fb4f583061be --- /dev/null +++ b/Packs/Box/ReleaseNotes/3_2_20.md @@ -0,0 +1,11 @@ + +#### Integrations + +##### Box Event Collector + +- Updated the Docker image to: *demisto/auth-utils:1.0.0.10133006*. + +##### Box v2 + +- Updated the Docker image to: *demisto/auth-utils:1.0.0.10133006*. + diff --git a/Packs/Box/pack_metadata.json b/Packs/Box/pack_metadata.json index ee3d2fd1f42e..f16380233e94 100644 --- a/Packs/Box/pack_metadata.json +++ b/Packs/Box/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Box", "description": "Manage Box users", "support": "xsoar", - "currentVersion": "3.2.19", + "currentVersion": "3.2.20", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/CTM360-CyberBlindspot/.pack-ignore b/Packs/CTM360-CyberBlindspot/.pack-ignore index c0981027878b..9755f43aac6f 100644 --- a/Packs/CTM360-CyberBlindspot/.pack-ignore +++ b/Packs/CTM360-CyberBlindspot/.pack-ignore @@ -10,6 +10,8 @@ ignore=IF115 ignore=IF115 [file:incidentfields-Module.json] ignore=IF115 +[file:incidentfields-Scan_Type.json] +ignore=IF115 [file:playbook-CyberBlindspot_Incident_Management_V2.yml] ignore=PB106 @@ -20,6 +22,9 @@ ignore=PB106 [file:playbook-CyberBlindspot_Retrieve_Incident_Screenshots.yml] ignore=PB106 +[file:playbook-HackerView_Incident_Management_V2.yml] +ignore=PB106 + [known_words] D lf diff --git a/Packs/CTM360-CyberBlindspot/.secrets-ignore b/Packs/CTM360-CyberBlindspot/.secrets-ignore index 4e273a11744a..e318167029f5 100644 --- a/Packs/CTM360-CyberBlindspot/.secrets-ignore +++ b/Packs/CTM360-CyberBlindspot/.secrets-ignore @@ -9,6 +9,16 @@ https://platform.ctm360.com 242.230.133.23 http://172.17.0.1 172.17.0.1 +172.17.20.4 +http://172.17.20.4 +3.121.143.46 +https://some.site.tld +http://some.site.tld +203.0.113.10 +203.0.113.11 +203.0.113.20 +203.0.113.21 +203.0.113.30 acid@hackthisexample.local sam@hackthisexample.local 440 diff --git a/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-CyberBlindspot.json b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-CyberBlindspot.json index 2886d5dd668e..dceb1772687d 100644 --- a/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-CyberBlindspot.json +++ b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-CyberBlindspot.json @@ -54,6 +54,87 @@ "CTM360 CyberBlindspot Incident Type": { "simple": "CustomFields.cbs_type" }, + "CTM360 CyberBlindspot Incident Status": { + "simple": "CustomFields.incident_status" + }, + "CTM360 CyberBlindspot GS Enrichment": { + "simple": "CustomFields.enrichment" + }, + "CTM360 CyberBlindspot GS External Links": { + "simple": "CustomFields.external_links" + }, + "CTM360 CyberBlindspot GS Finding ID": { + "simple": "CustomFields.finding_id" + }, + "CTM360 CyberBlindspot GS Internal Links": { + "simple": "CustomFields.internal_links" + }, + "CTM360 CyberBlindspot GS Landing URL": { + "simple": "CustomFields.landing_url" + }, + "CTM360 CyberBlindspot GS Resolving IP": { + "simple": "CustomFields.resolving_ip" + }, + "CTM360 CyberBlindspot GS Scan Status": { + "simple": "CustomFields.scan_status" + }, + "CTM360 CyberBlindspot GS Submitted URL": { + "simple": "CustomFields.submitted_url" + }, + "CTM360 CyberBlindspot GS Status Code": { + "simple": "CustomFields.status_code" + }, + "CTM360 CyberBlindspot GS Tags": { + "simple": "CustomFields.tags" + }, + "CTM360 CyberBlindspot GS Title": { + "simple": "CustomFields.title" + }, + "CTM360 CyberBlindspot GS URL": { + "simple": "CustomFields.url" + }, + "CTM360 CyberBlindspot GS URL Status": { + "simple": "CustomFields.url_status" + }, + "CTM360 CyberBlindspot MM Account Identifier": { + "simple": "CustomFields.account_identifier" + }, + "CTM360 CyberBlindspot MM BIC": { + "simple": "CustomFields.bic" + }, + "CTM360 CyberBlindspot MM Bank Account Country": { + "simple": "CustomFields.bank_account_country" + }, + "CTM360 CyberBlindspot MM Bank Account Holder Name": { + "simple": "CustomFields.bank_account_holder_name" + }, + "CTM360 CyberBlindspot MM Bank Name": { + "simple": "CustomFields.bank_name" + }, + "CTM360 CyberBlindspot MM Money Mule ID": { + "simple": "CustomFields.money_mule_id" + }, + "CTM360 CyberBlindspot MM Suspect Emails": { + "simple": "CustomFields.suspect_emails" + }, + "CTM360 CyberBlindspot MM Suspect Names": { + "simple": "CustomFields.suspect_names" + }, + "CTM360 CyberBlindspot MM Suspect Phones": { + "simple": "CustomFields.suspect_phones" + }, + "CTM360 CyberBlindspot MM Transfer Amount": { + "simple": "CustomFields.transfer_amount" + }, + "CTM360 CyberBlindspot MM Transfer Currency": { + "simple": "CustomFields.transfer_currency" + }, + "CTM360 CyberBlindspot SMF Platform": { + "simple": "CustomFields.platform" + }, + "CTM360 CyberBlindspot SMF Risk Score": { + "simple": "CustomFields.risk_score" + }, "CTM360 CyberBlindspot Last Seen": { "simple": "CustomFields.cbs_updated_date" }, diff --git a/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json index 9a1663502116..69e90eb6eab7 100644 --- a/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json +++ b/Packs/CTM360-CyberBlindspot/Classifiers/classifier-mapper-incoming-CTM360-HackerView.json @@ -12,6 +12,51 @@ "CTM360 HackerView Asset Type": { "simple": "CustomFields.asset_type" }, + "CTM360 HackerView Brand": { + "simple": "CustomFields.brand" + }, + "CTM360 HackerView DS Asset": { + "simple": "CustomFields.asset" + }, + "CTM360 HackerView DS CPE": { + "simple": "CustomFields.cpe" + }, + "CTM360 HackerView DS CVSS Metrics": { + "simple": "CustomFields.cvss_metrics" + }, + "CTM360 HackerView DS CVSS Score": { + "simple": "CustomFields.cvss_score" + }, + "CTM360 HackerView DS Domain": { + "simple": "CustomFields.domain" + }, + "CTM360 HackerView DS EPSS Score": { + "simple": "CustomFields.epss_score" + }, + "CTM360 HackerView DS Evidence Request": { + "simple": "CustomFields.evidence.request" + }, + "CTM360 HackerView DS Evidence Response": { + "simple": "CustomFields.evidence.response" + }, + "CTM360 HackerView DS Evidence cURL Command": { + "simple": "CustomFields.evidence.curl_command" + }, + "CTM360 HackerView DS IP": { + "simple": "CustomFields.ip" + }, + "CTM360 HackerView DS Issue Description": { + "simple": "CustomFields.issue_description" + }, + "CTM360 HackerView DS Issue Type": { + "simple": "CustomFields.issue_type" + }, + "CTM360 HackerView DS Known Exploited": { + "simple": "CustomFields.known_exploited" + }, + "CTM360 HackerView DS Port": { + "simple": "CustomFields.port" + }, "CTM360 HackerView Potential Attack Types": { "simple": "CustomFields.potential_attack_type" }, @@ -21,9 +66,24 @@ "CTM360 HackerView Technologies": { "simple": "CustomFields.technologies" }, - "CVE ID": { - "simple": "CustomFields.cve_id" - }, + "CTM360 HackerView DS Progress Status": { + "simple": "CustomFields.progress_status" + }, + "CTM360 HackerView DS Ticket ID": { + "simple": "CustomFields.id" + }, + "CTM360 HackerView DS URI": { + "simple": "CustomFields.uri" + }, + "CTM360 HackerView DS URL": { + "simple": "CustomFields.url" + }, + "CTM360 HackerView Scan Type": { + "simple": "CustomFields.hv_module" + }, + "CVE ID": { + "simple": "CustomFields.cve_id" + }, "Categories": { "simple": "CustomFields.issue_category" }, diff --git a/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-CyberBlindspot.json b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-CyberBlindspot.json index 02ed470d2037..7e45387a01ab 100644 --- a/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-CyberBlindspot.json +++ b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-CyberBlindspot.json @@ -110,7 +110,7 @@ "prevName": "Active Incidents - Pie chart", "dataType": "incidents", "widgetType": "pie", - "query": "-category:job and -status:archived and -status:closed", + "query": "-category:job and -status:archived and -status:closed type:\"CyberBlindspot Incident\"", "isPredefined": true, "dateRange": { "fromDate": "0001-01-01T00:00:00Z", @@ -169,7 +169,7 @@ "prevName": "Active Incidents - Pie chart", "dataType": "incidents", "widgetType": "pie", - "query": "-category:job and -status:archived and -status:closed", + "query": "-category:job and -status:archived and -status:closed type:\"CyberBlindspot Incident\"", "isPredefined": false, "dateRange": { "fromDate": "0001-01-01T00:00:00Z", @@ -602,7 +602,7 @@ "prevName": "Active Incidents - Pie chart", "dataType": "incidents", "widgetType": "pie", - "query": "-category:job and -status:archived and -status:closed", + "query": "-category:job and -status:archived and -status:closed and type:\"CyberBlindspot Incident\"", "isPredefined": false, "dateRange": { "fromDate": "0001-01-01T00:00:00Z", diff --git a/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView_DeepScan.json b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView_DeepScan.json new file mode 100644 index 000000000000..cda430e47c5b --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Dashboards/dashboard-HackerView_DeepScan.json @@ -0,0 +1,1533 @@ +{ + "id": "HackerView Deep Scan Dashboard", + "description": "Dashboard for HackerView Deep Scan module metrics", + "isPredefined": true, + "version": -1, + "fromVersion": "6.10.0", + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z", + "name": "HackerView Deep Scan Dashboard", + "layout": [ + { + "id": "82290230-9808-11ee-be91-bd9f71ef874f", + "forceRange": false, + "x": 0, + "y": 6, + "i": "82290230-9808-11ee-be91-bd9f71ef874f", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2023-12-10T11:20:59.026314772+03:00", + "created": "2023-12-10T11:20:52.129395461+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.39", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents - Line chart", + "prevName": "Active Incidents - Line chart", + "dataType": "incidents", + "widgetType": "line", + "query": "-category:job and -status:archived and -status:closed type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "occurred(d)", + "rawType" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "db208da0-aec8-11ee-82fa-dd707358474c", + "forceRange": false, + "x": 0, + "y": 0, + "i": "db208da0-aec8-11ee-82fa-dd707358474c", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "ctm360hackerviewscantype" + ], + "showGraphValues": false + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "32d91b60-aeca-11ee-82fa-dd707358474c", + "forceRange": false, + "x": 0, + "y": 4, + "i": "32d91b60-aeca-11ee-82fa-dd707358474c", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Incidents Pending Action - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "customGroupBy": [ + { + "New": { + "conditions": [ + [ + { + "field": "externalstatus", + "operator": "isEqualCaseString", + "right": "New", + "type": "string" + } + ] + ], + "name": "New" + }, + "Other": { + "name": "Other" + } + } + ], + "groupBy": [ + "externalstatus" + ], + "showGraphValues": false, + "showOthers": true + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "e6f69960-aef7-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 0, + "y": 2, + "i": "e6f69960-aef7-11ee-ac47-ef32c7f05a79", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Active", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "4a71f9d0-aef8-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 0, + "y": 3, + "i": "4a71f9d0-aef8-11ee-ac47-ef32c7f05a79", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.273909715+03:00", + "created": "2024-01-07T08:36:16.821190202+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Running playbooks", + "prevName": "Running playbooks", + "dataType": "incidents", + "widgetType": "number", + "query": "runStatus:running playbook:\"HackerView Incident Management\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": null, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 0 + }, + "#FAC100": { + "value": 5 + }, + "#FF1B15": { + "value": 20 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "7d10a7b0-aef8-11ee-ac47-ef32c7f05a79", + "forceRange": false, + "x": 6, + "y": 2, + "i": "7d10a7b0-aef8-11ee-ac47-ef32c7f05a79", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Closed Light Scan Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Closed and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "8c864490-af78-11ee-9dec-eb27c047d071", + "forceRange": false, + "x": 4, + "y": 9, + "i": "8c864490-af78-11ee-9dec-eb27c047d071", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.27343997+03:00", + "created": "2024-01-07T08:36:16.820119695+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "size": 5, + "Cache": null, + "name": "Top Active Light Scan Playbooks (Past Week)", + "prevName": "Top Active Playbooks", + "dataType": "incidents", + "widgetType": "column", + "query": "status:active -category:job and type:\"HackerView Incident\" and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "playbookId" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "0a967ed0-af7a-11ee-9dec-eb27c047d071", + "forceRange": false, + "x": 2, + "y": 3, + "i": "0a967ed0-af7a-11ee-9dec-eb27c047d071", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274785325+03:00", + "created": "2024-01-07T08:36:16.823354842+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "6.2.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Avg. runtime for Playbooks (Past Week)", + "prevName": "Average runtime for Playbooks", + "dataType": "metrics", + "widgetType": "duration", + "query": "type:playbook and incidentType:\"HackerView Incident\"", + "isPredefined": false, + "description": "Average runtime for Playbooks", + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "keys": [ + "avg|duration" + ], + "tableColumns": [], + "valuesFormat": "minutes" + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "1ec8eff0-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 4, + "i": "1ec8eff0-5c20-11f1-8ce8-df797597718e", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Deep Scan Incidents Pending Action - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "customGroupBy": [ + { + "New": { + "conditions": [ + [ + { + "field": "externalstatus", + "operator": "isEqualCaseString", + "right": "New", + "type": "string" + } + ] + ], + "name": "New" + }, + "Other": { + "name": "Other" + } + } + ], + "groupBy": [ + "externalstatus" + ], + "showGraphValues": false, + "showOthers": true + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "251618b0-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 0, + "i": "251618b0-5c20-11f1-8ce8-df797597718e", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Deep Scan Incidents - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and type:\"HackerView Incident\" and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "severity" + ], + "showGraphValues": false + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "27662330-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 9, + "i": "27662330-5c20-11f1-8ce8-df797597718e", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.27343997+03:00", + "created": "2024-01-07T08:36:16.820119695+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "size": 5, + "Cache": null, + "name": "Top DeepScan Active Playbooks (Past Week)", + "prevName": "Top Active Playbooks", + "dataType": "incidents", + "widgetType": "column", + "query": "status:active -category:job and type:\"HackerView Incident\" and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "playbookId" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "2a09f300-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 4, + "y": 6, + "i": "2a09f300-5c20-11f1-8ce8-df797597718e", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2023-12-10T11:20:59.026314772+03:00", + "created": "2023-12-10T11:20:52.129395461+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.39", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Light Scan Incidents - Line chart", + "prevName": "Active Incidents - Line chart", + "dataType": "incidents", + "widgetType": "line", + "query": "-category:job and -status:archived and -status:closed type:\"HackerView Incident\" and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "occurred(d)", + "rawType" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "36810510-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 4, + "y": 2, + "i": "36810510-5c20-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Light Scan Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Active and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "3aaa1550-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 4, + "y": 3, + "i": "3aaa1550-5c20-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.273909715+03:00", + "created": "2024-01-07T08:36:16.821190202+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Running Light Scan playbooks", + "prevName": "Running playbooks", + "dataType": "incidents", + "widgetType": "number", + "query": "runStatus:running playbook:\"HackerView Incident Management\" and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": null, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 0 + }, + "#FAC100": { + "value": 5 + }, + "#FF1B15": { + "value": 20 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "3db50b60-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 2, + "y": 2, + "i": "3db50b60-5c20-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Closed Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Closed", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "407feb30-5c20-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 6, + "y": 3, + "i": "407feb30-5c20-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274785325+03:00", + "created": "2024-01-07T08:36:16.823354842+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "6.2.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Avg. runtime for Light Scan Playbooks (Past Week)", + "prevName": "Average runtime for Playbooks", + "dataType": "metrics", + "widgetType": "duration", + "query": "type:playbook and incidentType:\"HackerView Incident\" and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "description": "Average runtime for Playbooks", + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "keys": [ + "avg|duration" + ], + "tableColumns": [], + "valuesFormat": "minutes" + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "ee7139f0-5c21-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 2, + "i": "ee7139f0-5c21-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Deep Scan Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Active and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "1230d670-5c22-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 10, + "y": 2, + "i": "1230d670-5c22-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274656973+03:00", + "created": "2024-01-07T08:36:16.823054025+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Closed Deep Scan Incidents (Past Week)", + "prevName": "Incidents in Error Run Status", + "dataType": "incidents", + "widgetType": "number", + "query": "-runStatus:error type:\"HackerView Incident\" and status:Closed and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 10 + }, + "#FAC100": { + "value": 20 + }, + "#FF1B15": { + "value": 40 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "344b7580-5c22-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 3, + "i": "344b7580-5c22-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.273909715+03:00", + "created": "2024-01-07T08:36:16.821190202+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Running Deep Scan playbooks", + "prevName": "Running playbooks", + "dataType": "incidents", + "widgetType": "number", + "query": "runStatus:running playbook:\"HackerView Incident Management\" and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": null, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "colors": { + "isEnabled": false, + "items": { + "#00CD33": { + "value": 0 + }, + "#FAC100": { + "value": 5 + }, + "#FF1B15": { + "value": 20 + } + }, + "type": "above" + } + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "619f7680-5c22-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 10, + "y": 3, + "i": "619f7680-5c22-11f1-8ce8-df797597718e", + "w": 2, + "h": 1, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274785325+03:00", + "created": "2024-01-07T08:36:16.823354842+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "6.2.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Avg. runtime for Deep Scan Playbooks (Past Week)", + "prevName": "Average runtime for Playbooks", + "dataType": "metrics", + "widgetType": "duration", + "query": "type:playbook and incidentType:\"HackerView Incident\" and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "description": "Average runtime for Playbooks", + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "keys": [ + "avg|duration" + ], + "tableColumns": [], + "valuesFormat": "minutes" + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "846f0db0-5c22-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 4, + "y": 4, + "i": "846f0db0-5c22-11f1-8ce8-df797597718e", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Light Scan Incidents Pending Action - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "customGroupBy": [ + { + "New": { + "conditions": [ + [ + { + "field": "externalstatus", + "operator": "isEqualCaseString", + "right": "New", + "type": "string" + } + ] + ], + "name": "New" + }, + "Other": { + "name": "Other" + } + } + ], + "groupBy": [ + "externalstatus" + ], + "showGraphValues": false, + "showOthers": true + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "93e7c700-5c22-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 4, + "y": 0, + "i": "93e7c700-5c22-11f1-8ce8-df797597718e", + "w": 4, + "h": 2, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.274425938+03:00", + "created": "2024-01-07T08:36:16.822326133+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Light Scan Incidents - Pie chart", + "prevName": "Active Incidents - Pie chart", + "dataType": "incidents", + "widgetType": "pie", + "query": "-category:job and -status:archived and -status:closed and type:\"HackerView Incident\" and -ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "severity" + ], + "showGraphValues": false + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "4c6a6920-5c2a-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 8, + "y": 6, + "i": "4c6a6920-5c2a-11f1-8ce8-df797597718e", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2023-12-10T11:20:59.026314772+03:00", + "created": "2023-12-10T11:20:52.129395461+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.39", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "Cache": null, + "name": "Active Deep Scan Incidents - Line chart", + "prevName": "Active Incidents - Line chart", + "dataType": "incidents", + "widgetType": "line", + "query": "-category:job and -status:archived and -status:closed type:\"HackerView Incident\" and ctm360hackerviewscantype:deepscan", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "occurred(d)", + "rawType" + ] + }, + "category": "" + }, + "reflectDimensions": true + }, + { + "id": "5235b7b0-5c2a-11f1-8ce8-df797597718e", + "forceRange": false, + "x": 0, + "y": 9, + "i": "5235b7b0-5c2a-11f1-8ce8-df797597718e", + "w": 4, + "h": 3, + "widget": { + "id": "", + "version": 0, + "cacheVersn": 0, + "modified": "2024-01-07T08:36:21.27343997+03:00", + "created": "2024-01-07T08:36:16.820119695+03:00", + "sizeInBytes": 0, + "packID": "CommonWidgets", + "packName": "Common Widgets", + "itemVersion": "1.2.44", + "fromServerVersion": "5.0.0", + "toServerVersion": "", + "propagationLabels": [], + "packPropagationLabels": [ + "all" + ], + "definitionId": "", + "vcShouldKeepItemLegacyProdMachine": false, + "size": 5, + "Cache": null, + "name": "Top Active Playbooks (Past Week)", + "prevName": "Top Active Playbooks", + "dataType": "incidents", + "widgetType": "column", + "query": "status:active -category:job and type:\"HackerView Incident\"", + "isPredefined": false, + "dateRange": { + "fromDate": "0001-01-01T00:00:00Z", + "toDate": "0001-01-01T00:00:00Z", + "period": { + "by": "", + "byTo": "", + "byFrom": "days", + "toValue": null, + "fromValue": 7, + "field": "" + }, + "fromDateLicense": "0001-01-01T00:00:00Z" + }, + "params": { + "groupBy": [ + "playbookId" + ] + }, + "category": "" + }, + "reflectDimensions": true + } + ] +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Asset.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Asset.json new file mode 100644 index 000000000000..3f105f13fe7d --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Asset.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_asset", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Asset", + "ownerOnly": false, + "description": "Affected asset (e.g. host:port).", + "cliName": "ctm360hackerviewdsasset", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CPE.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CPE.json new file mode 100644 index 000000000000..4906ac78ba6f --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CPE.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_cpe", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS CPE", + "ownerOnly": false, + "description": "Common Platform Enumeration (CPE) identifier.", + "cliName": "ctm360hackerviewdscpe", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Metrics.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Metrics.json new file mode 100644 index 000000000000..91c9bb9c2bf7 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Metrics.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_cvssmetrics", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS CVSS Metrics", + "ownerOnly": false, + "description": "CVSS vector or metric string.", + "cliName": "ctm360hackerviewdscvssmetrics", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Score.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Score.json new file mode 100644 index 000000000000..f035722b874a --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_CVSS_Score.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_cvssscore", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS CVSS Score", + "ownerOnly": false, + "description": "CVSS base score for the vulnerability.", + "cliName": "ctm360hackerviewdscvssscore", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Domain.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Domain.json new file mode 100644 index 000000000000..ef899b935117 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Domain.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_domain", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Domain", + "ownerOnly": false, + "description": "Domain associated with the asset.", + "cliName": "ctm360hackerviewdsdomain", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_EPSS_Score.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_EPSS_Score.json new file mode 100644 index 000000000000..3cf85fe3c80a --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_EPSS_Score.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_epssscore", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS EPSS Score", + "ownerOnly": false, + "description": "Exploit Prediction Scoring System (EPSS) score.", + "cliName": "ctm360hackerviewdsepssscore", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Curl_Command.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Curl_Command.json new file mode 100644 index 000000000000..fc33dc821ab1 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Curl_Command.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_evidencecurlcommand", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Evidence cURL Command", + "ownerOnly": false, + "description": "cURL command to reproduce the finding.", + "cliName": "ctm360hackerviewdsevidencecurlcommand", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Request.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Request.json new file mode 100644 index 000000000000..0dea069d4ae0 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Request.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_evidencerequest", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Evidence Request", + "ownerOnly": false, + "description": "HTTP request evidence for the finding.", + "cliName": "ctm360hackerviewdsevidencerequest", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Response.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Response.json new file mode 100644 index 000000000000..b4d453a0dd94 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Evidence_Response.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_evidenceresponse", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Evidence Response", + "ownerOnly": false, + "description": "HTTP response evidence for the finding.", + "cliName": "ctm360hackerviewdsevidenceresponse", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_IP.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_IP.json new file mode 100644 index 000000000000..0c8a42f200f9 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_IP.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_ip", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS IP", + "ownerOnly": false, + "description": "IP address associated with the finding.", + "cliName": "ctm360hackerviewdsip", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Description.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Description.json new file mode 100644 index 000000000000..69af0d1613f0 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Description.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_issuedescription", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Issue Description", + "ownerOnly": false, + "description": "Detailed description of the issue.", + "cliName": "ctm360hackerviewdsissuedescription", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Type.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Type.json new file mode 100644 index 000000000000..68d1d1f38177 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Issue_Type.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_issuetype", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Issue Type", + "ownerOnly": false, + "description": "Type or classification of the issue.", + "cliName": "ctm360hackerviewdsissuetype", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Known_Exploited.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Known_Exploited.json new file mode 100644 index 000000000000..dd11cad965d5 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Known_Exploited.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_knownexploited", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Known Exploited", + "ownerOnly": false, + "description": "Whether the vulnerability is known to be exploited.", + "cliName": "ctm360hackerviewdsknownexploited", + "type": "boolean", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Port.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Port.json new file mode 100644 index 000000000000..ce4f0fb39a8b --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Port.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_port", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Port", + "ownerOnly": false, + "description": "Network port associated with the finding.", + "cliName": "ctm360hackerviewdsport", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Progress_Status.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Progress_Status.json new file mode 100644 index 000000000000..6a713c6fead8 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Progress_Status.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_progressstatus", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Progress Status", + "ownerOnly": false, + "description": "Progress of incident response on the platform.", + "cliName": "ctm360hackerviewdsprogressstatus", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Ticket_ID.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Ticket_ID.json new file mode 100644 index 000000000000..e11e4eac27f5 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_Ticket_ID.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_ticketid", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS Ticket ID", + "ownerOnly": false, + "description": "HackerView ticket identifier (e.g. HVI-\u2026).", + "cliName": "ctm360hackerviewdsticketid", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URI.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URI.json new file mode 100644 index 000000000000..3b32306f40ea --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URI.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_uri", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS URI", + "ownerOnly": false, + "description": "URI path or resource related to the finding.", + "cliName": "ctm360hackerviewdsuri", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URL.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URL.json new file mode 100644 index 000000000000..102a37b3b48b --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-DS_URL.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_ds_url", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView DS URL", + "ownerOnly": false, + "description": "Base URL of the affected asset or service.", + "cliName": "ctm360hackerviewdsurl", + "type": "url", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Enrichment.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Enrichment.json new file mode 100644 index 000000000000..ce85ececdbb1 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Enrichment.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsenrichment", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Enrichment", + "ownerOnly": false, + "description": "DNS enrichment payload.", + "cliName": "ctm360cyberblindspotgsenrichment", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_External_Links.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_External_Links.json new file mode 100644 index 000000000000..5045395b9311 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_External_Links.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsexternallinks", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS External Links", + "ownerOnly": false, + "description": "External links discovered.", + "cliName": "ctm360cyberblindspotgsexternallinks", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Finding_ID.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Finding_ID.json new file mode 100644 index 000000000000..a2de8285f573 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Finding_ID.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsfindingid", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Finding ID", + "ownerOnly": false, + "description": "CBS gambling-site finding ID.", + "cliName": "ctm360cyberblindspotgsfindingid", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Internal_Links.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Internal_Links.json new file mode 100644 index 000000000000..f5749bdefb93 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Internal_Links.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsinternallinks", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Internal Links", + "ownerOnly": false, + "description": "Internal links discovered.", + "cliName": "ctm360cyberblindspotgsinternallinks", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Landing_URL.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Landing_URL.json new file mode 100644 index 000000000000..8193c7a7ad26 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Landing_URL.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gslandingurl", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Landing URL", + "ownerOnly": false, + "description": "Landing page URL observed for the site.", + "cliName": "ctm360cyberblindspotgslandingurl", + "type": "url", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Resolving_IP.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Resolving_IP.json new file mode 100644 index 000000000000..543854fa0e8e --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Resolving_IP.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsresolvingip", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Resolving IP", + "ownerOnly": false, + "description": "Resolved IP for the site.", + "cliName": "ctm360cyberblindspotgsresolvingip", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Scan_Status.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Scan_Status.json new file mode 100644 index 000000000000..2b08321ec3ab --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Scan_Status.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsscanstatus", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Scan Status", + "ownerOnly": false, + "description": "Scan completion status.", + "cliName": "ctm360cyberblindspotgsscanstatus", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Status_Code.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Status_Code.json new file mode 100644 index 000000000000..499d57db40be --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Status_Code.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsstatuscode", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Status Code", + "ownerOnly": false, + "description": "HTTP status code from scan.", + "cliName": "ctm360cyberblindspotgsstatuscode", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Submitted_URL.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Submitted_URL.json new file mode 100644 index 000000000000..7d408aae6133 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Submitted_URL.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gssubmittedurl", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Submitted URL", + "ownerOnly": false, + "description": "URL submitted to CBS for scanning.", + "cliName": "ctm360cyberblindspotgssubmittedurl", + "type": "url", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Tags.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Tags.json new file mode 100644 index 000000000000..f2069e7f9bbe --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Tags.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gstags", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Tags", + "ownerOnly": false, + "description": "Tags applied to the finding.", + "cliName": "ctm360cyberblindspotgstags", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Title.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Title.json new file mode 100644 index 000000000000..2993522e4987 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_Title.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gstitle", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS Title", + "ownerOnly": false, + "description": "Page title observed during scan.", + "cliName": "ctm360cyberblindspotgstitle", + "type": "longText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL.json new file mode 100644 index 000000000000..64e0e14fbb2a --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsurl", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS URL", + "ownerOnly": false, + "description": "Primary gambling site URL.", + "cliName": "ctm360cyberblindspotgsurl", + "type": "url", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL_Status.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL_Status.json new file mode 100644 index 000000000000..a8b920fc1511 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-GS_URL_Status.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_gsurlstatus", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot GS URL Status", + "ownerOnly": false, + "description": "URL reachability status.", + "cliName": "ctm360cyberblindspotgsurlstatus", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-HV_Brand.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-HV_Brand.json new file mode 100644 index 000000000000..c962afafd456 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-HV_Brand.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_brand", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Brand", + "ownerOnly": false, + "description": "The organization brand the incident belongs to.", + "cliName": "ctm360hackerviewbrand", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Incident_Status.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Incident_Status.json new file mode 100644 index 000000000000..c72c4120da2f --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Incident_Status.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_incidentstatus", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot Incident Status", + "ownerOnly": false, + "description": "Platform-specific incident status (distinct from status).", + "cliName": "ctm360cyberblindspotincidentstatus", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Account_Identifier.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Account_Identifier.json new file mode 100644 index 000000000000..87b1348450a1 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Account_Identifier.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmaccountidentifier", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Account Identifier", + "ownerOnly": false, + "description": "Account identifier tied to the mule.", + "cliName": "ctm360cyberblindspotmmaccountidentifier", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_BIC.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_BIC.json new file mode 100644 index 000000000000..b9bf705a2d7f --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_BIC.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmbic", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM BIC", + "ownerOnly": false, + "description": "Bank Identifier Code.", + "cliName": "ctm360cyberblindspotmmbic", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Country.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Country.json new file mode 100644 index 000000000000..6228a89efbeb --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Country.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmbankaccountcountry", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Bank Account Country", + "ownerOnly": false, + "description": "Country of the bank account.", + "cliName": "ctm360cyberblindspotmmbankaccountcountry", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Holder_Name.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Holder_Name.json new file mode 100644 index 000000000000..cdb78378e0a0 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Account_Holder_Name.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmbankaccountholdername", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Bank Account Holder Name", + "ownerOnly": false, + "description": "Name on the bank account.", + "cliName": "ctm360cyberblindspotmmbankaccountholdername", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Name.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Name.json new file mode 100644 index 000000000000..60870c59f83c --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Bank_Name.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmbankname", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Bank Name", + "ownerOnly": false, + "description": "Bank name tied to the mule.", + "cliName": "ctm360cyberblindspotmmbankname", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Money_Mule_ID.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Money_Mule_ID.json new file mode 100644 index 000000000000..411cc1739179 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Money_Mule_ID.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmmoneymuleid", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Money Mule ID", + "ownerOnly": false, + "description": "CBS money mule finding ID.", + "cliName": "ctm360cyberblindspotmmmoneymuleid", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Emails.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Emails.json new file mode 100644 index 000000000000..ea415c8c700f --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Emails.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmsuspectemails", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Suspect Emails", + "ownerOnly": false, + "description": "Email addresses associated with the money mule.", + "cliName": "ctm360cyberblindspotmmsuspectemails", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Names.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Names.json new file mode 100644 index 000000000000..67a22acb2867 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Names.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmsuspectnames", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Suspect Names", + "ownerOnly": false, + "description": "Names associated with the money mule.", + "cliName": "ctm360cyberblindspotmmsuspectnames", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Phones.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Phones.json new file mode 100644 index 000000000000..522c462b6326 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Suspect_Phones.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmsuspectphones", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Suspect Phones", + "ownerOnly": false, + "description": "Phone numbers associated with the money mule.", + "cliName": "ctm360cyberblindspotmmsuspectphones", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Amount.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Amount.json new file mode 100644 index 000000000000..2fd2d3055f38 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Amount.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmtransferamount", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Transfer Amount", + "ownerOnly": false, + "description": "Transfer amount when present.", + "cliName": "ctm360cyberblindspotmmtransferamount", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Currency.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Currency.json new file mode 100644 index 000000000000..d284afee282e --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-MM_Transfer_Currency.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_mmtransfercurrency", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot MM Transfer Currency", + "ownerOnly": false, + "description": "Currency code for the transfer.", + "cliName": "ctm360cyberblindspotmmtransfercurrency", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Platform.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Platform.json new file mode 100644 index 000000000000..57ef9ccf4ed9 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Platform.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_smfplatform", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot SMF Platform", + "ownerOnly": false, + "description": "Social network platform (e.g. Twitter).", + "cliName": "ctm360cyberblindspotsmfplatform", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Risk_Score.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Risk_Score.json new file mode 100644 index 000000000000..f579d18d64b9 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-SMF_Risk_Score.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_cyberblindspot_smfriskscore", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 CyberBlindspot SMF Risk Score", + "ownerOnly": false, + "description": "Numeric risk score from CBS.", + "cliName": "ctm360cyberblindspotsmfriskscore", + "type": "number", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "CyberBlindspot Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": true, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} diff --git a/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Scan_Type.json b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Scan_Type.json new file mode 100644 index 000000000000..7f6bfb4ea32a --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/IncidentFields/incidentfields-Scan_Type.json @@ -0,0 +1,31 @@ +{ + "id": "incident_ctm360_hackerview_scantype", + "fromVersion": "6.10.0", + "version": -1, + "name": "CTM360 HackerView Scan Type", + "ownerOnly": false, + "description": "Scan type of the incident.", + "cliName": "ctm360hackerviewscantype", + "type": "shortText", + "closeForm": false, + "editForm": true, + "required": false, + "neverSetAsRequired": false, + "isReadOnly": false, + "useAsKpi": false, + "locked": false, + "system": false, + "content": true, + "group": 0, + "hidden": false, + "openEnded": false, + "associatedTypes": [ + "HackerView Incident" + ], + "associatedToAll": false, + "unmapped": false, + "unsearchable": false, + "caseInsensitive": true, + "sla": 0, + "threshold": 72 +} \ No newline at end of file diff --git a/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json b/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json index 641e8f3d022e..69fef8cf11fb 100644 --- a/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json +++ b/Packs/CTM360-CyberBlindspot/IncidentTypes/HackerView_Incident.json @@ -7,7 +7,7 @@ "name": "HackerView Incident", "prevName": "HackerView Incident", "color": "#9A1C90", - "playbookId": "HackerView Incident Management", + "playbookId": "HackerView Incident Management V2", "hours": 0, "days": 0, "weeks": 0, diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.py b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.py index 690e7473d440..34d1646d2a26 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.py +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.py @@ -1,11 +1,11 @@ +from typing import Any + import demistomock as demisto # noqa: F401 +import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa -import urllib3 -from typing import Any - """ IMPORTS """ from inspect import getfullargspec @@ -37,23 +37,37 @@ } LOGGING_PREFIX = "[CYBER-BLINDSPOT]" +CBS_MODULE_DISPLAY_TO_TYPE = { + "Incidents": "incidents", + "Compromised Cards": "compromised_cards", + "Breached Credentials": "breached_credentials", + "Malware Logs": "malware_logs", + "Domain Infringement": "domain_infringement", + "Subdomain Infringement": "subdomain_infringement", + "Social Media Fraud": "social_media_fraud", + "Gambling Sites": "gambling_sites", + "Money Mules": "money_mules", +} +CBS_DEFAULT_MODULE_DISPLAY = "Incidents" +CBS_DEFAULT_MODULE_TYPE = "incidents" + DEFAULT_FIELDS = [ - {"name": "first_seen", "description": "The creation date of the incident"}, - {"name": "last_seen", "description": "The date the incident got last updated"}, - {"name": "timestamp", "description": "The timestamp of when the record was created"}, - {"name": "brand", "description": "The organization the incident belongs to"}, - {"name": "status", "description": "Incident's current state of affairs"}, - {"name": "severity", "description": "The severity of the incident"}, - {"name": "remarks", "description": "Remarks about the incident"}, - {"name": "type", "description": "Incident type"}, - {"name": "id", "description": "Unique ID for the incident record"}, - {"name": "external_link", "description": "External link to the remote platform"}, + {"name": "first_seen", "description": "The creation date of the incident."}, + {"name": "last_seen", "description": "The date the incident was last updated."}, + {"name": "timestamp", "description": "The timestamp of when the record was created."}, + {"name": "brand", "description": "The organization the incident belongs to."}, + {"name": "status", "description": "The current state of affairs of the incident."}, + {"name": "severity", "description": "The severity of the incident."}, + {"name": "remarks", "description": "The remarks about the incident."}, + {"name": "type", "description": "The incident type."}, + {"name": "id", "description": "The unique ID for the incident record."}, + {"name": "external_link", "description": "The external link to the remote platform."}, ] CBS_INCIDENT_FIELDS = [ - {"name": "subject", "description": "Asset or title of incident"}, - {"name": "screenshots", "description": "The screenshot evidence if available"}, - {"name": "class", "description": "Subject class"}, - {"name": "coa", "description": "The possible course of action"}, + {"name": "subject", "description": "The asset or title of the incident."}, + {"name": "screenshots", "description": "The screenshot evidence if available."}, + {"name": "class", "description": "The subject class."}, + {"name": "coa", "description": "The possible course of action."}, *DEFAULT_FIELDS, ] @@ -67,7 +81,7 @@ CBS_MALWARE_LOG_FIELDS = [ {"name": "masked_password", "description": "The masked password related to the breached data."}, - {"name": "password", "description": "Password found in the breached data or compromised account."}, + {"name": "password", "description": "The password found in the breached data or compromised account."}, {"name": "software", "description": "The software related to the breached data."}, {"name": "user", "description": "The user related to the breached data."}, {"name": "user_domain", "description": "The domain of the user related to the breached data."}, @@ -89,10 +103,10 @@ CBS_CRED_FIELDS = [ {"name": "breach_source", "description": "The source of breached data."}, {"name": "domain", "description": "The domain related to the breached data."}, - {"name": "email", "description": "Email found in the breached data."}, - {"name": "username", "description": "Username found in the breached data."}, - {"name": "executive_name", "description": "Executive member's name related to the breached data."}, - {"name": "password", "description": "Password found in the breached data."}, + {"name": "email", "description": "The email found in the breached data."}, + {"name": "username", "description": "The username found in the breached data."}, + {"name": "executive_name", "description": "The executive member's name related to the breached data."}, + {"name": "password", "description": "The password found in the breached data."}, *DEFAULT_FIELDS, ] @@ -103,12 +117,65 @@ *DEFAULT_FIELDS, ] +CBS_SMF_FIELDS = [ + {"name": "platform", "description": "The social network platform for social media fraud findings."}, + {"name": "subject", "description": "The subject URL or profile link."}, + {"name": "risk_score", "description": "The numeric risk score from CBS."}, + {"name": "risks", "description": "The risk indicators associated with the finding."}, + {"name": "incident_status", "description": "The platform-specific incident status."}, + *DEFAULT_FIELDS, +] + +CBS_MM_FIELDS = [ + {"name": "money_mule_id", "description": "The CBS money mule finding ID."}, + {"name": "account_identifier", "description": "The account identifier tied to the money mule."}, + {"name": "suspect_names", "description": "The names associated with the money mule."}, + {"name": "suspect_emails", "description": "The email addresses associated with the money mule."}, + {"name": "suspect_phones", "description": "The phone numbers associated with the money mule."}, + {"name": "transfer_amount", "description": "The transfer amount when present."}, + {"name": "transfer_currency", "description": "The currency code for the transfer."}, + {"name": "bank_account_holder_name", "description": "The name on the bank account."}, + {"name": "bank_name", "description": "The bank name tied to the money mule."}, + {"name": "bank_account_country", "description": "The country of the bank account."}, + {"name": "bic", "description": "The Bank Identifier Code."}, + *DEFAULT_FIELDS, +] + +CBS_GS_FIELDS = [ + {"name": "finding_id", "description": "The CBS gambling-site finding ID."}, + {"name": "url", "description": "The primary gambling site URL."}, + {"name": "submitted_url", "description": "The URL submitted to CBS for scanning."}, + {"name": "landing_url", "description": "The landing page URL observed for the site."}, + {"name": "title", "description": "The page title observed during scan."}, + {"name": "resolving_ip", "description": "The resolved IP for the gambling site."}, + {"name": "tags", "description": "The tags applied to the gambling site finding."}, + {"name": "status_code", "description": "The HTTP status code from scan."}, + {"name": "url_status", "description": "The URL reachability status."}, + {"name": "scan_status", "description": "The scan completion status."}, + {"name": "enrichment", "description": "The DNS enrichment payload."}, + {"name": "external_links", "description": "The external links discovered on the gambling site."}, + {"name": "internal_links", "description": "The internal links discovered on the gambling site."}, + *DEFAULT_FIELDS, +] + MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}.get( demisto.params().get("mirror_direction", "None"), None ) +def resolve_cbs_module(module_to_use: str | None = None) -> str: + """Resolve API module_type from instance config. + + Pre-upgrade instances may omit module_to_use; treat missing/blank/unknown as Incidents. + """ + if module_to_use is None: + module_to_use = demisto.params().get("module_to_use", CBS_DEFAULT_MODULE_DISPLAY) + if not module_to_use or not str(module_to_use).strip(): + return CBS_DEFAULT_MODULE_TYPE + return CBS_MODULE_DISPLAY_TO_TYPE.get(module_to_use, CBS_DEFAULT_MODULE_TYPE) + + class Instance: def __init__(self, **kwargs) -> None: self.module: str = kwargs.get("module", "incidents") @@ -125,20 +192,17 @@ def __init__(self, **kwargs) -> None: self.mapping_fields = CBS_DOMAIN_INFRINGE_FIELDS case "subdomain_infringement": self.mapping_fields = CBS_DOMAIN_INFRINGE_FIELDS + case "social_media_fraud": + self.mapping_fields = CBS_SMF_FIELDS + case "money_mules": + self.mapping_fields = CBS_MM_FIELDS + case "gambling_sites": + self.mapping_fields = CBS_GS_FIELDS case _: self.mapping_fields = CBS_INCIDENT_FIELDS -INSTANCE = Instance( - module={ - "Incidents": "incidents", - "Compromised Cards": "compromised_cards", - "Breached Credentials": "breached_credentials", - "Malware Logs": "malware_logs", - "Domain Infringement": "domain_infringement", - "Subdomain Infringement": "subdomain_infringement", - }.get(demisto.params().get("module_to_use", "Incidents"), "incidents") -) +INSTANCE = Instance(module=resolve_cbs_module()) INTEGRATION_INSTANCE = demisto.integrationInstance() @@ -366,6 +430,28 @@ def convert_time_string( return "" +def normalize_timestamp(value: Any) -> Any: + """Normalize CBS record timestamp to epoch milliseconds. + + CBS modules may return timestamp as millis (int), numeric string, or ISO datetime string. + Fetch cursor logic requires a consistent numeric value for sorting and date_from. + """ + if value is None or value == "": + return value + if isinstance(value, bool): + return value + if isinstance(value, int | float): + return int(value) + if isinstance(value, str): + stripped = value.strip() + if stripped.isdigit(): + return int(stripped) + parsed = convert_time_string(stripped, "", timestamp=True, is_utc=True) + if isinstance(parsed, int): + return parsed + return value + + def deduplicate_and_create_incidents(fetched_incidents: List, last_run_incident_identifiers: List[str]) -> tuple[list, list]: """De-duplicates the fetched incidents and creates a list of actionable incidents. @@ -408,6 +494,7 @@ def map_and_create_incident(unmapped_incident: dict) -> dict: """ unmapped_incident.pop("screenshots", "") incident_id: str = unmapped_incident.pop("id", "") + mapped_severity = convert_to_demisto_severity(unmapped_incident.pop("severity", "low")) mapped_incident = { "name": unmapped_incident.pop("remarks", ""), "occurred": convert_time_string( @@ -415,7 +502,7 @@ def map_and_create_incident(unmapped_incident: dict) -> dict: ), "externalstatus": unmapped_incident.pop("status", "monitoring"), "externallink": unmapped_incident.pop("external_link", ""), - "severity": convert_to_demisto_severity(unmapped_incident.pop("severity", "low")), + "severity": mapped_severity, "CustomFields": { "cbs_type": unmapped_incident.pop("type", ""), "cbs_module": INSTANCE.module, @@ -426,6 +513,8 @@ def map_and_create_incident(unmapped_incident: dict) -> dict: **unmapped_incident, }, } + if "timestamp" in mapped_incident["CustomFields"]: + mapped_incident["CustomFields"]["timestamp"] = normalize_timestamp(mapped_incident["CustomFields"]["timestamp"]) if MIRROR_DIRECTION: mapped_incident["xsoar_mirroring"] = { "mirror_direction": MIRROR_DIRECTION, @@ -474,8 +563,6 @@ def test_module(client: Client, params) -> str: date_from = params.get("date_from", "") date_to = params.get("date_to", "") api_key = params.get("api_key", {}).get("password", "") - module_to_use = params.get("module_to_use", "") - if mirror_direction not in ["None", "Incoming", "Outgoing", "Incoming And Outgoing"]: log(INFO, 'Invalid "Mirror Direction" Value') raise DemistoException('Invalid "Mirroring Direction" Value') @@ -500,9 +587,7 @@ def test_module(client: Client, params) -> str: if not api_key: log(INFO, 'Invalid "API Key" Value') raise DemistoException('Invalid "API Key" Value') - if not module_to_use: - log(INFO, 'Invalid "Module" Value') - raise DemistoException('Invalid "Module" Value') + args["module_type"] = resolve_cbs_module(params.get("module_to_use")) incidents = client.test_configuration(args) if max_fetch and len(incidents) > max_fetch: log(INFO, f"Incidents fetched exceed the limit, removing the excess {len(incidents) - max_fetch} incidents.") @@ -558,7 +643,11 @@ def fetch_incidents( log(INFO, f"Received {len(incidents) - len(unique_incidents)} duplicates incidents to skip.") log(INFO, f"Calculated {len(incident_ids)} id(s).") - dates = sorted([d["CustomFields"]["timestamp"] for d in unique_incidents]) + dates = sorted( + normalized + for incident in unique_incidents + if (normalized := normalize_timestamp(incident["CustomFields"].get("timestamp"))) not in (None, "") + ) last_fetched_timestamp = dates[-1] if dates else last_run.get("last_fetched_timestamp") log(INFO, f"setting last fetched timestamp - {last_fetched_timestamp=}") @@ -566,6 +655,38 @@ def fetch_incidents( return next_run, unique_incidents +def build_fetch_params(demisto_params: dict[str, Any], last_run: dict[str, Any]) -> dict[str, Any]: + """Build API params for fetch-incidents.""" + last_fetched_timestamp = last_run.get("last_fetched_timestamp", "") + if last_fetched_timestamp not in ("", None): + last_fetched_timestamp = normalize_timestamp(last_fetched_timestamp) + first_fetch = demisto_params.get("first_fetch", "7 days") + try: + dateparser.parse(f"{first_fetch} UTC") + except Exception: + log(DEBUG, "first_fetch is not parsable, setting to `7 days`") + first_fetch = "7 days" + + if not last_fetched_timestamp: + log(DEBUG, f"Fetch is set to fetch from the {first_fetch} ago.") + + params: dict[str, Any] = { + "date_field": "@timestamp", + "order": "asc", + "max_hits": MAX_FETCH, + "module_type": INSTANCE.module, + "date_from": last_fetched_timestamp + if last_fetched_timestamp + else convert_time_string(f"{first_fetch} UTC", "", timestamp=True), + "t": datetime.now().timestamp() * 1000, + } + if "domain_infringement" in INSTANCE.module: + params["finding_status"] = demisto_params.get("finding_status", "") + params["risk_score_min"] = demisto_params.get("risk_score_min", "") + params["risk_score_max"] = demisto_params.get("risk_score_max", "") + return params + + def get_remote_data_command(client: Client, args: dict): """get-remote-data command: Returns an updated incident and error entry (if needed) @@ -746,7 +867,7 @@ def ctm360_cbs_details_command(client: Client, args: dict[str, Any]) -> CommandR result = client.fetch_incident(params) log(INFO, f"Received {result}") if result.get("timestamp", ""): - result["timestamp"] = str(result["timestamp"]) + result["timestamp"] = str(normalize_timestamp(result["timestamp"])) return CommandResults( outputs_prefix=INSTANCE.details_prefix, @@ -899,50 +1020,17 @@ def main() -> None: } if demisto_command == "fetch-incidents": - log(DEBUG, "at fetch command") + log(DEBUG, "at fetch-incidents command") last_run = demisto.getLastRun() - last_fetched_timestamp = last_run.get("last_fetched_timestamp", "") last_fetch_ids = last_run.get("last_fetch_ids", []) - first_fetch = demisto_params.get("first_fetch", "7 days") - try: - dateparser.parse(f"{first_fetch} UTC") - except Exception: - log(DEBUG, "first_fetch is not parsable, setting to `7 days`") - first_fetch = "7 days" - - if not last_fetched_timestamp: - log(DEBUG, f"Fetch is set to fetch incidents from the {first_fetch} ago.") - - params = { - "date_field": "@timestamp", - "order": "asc", - "max_hits": MAX_FETCH, - "module_type": INSTANCE.module, - "date_from": last_fetched_timestamp - if last_fetched_timestamp - else convert_time_string(f"{first_fetch} UTC", "", timestamp=True), - "t": datetime.now().timestamp() * 1000, - } - - if "domain_infringement" in INSTANCE.module: - params["finding_status"] = demisto_params.get("finding_status", "") - params["risk_score_min"] = demisto_params.get("risk_score_min", "") - params["risk_score_max"] = demisto_params.get("risk_score_max", "") - - log(DEBUG, f'{demisto_params.get("date_from")=}') - - log(INFO, f"Will be fetching {MAX_FETCH} incidents.") - - log(DEBUG, f'LastRun was {last_fetched_timestamp if last_fetched_timestamp else "NOT FOUND"}') - log(DEBUG, f'last run\'s calculated ids were {last_run.get("last_fetch_ids")}') + params = build_fetch_params(demisto_params, last_run) + log(INFO, f"Will be fetching up to {MAX_FETCH} records.") log(DEBUG, f"Calling fetch with the following: {params=}") log(DEBUG, f"Mirroring set as: {MIRROR_DIRECTION}") next_run, incidents = fetch_incidents(client, last_fetch_ids, params, last_run) - - log(DEBUG, "Setting incidents and last run") - log(DEBUG, f"Fetched {len(incidents)} incidents") + log(DEBUG, f"Fetched {len(incidents)} incidents, {next_run=}") demisto.setLastRun(next_run) demisto.incidents(incidents) elif demisto_command == "test-module": diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml index 86a8916883f9..03d57ca4aa82 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot.yml @@ -24,7 +24,7 @@ configuration: section: Collect - defaultvalue: Incidents display: Module To Use - additionalinfo: "Choose the module you want to use: Incidents, Compromised Cards, Breached Credentials, and Domain/Subdomain Infringements." + additionalinfo: "The module to use: Incidents, Compromised Cards, Breached Credentials, Malware Logs, Domain/Subdomain Infringements, Social Media Fraud, Gambling Sites, or Money Mules." name: module_to_use required: false type: 15 @@ -35,10 +35,10 @@ configuration: - Malware Logs - Domain Infringement - Subdomain Infringement + - Social Media Fraud + - Gambling Sites + - Money Mules section: Collect - hidden: - - marketplacev2 - - platform - name: retrieve_screenshots required: false type: 8 @@ -139,7 +139,7 @@ script: required: true outputs: - contextPath: CyberBlindspot.RemoteIncident.id - description: Unique ID for the incident record. + description: The unique ID for the incident record. - contextPath: CyberBlindspot.RemoteIncident.brand description: The organization the incident is associated with. - contextPath: CyberBlindspot.RemoteIncident.coa @@ -151,11 +151,11 @@ script: - contextPath: CyberBlindspot.RemoteIncident.severity description: The severity of the incident. - contextPath: CyberBlindspot.RemoteIncident.subject - description: Asset or title of incident. + description: The asset or title of the incident. - contextPath: CyberBlindspot.RemoteIncident.type - description: Incident type on remote server. + description: The incident type on the remote server. - contextPath: CyberBlindspot.RemoteIncident.remarks - description: Remarks about the incident. + description: The remarks about the incident. - contextPath: CyberBlindspot.RemoteIncident.created_date description: The creation date of the incident (legacy). - contextPath: CyberBlindspot.RemoteIncident.updated_date @@ -179,13 +179,13 @@ script: - contextPath: CyberBlindspot.RemoteIncident.domain description: The domain related to the breached data or compromised device. - contextPath: CyberBlindspot.RemoteIncident.email - description: Email found in the breached data. + description: The email found in the breached data. - contextPath: CyberBlindspot.RemoteIncident.username - description: Username found in the breached data. + description: The username found in the breached data. - contextPath: CyberBlindspot.RemoteIncident.password - description: Password found in the breached data or compromised account. + description: The password found in the breached data or compromised account. - contextPath: CyberBlindspot.RemoteIncident.executive_name - description: Executive member's name related to the breached data. + description: The executive member's name related to the breached data. - contextPath: CyberBlindspot.RemoteIncident.confirmation_time description: The time of infringement confirmation. - contextPath: CyberBlindspot.RemoteIncident.risks @@ -213,7 +213,7 @@ script: - contextPath: CyberBlindspot.RemoteIncident.user_domain description: The domain of the compromised user. - contextPath: CyberBlindspot.RemoteIncident.website - description: The website compromised. + description: The website that was compromised. - contextPath: CyberBlindspot.RemoteIncident.sources description: The sources reporting the compromise. - contextPath: CyberBlindspot.RemoteIncident.source_uri @@ -226,6 +226,58 @@ script: description: The external link to the remote platform. - contextPath: CyberBlindspot.RemoteIncident.compromise_details description: The details of the compromise. + - contextPath: CyberBlindspot.RemoteIncident.platform + description: The social network platform for social media fraud findings. + - contextPath: CyberBlindspot.RemoteIncident.risk_score + description: The numeric risk score from CBS. + - contextPath: CyberBlindspot.RemoteIncident.money_mule_id + description: The CBS money mule finding ID. + - contextPath: CyberBlindspot.RemoteIncident.account_identifier + description: The account identifier tied to the money mule. + - contextPath: CyberBlindspot.RemoteIncident.suspect_names + description: The names associated with the money mule. + - contextPath: CyberBlindspot.RemoteIncident.suspect_emails + description: The email addresses associated with the money mule. + - contextPath: CyberBlindspot.RemoteIncident.suspect_phones + description: The phone numbers associated with the money mule. + - contextPath: CyberBlindspot.RemoteIncident.transfer_amount + description: The transfer amount when present. + - contextPath: CyberBlindspot.RemoteIncident.transfer_currency + description: The currency code for the transfer. + - contextPath: CyberBlindspot.RemoteIncident.bank_account_holder_name + description: The name on the bank account. + - contextPath: CyberBlindspot.RemoteIncident.bank_name + description: The bank name tied to the money mule. + - contextPath: CyberBlindspot.RemoteIncident.bank_account_country + description: The country of the bank account. + - contextPath: CyberBlindspot.RemoteIncident.bic + description: The Bank Identifier Code. + - contextPath: CyberBlindspot.RemoteIncident.finding_id + description: The CBS gambling-site finding ID. + - contextPath: CyberBlindspot.RemoteIncident.url + description: The primary gambling site URL. + - contextPath: CyberBlindspot.RemoteIncident.submitted_url + description: The URL submitted to CBS for scanning. + - contextPath: CyberBlindspot.RemoteIncident.landing_url + description: The landing page URL observed for the site. + - contextPath: CyberBlindspot.RemoteIncident.title + description: The page title observed during scan. + - contextPath: CyberBlindspot.RemoteIncident.resolving_ip + description: The resolved IP for the gambling site. + - contextPath: CyberBlindspot.RemoteIncident.tags + description: The tags applied to the gambling site finding. + - contextPath: CyberBlindspot.RemoteIncident.status_code + description: The HTTP status code from scan. + - contextPath: CyberBlindspot.RemoteIncident.url_status + description: The URL reachability status. + - contextPath: CyberBlindspot.RemoteIncident.scan_status + description: The scan completion status. + - contextPath: CyberBlindspot.RemoteIncident.enrichment + description: The DNS enrichment payload. + - contextPath: CyberBlindspot.RemoteIncident.external_links + description: The external links discovered on the gambling site. + - contextPath: CyberBlindspot.RemoteIncident.internal_links + description: The internal links discovered on the gambling site. - name: ctm360-cbs-incident-retrieve-screenshots arguments: - description: The files to retrieve. @@ -278,7 +330,7 @@ script: script: '-' type: python subtype: python3 - dockerimage: demisto/python3:3.12.11.4095827 + dockerimage: demisto/python3:3.12.13.9059085 fromversion: 6.10.0 tests: - No tests (auto formatted) diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot_test.py b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot_test.py index 200c1af67c2e..b56b03128ad6 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot_test.py +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/CyberBlindspot_test.py @@ -1,20 +1,30 @@ -import pytest import logging from datetime import datetime -from dateparser import parse from unittest.mock import patch -from CommonServerPython import DemistoException, IncidentStatus, EntryType, CommandResults + +import pytest +from CommonServerPython import ( + CommandResults, + DemistoException, + EntryType, + IncidentSeverity, + IncidentStatus, +) from CyberBlindspot import ( - LOGGING_PREFIX, - CBS_INCOMING_DATE_FORMAT, - CBS_OUTGOING_DATE_FORMAT, ABSOLUTE_MAX_FETCH, - CBS_INCIDENT_FIELDS, CBS_CARD_FIELDS, CBS_CRED_FIELDS, CBS_DOMAIN_INFRINGE_FIELDS, + CBS_GS_FIELDS, + CBS_INCIDENT_FIELDS, + CBS_INCOMING_DATE_FORMAT, CBS_MALWARE_LOG_FIELDS, + CBS_MM_FIELDS, + CBS_OUTGOING_DATE_FORMAT, + CBS_SMF_FIELDS, + LOGGING_PREFIX, ) +from dateparser import parse """CONSTANTS""" BASE_URL = "https://example.com:443" @@ -67,6 +77,24 @@ "CyberBlindspot.RemoteIncident", CBS_MALWARE_LOG_FIELDS, ), + ( + "social_media_fraud", + "CyberBlindspot.IncidentList", + "CyberBlindspot.RemoteIncident", + CBS_SMF_FIELDS, + ), + ( + "money_mules", + "CyberBlindspot.IncidentList", + "CyberBlindspot.RemoteIncident", + CBS_MM_FIELDS, + ), + ( + "gambling_sites", + "CyberBlindspot.IncidentList", + "CyberBlindspot.RemoteIncident", + CBS_GS_FIELDS, + ), ] @@ -314,6 +342,23 @@ def test_convert_time_string(mock_input, mock_args, mock_asserts, capfd, caplog) ) +@pytest.mark.parametrize( + "value,expected", + [ + (1642607418000, 1642607418000), + ("1642607418000", 1642607418000), + ("2022-03-01T13:02:00", 1646139720000), + ("", ""), + (None, None), + ], +) +def test_normalize_timestamp(value, expected): + """ISO and numeric-string CBS timestamps must normalize to epoch milliseconds.""" + from CyberBlindspot import normalize_timestamp + + assert normalize_timestamp(value) == expected + + @pytest.mark.parametrize( "mock_input_file,mock_assert_file,mock_module", [ @@ -323,6 +368,9 @@ def test_convert_time_string(mock_input, mock_args, mock_asserts, capfd, caplog) ("fetch_domains_response_valid.json", "domain_list_cmd_result_valid.json", MODULES[3][0]), ("fetch_subdomains_response_valid.json", "subdomain_list_cmd_result_valid.json", MODULES[4][0]), ("fetch_malware_logs_response_valid.json", "malware_logs_list_cmd_result_valid.json", MODULES[5][0]), + ("fetch_social_media_fraud_response_valid.json", "social_media_fraud_list_cmd_result_valid.json", MODULES[6][0]), + ("fetch_money_mules_response_valid.json", "money_mules_list_cmd_result_valid.json", MODULES[7][0]), + ("fetch_gambling_sites_response_valid.json", "gambling_sites_list_cmd_result_valid.json", MODULES[8][0]), ], ) def test_map_and_create_incident(mock_input_file, mock_assert_file, mock_module): @@ -338,11 +386,11 @@ def test_map_and_create_incident(mock_input_file, mock_assert_file, mock_module) mock_fetched_incident = load_mock_response(mock_input_file)[0] mock_assert = load_mock_response(mock_assert_file)[0] - del mock_assert["rawJson"] + mock_assert.pop("rawJson", None) with patch("CyberBlindspot.INSTANCE.module", new=mock_module): result = map_and_create_incident(mock_fetched_incident) - del result["rawJson"] + result.pop("rawJson", None) logging.debug(result) logging.debug(mock_assert) assert result == mock_assert @@ -369,6 +417,15 @@ def test_map_and_create_incident(mock_input_file, mock_assert_file, mock_module) ("", ([], []), ([], []), MODULES[5][0]), ("fetch_malware_logs_response_valid.json", 2, ([], []), MODULES[5][0]), ("fetch_malware_logs_response_valid.json", -2, ([], []), MODULES[5][0]), + ("", ([], []), ([], []), MODULES[6][0]), + ("fetch_social_media_fraud_response_valid.json", 2, ([], []), MODULES[6][0]), + ("fetch_social_media_fraud_response_valid.json", -2, ([], []), MODULES[6][0]), + ("", ([], []), ([], []), MODULES[7][0]), + ("fetch_money_mules_response_valid.json", 2, ([], []), MODULES[7][0]), + ("fetch_money_mules_response_valid.json", -2, ([], []), MODULES[7][0]), + ("", ([], []), ([], []), MODULES[8][0]), + ("fetch_gambling_sites_response_valid.json", 2, ([], []), MODULES[8][0]), + ("fetch_gambling_sites_response_valid.json", -2, ([], []), MODULES[8][0]), ], ) def test_deduplicate_and_create_incidents( @@ -492,10 +549,6 @@ def test_test_configuration(mock_response, mock_client, mocker): DemistoException('Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")'), ), ({"mirror_direction": "None", "api_key": {"password": ""}}, DemistoException('Invalid "API Key" Value')), - ( - {"mirror_direction": "None", "api_key": {"password": "test"}, "module_to_use": ""}, - DemistoException('Invalid "Module" Value'), - ), ], ) def test_test_module(mock_params, mock_side_effect, mock_client, mocker): @@ -521,151 +574,72 @@ def test_test_module(mock_params, mock_side_effect, mock_client, mocker): assert str(e.value) == mock_side_effect.message +def test_test_module_ok_when_module_to_use_missing(mock_client, mocker): + """Upgraded instances without module_to_use should still test and fetch as Incidents.""" + from CyberBlindspot import test_module + + mock_test = mocker.patch.object(mock_client, "test_configuration", return_value=[]) + result = test_module( + mock_client, + {"mirror_direction": "None", "api_key": {"password": "test"}}, + ) + assert result == "ok" + assert mock_test.call_args[0][0]["module_type"] == "incidents" + + @pytest.mark.parametrize( - "mock_module,expected_mappings", + "module_to_use,expected", [ - ( - MODULES[0][0], - { - "CyberBlindspot Incident": { - "id": "Unique ID for the incident record", - "subject": "Asset or title of incident", - "severity": "The severity of the incident", - "type": "Incident type", - "class": "Subject class", - "status": "Incident's current state of affairs", - "coa": "The possible course of action", - "remarks": "Remarks about the incident", - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "screenshots": "The screenshot evidence if available", - "brand": "The organization the incident belongs to", - "timestamp": "The timestamp of when the record was created", - "external_link": "External link to the remote platform", - } - }, - ), - ( - MODULES[1][0], - { - "CyberBlindspot Incident": { - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "timestamp": "The timestamp of when the record was created", - "brand": "The organization the incident belongs to", - "status": "Incident's current state of affairs", - "severity": "The severity of the incident", - "remarks": "Remarks about the incident", - "type": "Incident type", - "id": "Unique ID for the incident record", - "external_link": "External link to the remote platform", - "card_number": "The compromised card's number.", - "cvv": "The compromised card's Card Verification Value (CVV).", - "expiry_month": "The compromised card's expiration month.", - "expiry_year": "The compromised card's expiration year.", - } - }, - ), - ( - MODULES[2][0], - { - "CyberBlindspot Incident": { - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "timestamp": "The timestamp of when the record was created", - "brand": "The organization the incident belongs to", - "status": "Incident's current state of affairs", - "severity": "The severity of the incident", - "remarks": "Remarks about the incident", - "type": "Incident type", - "id": "Unique ID for the incident record", - "external_link": "External link to the remote platform", - "breach_source": "The source of breached data.", - "domain": "The domain related to the breached data.", - "email": "Email found in the breached data.", - "username": "Username found in the breached data.", - "executive_name": "Executive member's name related to the breached data.", - "password": "Password found in the breached data.", - } - }, - ), - ( - MODULES[3][0], - { - "CyberBlindspot Incident": { - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "timestamp": "The timestamp of when the record was created", - "brand": "The organization the incident belongs to", - "status": "Incident's current state of affairs", - "severity": "The severity of the incident", - "remarks": "Remarks about the incident", - "type": "Incident type", - "id": "Unique ID for the incident record", - "external_link": "External link to the remote platform", - "confirmation_time": "The time of infringement confirmation.", - "risks": "The potential difficulties carried by the infringement.", - "incident_status": "The status of the infringement incident.", - } - }, - ), - ( - MODULES[4][0], - { - "CyberBlindspot Incident": { - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "timestamp": "The timestamp of when the record was created", - "brand": "The organization the incident belongs to", - "status": "Incident's current state of affairs", - "severity": "The severity of the incident", - "remarks": "Remarks about the incident", - "type": "Incident type", - "id": "Unique ID for the incident record", - "external_link": "External link to the remote platform", - "confirmation_time": "The time of infringement confirmation.", - "risks": "The potential difficulties carried by the infringement.", - "incident_status": "The status of the infringement incident.", - } - }, - ), - ( - MODULES[5][0], - { - "CyberBlindspot Incident": { - "first_seen": "The creation date of the incident", - "last_seen": "The date the incident got last updated", - "timestamp": "The timestamp of when the record was created", - "brand": "The organization the incident belongs to", - "status": "Incident's current state of affairs", - "severity": "The severity of the incident", - "remarks": "Remarks about the incident", - "type": "Incident type", - "id": "Unique ID for the incident record", - "external_link": "External link to the remote platform", - "masked_password": "The masked password related to the breached data.", - "password": "Password found in the breached data or compromised account.", - "software": "The software related to the breached data.", - "user": "The user related to the breached data.", - "user_domain": "The domain of the user related to the breached data.", - "website": "The website related to the breached data.", - "sources": "The sources related to the breached data.", - "source_uri": "The source URI related to the breached data.", - "domain": "The domain related to the breached data or compromised device.", - "hostname": "The hostname related to the breached data.", - "stealer_family": "The family of the malware.", - "compromise_details": "The details of the compromise.", - "date_compromised": "The date the malware was compromised.", - "computer_name": "The name of the computer that was compromised.", - "operating_system": "The operating system of the computer that was compromised.", - "malware_path": "The path of the malware.", - "url_path": "The URL path of the malware.", - } - }, - ), + (None, "incidents"), + ("", "incidents"), + ("Incidents", "incidents"), + ("Malware Logs", "malware_logs"), + ("Social Media Fraud", "social_media_fraud"), + ("Gambling Sites", "gambling_sites"), + ("Money Mules", "money_mules"), + ("legacy-unknown", "incidents"), ], ) -def test_get_mapping_fields_command(mock_module, expected_mappings): +def test_resolve_cbs_module_defaults_to_incidents(module_to_use, expected): + """Missing or blank module_to_use must keep pre-module-selector behavior (Incidents).""" + from CyberBlindspot import resolve_cbs_module + + assert resolve_cbs_module(module_to_use) == expected + + +@pytest.mark.parametrize( + "is_xsiam_platform, expected_severity", + [ + (True, IncidentSeverity.MEDIUM), + (False, IncidentSeverity.MEDIUM), + ], +) +def test_map_and_create_incident_severity_by_platform(is_xsiam_platform, expected_severity, mocker): + """Incident fetch maps API severity to numeric Demisto severity on all platforms.""" + from CyberBlindspot import map_and_create_incident + + mocker.patch("CyberBlindspot.is_xsiam", return_value=is_xsiam_platform) + record = { + "remarks": "Test incident", + "first_seen": "05-07-2024 06:28:28", + "last_seen": "26-10-2024 12:15:24", + "status": "monitoring", + "severity": "medium", + "id": "CBS-1", + "timestamp": 1720161042000, + "external_link": "https://example.com", + } + result = map_and_create_incident(record) + + assert result["severity"] == expected_severity + + +@pytest.mark.parametrize( + "mock_module,module_fields", + [(module[0], module[3]) for module in MODULES], + ids=[module[0] for module in MODULES], +) +def test_get_mapping_fields_command(mock_module, module_fields): """ Given: Nothing. When: @@ -674,9 +648,10 @@ def test_get_mapping_fields_command(mock_module, expected_mappings): Then: - Ensure a GetMappingFieldsResponse object that contains the application fields is returned. """ - from CyberBlindspot import get_mapping_fields_command, Instance + from CyberBlindspot import Instance, get_mapping_fields_command mock_instance = Instance(module=mock_module) + expected_mappings = {"CyberBlindspot Incident": {field["name"]: field["description"] for field in module_fields}} with patch("CyberBlindspot.INSTANCE", new=mock_instance): mappings = get_mapping_fields_command() @@ -850,6 +825,48 @@ def test_fetch_incidents_command(response_files_names, mock_params, mock_module, MODULES[5][0], MODULES[5][1], ), + ( + "fetch_social_media_fraud_response_valid.json", + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + "social_media_fraud_list_cmd_result_valid.json", + MODULES[6][0], + MODULES[6][1], + ), + ( + False, + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + False, + MODULES[6][0], + MODULES[6][1], + ), + ( + "fetch_money_mules_response_valid.json", + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + "money_mules_list_cmd_result_valid.json", + MODULES[7][0], + MODULES[7][1], + ), + ( + False, + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + False, + MODULES[7][0], + MODULES[7][1], + ), + ( + "fetch_gambling_sites_response_valid.json", + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + "gambling_sites_list_cmd_result_valid.json", + MODULES[8][0], + MODULES[8][1], + ), + ( + False, + {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"}, + False, + MODULES[8][0], + MODULES[8][1], + ), ], ) def test_ctm360_cbs_incident_list_command( @@ -864,7 +881,7 @@ def test_ctm360_cbs_incident_list_command( Then: - Fetch the list of incidents from the remote server. """ - from CyberBlindspot import ctm360_cbs_list_command, Instance + from CyberBlindspot import Instance, ctm360_cbs_list_command patched_response = load_mock_response(response_file_name) if response_file_name else [] mocker.patch.object(mock_client, "fetch_incidents", return_value=patched_response) @@ -1078,6 +1095,42 @@ def test_ctm360_cbs_incident_list_command( {}, MODULES[5], ), + ( + "social_media_fraud_details_response_valid.json", + {"ticketId": "BI-A81ET1215"}, + "social_media_fraud_details_response_valid.json", + MODULES[6], + ), + ( + False, + {"ticketId": "COMX165756654321"}, + {}, + MODULES[6], + ), + ( + "money_mules_details_response_valid.json", + {"ticketId": "MM-C7FDB1092"}, + "money_mules_details_response_valid.json", + MODULES[7], + ), + ( + False, + {"ticketId": "COMX165756654321"}, + {}, + MODULES[7], + ), + ( + "gambling_sites_details_response_valid.json", + {"ticketId": "GS-162RTEE07"}, + "gambling_sites_details_response_valid.json", + MODULES[8], + ), + ( + False, + {"ticketId": "COMX165756654321"}, + {}, + MODULES[8], + ), ], ) def test_ctm360_cbs_incident_details_command(response_file_name, mock_args, mock_asserts, mock_module, mock_client, mocker): @@ -1089,13 +1142,17 @@ def test_ctm360_cbs_incident_details_command(response_file_name, mock_args, mock Then: - Ensure result is as expected. """ - from CyberBlindspot import ctm360_cbs_details_command, Instance + from CyberBlindspot import Instance, ctm360_cbs_details_command, normalize_timestamp mock_instance = Instance(module=mock_module) with patch("CyberBlindspot.INSTANCE", new=mock_instance): patched_response = load_mock_response(response_file_name) if response_file_name else {} mocker.patch.object(mock_client, "fetch_incident", return_value=patched_response) + if isinstance(mock_asserts, str): + mock_asserts = load_mock_response(mock_asserts) + if mock_asserts.get("timestamp", ""): + mock_asserts = {**mock_asserts, "timestamp": str(normalize_timestamp(mock_asserts["timestamp"]))} cmd_results = ctm360_cbs_details_command(mock_client, mock_args) assert cmd_results.to_context().get("Contents") == mock_asserts @@ -1226,7 +1283,12 @@ def test_get_remote_data(mock_module, mock_response, mock_entry, mock_status, mo - Ensure result is as expected. """ from copy import deepcopy - from CyberBlindspot import get_remote_data_command, map_and_create_incident, Instance + + from CyberBlindspot import ( + Instance, + get_remote_data_command, + map_and_create_incident, + ) mock_args = {"id": "COMX165756654321", "lastUpdate": "2024-01-02T13:30:21.172707565Z"} mock_result = load_mock_response(mock_response) diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/README.md b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/README.md index 7f3451a339fe..dcc4b0356c10 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/README.md +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/README.md @@ -6,7 +6,7 @@ This integration was integrated and tested with version `1.0.0` of CTM360_CyberB | **Parameter** | **Description** | **Required** | | --- | --- | --- | | Incident Mirroring Direction | Choose the direction to mirror the incident: Incoming \(from CyberBlindspot to Cortex XSOAR\), Outgoing \(from Cortex XSOAR to CyberBlindspot\), or Incoming and Outgoing \(from/to Cortex XSOAR and CyberBlindspot\). | False | -| Module To Use | Choose the module you want to use: Incidents, Compromised Cards, Breached Credentials, and Domain/Subdomain Infringements. | False | +| Module To Use | The module to use: Incidents, Compromised Cards, Breached Credentials, Malware Logs, Domain/Subdomain Infringements, Social Media Fraud, Gambling Sites, or Money Mules. | False | | Retrieve Screenshots | | False | | First fetch (<number> <time unit>, e.g., 12 hours. Default is `7 days`) | The time the incidents should be fetched starting from. | False | | API Key | The CTM360 CyberBlindspot API Key to use for fetching data. | True | @@ -204,15 +204,15 @@ Fetch details of a single incident from the CyberBlindspot platform. | **Path** | **Type** | **Description** | | --- | --- | --- | -| CyberBlindspot.RemoteIncident.id | unknown | Unique ID for the incident record. | +| CyberBlindspot.RemoteIncident.id | unknown | The unique ID for the incident record. | | CyberBlindspot.RemoteIncident.brand | unknown | The organization the incident is associated with. | | CyberBlindspot.RemoteIncident.coa | unknown | The course of action to take. | | CyberBlindspot.RemoteIncident.class | unknown | The classification of the incident on remote server. | | CyberBlindspot.RemoteIncident.status | unknown | The current status of the incident on remote server. | | CyberBlindspot.RemoteIncident.severity | unknown | The severity of the incident. | -| CyberBlindspot.RemoteIncident.subject | unknown | Asset or title of incident. | -| CyberBlindspot.RemoteIncident.type | unknown | Incident type on remote server. | -| CyberBlindspot.RemoteIncident.remarks | unknown | Remarks about the incident. | +| CyberBlindspot.RemoteIncident.subject | unknown | The asset or title of the incident. | +| CyberBlindspot.RemoteIncident.type | unknown | The incident type on the remote server. | +| CyberBlindspot.RemoteIncident.remarks | unknown | The remarks about the incident. | | CyberBlindspot.RemoteIncident.created_date | unknown | The creation date of the incident \(legacy\). | | CyberBlindspot.RemoteIncident.updated_date | unknown | The date the incident last got updated \(legacy\). | | CyberBlindspot.RemoteIncident.first_seen | unknown | The creation date of the incident. | @@ -224,10 +224,10 @@ Fetch details of a single incident from the CyberBlindspot platform. | CyberBlindspot.RemoteIncident.expiry_year | unknown | The compromised card's expiration year. | | CyberBlindspot.RemoteIncident.breach_source | unknown | The source of the breached data. | | CyberBlindspot.RemoteIncident.domain | unknown | The domain related to the breached data or compromised device. | -| CyberBlindspot.RemoteIncident.email | unknown | Email found in the breached data. | -| CyberBlindspot.RemoteIncident.username | unknown | Username found in the breached data. | -| CyberBlindspot.RemoteIncident.password | unknown | Password found in the breached data or compromised account. | -| CyberBlindspot.RemoteIncident.executive_name | unknown | Executive member's name related to the breached data. | +| CyberBlindspot.RemoteIncident.email | unknown | The email found in the breached data. | +| CyberBlindspot.RemoteIncident.username | unknown | The username found in the breached data. | +| CyberBlindspot.RemoteIncident.password | unknown | The password found in the breached data or compromised account. | +| CyberBlindspot.RemoteIncident.executive_name | unknown | The executive member's name related to the breached data. | | CyberBlindspot.RemoteIncident.confirmation_time | unknown | The time of infringement confirmation. | | CyberBlindspot.RemoteIncident.risks | unknown | The potential difficulties carried by the infringement. | | CyberBlindspot.RemoteIncident.incident_status | unknown | The status of the infringement incident. | @@ -235,19 +235,45 @@ Fetch details of a single incident from the CyberBlindspot platform. | CyberBlindspot.RemoteIncident.date_compromised | unknown | The date of the compromise. | | CyberBlindspot.RemoteIncident.computer_name | unknown | The name of the computer that was compromised. | | CyberBlindspot.RemoteIncident.operating_system | unknown | The operating system of the compromised device. | -| CyberBlindspot.RemoteIncident.malware_path | unknown | The path of the in which the malware was installed on the compromised device. | +| CyberBlindspot.RemoteIncident.malware_path | unknown | The path in which the malware was installed on the compromised device. | | CyberBlindspot.RemoteIncident.url_path | unknown | The path of the URL. | | CyberBlindspot.RemoteIncident.masked_password | unknown | The masked password of the compromised account. | | CyberBlindspot.RemoteIncident.software | unknown | The software that was compromised. | | CyberBlindspot.RemoteIncident.user | unknown | The user of the compromised account. | | CyberBlindspot.RemoteIncident.user_domain | unknown | The domain of the compromised user. | -| CyberBlindspot.RemoteIncident.website | unknown | The website compromised. | +| CyberBlindspot.RemoteIncident.website | unknown | The website that was compromised. | | CyberBlindspot.RemoteIncident.sources | unknown | The sources reporting the compromise. | | CyberBlindspot.RemoteIncident.source_uri | unknown | The source URI of the compromise report. | | CyberBlindspot.RemoteIncident.hostname | unknown | The hostname of the compromised device. | | CyberBlindspot.RemoteIncident.stealer_family | unknown | The family of the malware. | | CyberBlindspot.RemoteIncident.external_link | unknown | The external link to the remote platform. | | CyberBlindspot.RemoteIncident.compromise_details | unknown | The details of the compromise. | +| CyberBlindspot.RemoteIncident.platform | unknown | The social network platform for social media fraud findings. | +| CyberBlindspot.RemoteIncident.risk_score | unknown | The numeric risk score from CBS. | +| CyberBlindspot.RemoteIncident.money_mule_id | unknown | The CBS money mule finding ID. | +| CyberBlindspot.RemoteIncident.account_identifier | unknown | The account identifier tied to the money mule. | +| CyberBlindspot.RemoteIncident.suspect_names | unknown | The names associated with the money mule. | +| CyberBlindspot.RemoteIncident.suspect_emails | unknown | The email addresses associated with the money mule. | +| CyberBlindspot.RemoteIncident.suspect_phones | unknown | The phone numbers associated with the money mule. | +| CyberBlindspot.RemoteIncident.transfer_amount | unknown | The transfer amount when present. | +| CyberBlindspot.RemoteIncident.transfer_currency | unknown | The currency code for the transfer. | +| CyberBlindspot.RemoteIncident.bank_account_holder_name | unknown | The name on the bank account. | +| CyberBlindspot.RemoteIncident.bank_name | unknown | The bank name tied to the money mule. | +| CyberBlindspot.RemoteIncident.bank_account_country | unknown | The country of the bank account. | +| CyberBlindspot.RemoteIncident.bic | unknown | The Bank Identifier Code. | +| CyberBlindspot.RemoteIncident.finding_id | unknown | The CBS gambling-site finding ID. | +| CyberBlindspot.RemoteIncident.url | unknown | The primary gambling site URL. | +| CyberBlindspot.RemoteIncident.submitted_url | unknown | The URL submitted to CBS for scanning. | +| CyberBlindspot.RemoteIncident.landing_url | unknown | The landing page URL observed for the site. | +| CyberBlindspot.RemoteIncident.title | unknown | The page title observed during scan. | +| CyberBlindspot.RemoteIncident.resolving_ip | unknown | The resolved IP for the gambling site. | +| CyberBlindspot.RemoteIncident.tags | unknown | The tags applied to the gambling site finding. | +| CyberBlindspot.RemoteIncident.status_code | unknown | The HTTP status code from scan. | +| CyberBlindspot.RemoteIncident.url_status | unknown | The URL reachability status. | +| CyberBlindspot.RemoteIncident.scan_status | unknown | The scan completion status. | +| CyberBlindspot.RemoteIncident.enrichment | unknown | The DNS enrichment payload. | +| CyberBlindspot.RemoteIncident.external_links | unknown | The external links discovered on the gambling site. | +| CyberBlindspot.RemoteIncident.internal_links | unknown | The internal links discovered on the gambling site. | ### ctm360-cbs-incident-retrieve-screenshots diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_gambling_sites_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_gambling_sites_response_valid.json new file mode 100644 index 000000000000..48ea37ae319c --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_gambling_sites_response_valid.json @@ -0,0 +1,178 @@ +[ + { + "id": "GS-162RTEE07", + "finding_id": "GS-162RTEE07", + "first_seen": "11-06-2026 01:41:21 PM", + "last_seen": "11-06-2026 01:41:21 PM", + "type": "Gambling Sites", + "remarks": "GS-162RTEE07", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-162RTEE07&selectedSearchField=finding_id", + "brand": "GIBM Giga", + "timestamp": 1781185291401, + "status": "new", + "url": "http://gambling-site-1.example.local", + "submitted_url": "http://gambling-site-1.example.local", + "landing_url": "https://gambling-site-1.example.local", + "title": "Example Gambling Site One", + "resolving_ip": "203.0.113.10", + "tags": [], + "status_code": 200, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [], + "a_record": [ + "203.0.113.10", + "203.0.113.11" + ], + "spf_record": null, + "ns_record": [ + "ns1.example.local", + "ns2.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-1.example.local/assets/style-1.css", + "https://gambling-site-1.example.local/games/crash", + "https://gambling-site-1.example.local/assets/font-1.woff2", + "https://gambling-site-1.example.local/assets/font-2.woff2", + "https://gambling-site-1.example.local/images/favicon.png", + "https://gambling-site-1.example.local/games/fishing", + "https://gambling-site-1.example.local/partners/portal", + "https://gambling-site-1.example.local/contact/chat", + "https://gambling-site-1.example.local/desktop", + "https://gambling-site-1.example.local/games/slots", + "https://gambling-site-1.example.local/register", + "https://gambling-site-1.example.local/support/live-chat", + "https://gambling-site-1.example.local/games/live-casino", + "https://gambling-site-1.example.local/games/lottery", + "https://gambling-site-1.example.local/assets/font-3.woff2", + "https://gambling-site-1.example.local/games/cockfight", + "https://gambling-site-1.example.local/support/welcome", + "https://gambling-site-1.example.local/assets/bootstrap.min.css", + "https://gambling-site-1.example.local/games/sports", + "https://gambling-site-1.example.local/login", + "https://gambling-site-1.example.local/assets/icons.min.css", + "https://gambling-site-1.example.local/jackpot", + "https://gambling-site-1.example.local/games/virtual" + ], + "internal_links": [] + }, + { + "id": "GS-5169F4D92", + "finding_id": "GS-5169F4D92", + "first_seen": "11-06-2026 01:38:45 PM", + "last_seen": "11-06-2026 01:38:45 PM", + "type": "Gambling Sites", + "remarks": "GS-5169F4D92", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-5169F4D92&selectedSearchField=finding_id", + "brand": "Bank Sterling McQueen", + "timestamp": 1781185188449, + "status": "new", + "url": "https://gambling-site-2.example.local", + "submitted_url": "https://gambling-site-2.example.local", + "landing_url": "https://gambling-site-2.example.local/landing", + "title": "Example Gambling Site Two", + "resolving_ip": "203.0.113.20", + "tags": [], + "status_code": 200, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [], + "a_record": [ + "203.0.113.20", + "203.0.113.21" + ], + "spf_record": null, + "ns_record": [ + "ns3.example.local", + "ns4.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-2.example.local/promotions/event-1", + "https://gambling-site-2.example.local/promotions/event-2", + "https://gambling-site-2.example.local/mobile", + "https://gambling-site-2.example.local/social/telegram", + "https://gambling-site-2.example.local/streaming/sports", + "https://gambling-site-2.example.local/support/welcome", + "https://gambling-site-2.example.local/support/live-chat", + "https://gambling-site-2.example.local/partners/affiliate", + "https://gambling-site-2.example.local/assets/style.css", + "https://gambling-site-2.example.local/assets/bootstrap-icons.css", + "https://gambling-site-2.example.local/promotions/cheat-sheet", + "https://gambling-site-2.example.local/support/licence", + "https://gambling-site-2.example.local/social/bot", + "https://gambling-site-2.example.local/assets/driver.css", + "https://gambling-site-2.example.local/amp/home", + "https://gambling-site-2.example.local/images/icons.png", + "https://gambling-site-2.example.local/spin", + "https://gambling-site-2.example.local/downloads/app.apk", + "https://gambling-site-2.example.local/scores", + "https://gambling-site-2.example.local/assets/bootstrap.min.css" + ], + "internal_links": [] + }, + { + "id": "GS-6VG22CF48", + "finding_id": "GS-6VG22CF48", + "first_seen": "11-06-2026 01:39:31 PM", + "last_seen": "11-06-2026 01:39:31 PM", + "type": "Gambling Sites", + "remarks": "GS-6VG22CF48", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-6VG22CF48&selectedSearchField=finding_id", + "brand": "GT. Vespa Credit Malaysia Miru (CONDA)", + "timestamp": 1781185182623, + "status": "new", + "url": "http://gambling-site-3.example.local", + "submitted_url": "http://gambling-site-3.example.local", + "landing_url": "https://gambling-site-3.example.local/landing", + "title": "Example Gambling Site Three", + "resolving_ip": "203.0.113.30", + "tags": [], + "status_code": 301, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [ + "mail.example.local" + ], + "a_record": [ + "203.0.113.30" + ], + "spf_record": null, + "ns_record": [ + "ns5.example.local", + "ns6.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-3.example.local/wp-json/wp/v2/pages/183", + "https://gambling-site-3.example.local/comments/feed/", + "https://gambling-site-3.example.local", + "https://cdn.example.local/amp", + "https://gambling-site-3.example.local/", + "https://images.example.local/asset-host", + "https://gambling-site-3.example.local/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fgambling-site-3.example.local%2F&format=xml", + "https://gambling-site-3.example.local/feed/", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/Lato-Regular.woff2", + "https://gambling-site-3.example.local/xmlrpc.php?rsd", + "https://chat.example.local/live", + "https://gambling-site-3.example.local/wp-content/themes/example/images/favicon.png", + "https://gambling-site-3.example.local/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fgambling-site-3.example.local%2F", + "https://gambling-site-3.example.local/wp-json/", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-brands-400.eot#iefix", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/digital_sans_ef_medium.woff2", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-regular-400.eot#iefix", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-solid-900.woff" + ], + "internal_links": [] + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_money_mules_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_money_mules_response_valid.json new file mode 100644 index 000000000000..33a91d082346 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_money_mules_response_valid.json @@ -0,0 +1,92 @@ +[ + { + "id": "MM-C7FDB1092", + "money_mule_id": "MM-C7FDB1092", + "first_seen": "01-03-2022 01:02:00 PM", + "last_seen": "27-05-2025 08:06:48 AM", + "type": "Advance Fee Fraud", + "remarks": "MM-C7FDB1092", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-C7FDB1092&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": "2022-03-01T13:02:00", + "account_identifier": "123543351324124.0", + "status": "addressed", + "suspect_names": [ + "Suspect One", + "Suspect Two", + "Suspect Three" + ], + "suspect_emails": [ + "suspect.one@example.local", + "suspect.two@example.local" + ], + "suspect_phones": [ + "(555) 111-0001", + "(555) 111-0002", + "(555) 111-0003" + ], + "transfer_amount": null, + "transfer_currency": null, + "bank_account_holder_name": null, + "bank_name": "JAX Labs", + "bank_account_country": null, + "bic": null + }, + { + "id": "MM-A12BC3456", + "money_mule_id": "MM-A12BC3456", + "first_seen": "15-06-2023 09:30:00 AM", + "last_seen": "20-08-2024 02:15:00 PM", + "type": "Advance Fee Fraud", + "remarks": "MM-A12BC3456", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-A12BC3456&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1686814200000, + "account_identifier": "987654321098765.0", + "status": "new", + "suspect_names": [ + "Jane Doe" + ], + "suspect_emails": [ + "jane.doe@example.local" + ], + "suspect_phones": [ + "(555) 123-4567" + ], + "transfer_amount": 1500.0, + "transfer_currency": "USD", + "bank_account_holder_name": "Jane Doe", + "bank_name": "Example Bank", + "bank_account_country": "US", + "bic": "EXAMUS33" + }, + { + "id": "MM-D78EF9012", + "money_mule_id": "MM-D78EF9012", + "first_seen": "10-01-2024 11:00:00 AM", + "last_seen": "10-01-2024 11:00:00 AM", + "type": "Advance Fee Fraud", + "remarks": "MM-D78EF9012", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-D78EF9012&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1704884400000, + "account_identifier": "555123456789012.0", + "status": "monitoring", + "suspect_names": [ + "John Smith", + "Alice Brown" + ], + "suspect_emails": [ + "john@example.local" + ], + "suspect_phones": [ + "(555) 222-0199" + ], + "transfer_amount": null, + "transfer_currency": null, + "bank_account_holder_name": null, + "bank_name": "Regional Credit Union", + "bank_account_country": "GB", + "bic": null + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_social_media_fraud_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_social_media_fraud_response_valid.json new file mode 100644 index 000000000000..63bccbb5f418 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/fetch_social_media_fraud_response_valid.json @@ -0,0 +1,59 @@ +[ + { + "id": "BI-A81ET1215", + "first_seen": "19-01-2022 03:50:18 PM", + "last_seen": "19-01-2022 03:50:18 PM", + "type": "Brand Impersonation", + "remarks": "BI-A81ET1215", + "external_link": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-A81ET1215&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1642607418000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-1", + "risk_score": 70, + "risks": [ + "some posts or followers", + "exact name detected" + ], + "status": "resolved", + "incident_status": "resolved" + }, + { + "id": "BI-4389DS950", + "first_seen": "19-01-2022 03:50:19 PM", + "last_seen": "19-01-2022 03:50:19 PM", + "type": "Brand Impersonation", + "remarks": "BI-4389DS950", + "external_link": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-4389DS950&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1642607419000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-2", + "risk_score": 70, + "risks": [ + "many posts or followers", + "exact name detected" + ], + "status": "resolved", + "incident_status": "resolved" + }, + { + "id": "BI-29EJK0A81", + "first_seen": "19-01-2022 03:50:19 PM", + "last_seen": "19-01-2022 03:50:19 PM", + "type": "Brand Impersonation", + "remarks": "BI-29EJK0A81", + "external_link": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-29EJK0A81&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1642607419000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-3", + "risk_score": 70, + "risks": [ + "limited posts or follower", + "exact name detected" + ], + "status": "resolved", + "incident_status": "resolved" + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_details_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_details_response_valid.json new file mode 100644 index 000000000000..c5e0885ec3f0 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_details_response_valid.json @@ -0,0 +1,61 @@ +{ + "id": "GS-162RTEE07", + "finding_id": "GS-162RTEE07", + "first_seen": "11-06-2026 01:41:21 PM", + "last_seen": "11-06-2026 01:41:21 PM", + "type": "Gambling Sites", + "remarks": "GS-162RTEE07", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-162RTEE07&selectedSearchField=finding_id", + "brand": "GIBM Giga", + "timestamp": 1781185291401, + "status": "new", + "url": "http://gambling-site-1.example.local", + "submitted_url": "http://gambling-site-1.example.local", + "landing_url": "https://gambling-site-1.example.local", + "title": "Example Gambling Site One", + "resolving_ip": "203.0.113.10", + "tags": [], + "status_code": 200, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [], + "a_record": [ + "203.0.113.10", + "203.0.113.11" + ], + "spf_record": null, + "ns_record": [ + "ns1.example.local", + "ns2.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-1.example.local/assets/style-1.css", + "https://gambling-site-1.example.local/games/crash", + "https://gambling-site-1.example.local/assets/font-1.woff2", + "https://gambling-site-1.example.local/assets/font-2.woff2", + "https://gambling-site-1.example.local/images/favicon.png", + "https://gambling-site-1.example.local/games/fishing", + "https://gambling-site-1.example.local/partners/portal", + "https://gambling-site-1.example.local/contact/chat", + "https://gambling-site-1.example.local/desktop", + "https://gambling-site-1.example.local/games/slots", + "https://gambling-site-1.example.local/register", + "https://gambling-site-1.example.local/support/live-chat", + "https://gambling-site-1.example.local/games/live-casino", + "https://gambling-site-1.example.local/games/lottery", + "https://gambling-site-1.example.local/assets/font-3.woff2", + "https://gambling-site-1.example.local/games/cockfight", + "https://gambling-site-1.example.local/support/welcome", + "https://gambling-site-1.example.local/assets/bootstrap.min.css", + "https://gambling-site-1.example.local/games/sports", + "https://gambling-site-1.example.local/login", + "https://gambling-site-1.example.local/assets/icons.min.css", + "https://gambling-site-1.example.local/jackpot", + "https://gambling-site-1.example.local/games/virtual" + ], + "internal_links": [] +} diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_list_cmd_result_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_list_cmd_result_valid.json new file mode 100644 index 000000000000..324b8daab9a2 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/gambling_sites_list_cmd_result_valid.json @@ -0,0 +1,190 @@ +[ + { + "name": "GS-162RTEE07", + "occurred": "2026-06-11T13:41:21+00:00", + "externalstatus": "new", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-162RTEE07&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Gambling Sites", + "cbs_module": "gambling_sites", + "cbs_coa": "", + "cbs_updated_date": "2026-06-11T13:41:21+00:00", + "finding_id": "GS-162RTEE07", + "brand": "GIBM Giga", + "timestamp": 1781185291401, + "url": "http://gambling-site-1.example.local", + "submitted_url": "http://gambling-site-1.example.local", + "landing_url": "https://gambling-site-1.example.local", + "title": "Example Gambling Site One", + "resolving_ip": "203.0.113.10", + "tags": [], + "status_code": 200, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [], + "a_record": [ + "203.0.113.10", + "203.0.113.11" + ], + "spf_record": null, + "ns_record": [ + "ns1.example.local", + "ns2.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-1.example.local/assets/style-1.css", + "https://gambling-site-1.example.local/games/crash", + "https://gambling-site-1.example.local/assets/font-1.woff2", + "https://gambling-site-1.example.local/assets/font-2.woff2", + "https://gambling-site-1.example.local/images/favicon.png", + "https://gambling-site-1.example.local/games/fishing", + "https://gambling-site-1.example.local/partners/portal", + "https://gambling-site-1.example.local/contact/chat", + "https://gambling-site-1.example.local/desktop", + "https://gambling-site-1.example.local/games/slots", + "https://gambling-site-1.example.local/register", + "https://gambling-site-1.example.local/support/live-chat", + "https://gambling-site-1.example.local/games/live-casino", + "https://gambling-site-1.example.local/games/lottery", + "https://gambling-site-1.example.local/assets/font-3.woff2", + "https://gambling-site-1.example.local/games/cockfight", + "https://gambling-site-1.example.local/support/welcome", + "https://gambling-site-1.example.local/assets/bootstrap.min.css", + "https://gambling-site-1.example.local/games/sports", + "https://gambling-site-1.example.local/login", + "https://gambling-site-1.example.local/assets/icons.min.css", + "https://gambling-site-1.example.local/jackpot", + "https://gambling-site-1.example.local/games/virtual" + ], + "internal_links": [] + } + }, + { + "name": "GS-5169F4D92", + "occurred": "2026-06-11T13:38:45+00:00", + "externalstatus": "new", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-5169F4D92&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Gambling Sites", + "cbs_module": "gambling_sites", + "cbs_coa": "", + "cbs_updated_date": "2026-06-11T13:38:45+00:00", + "finding_id": "GS-5169F4D92", + "brand": "Bank Sterling McQueen", + "timestamp": 1781185188449, + "url": "https://gambling-site-2.example.local", + "submitted_url": "https://gambling-site-2.example.local", + "landing_url": "https://gambling-site-2.example.local/landing", + "title": "Example Gambling Site Two", + "resolving_ip": "203.0.113.20", + "tags": [], + "status_code": 200, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [], + "a_record": [ + "203.0.113.20", + "203.0.113.21" + ], + "spf_record": null, + "ns_record": [ + "ns3.example.local", + "ns4.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-2.example.local/promotions/event-1", + "https://gambling-site-2.example.local/promotions/event-2", + "https://gambling-site-2.example.local/mobile", + "https://gambling-site-2.example.local/social/telegram", + "https://gambling-site-2.example.local/streaming/sports", + "https://gambling-site-2.example.local/support/welcome", + "https://gambling-site-2.example.local/support/live-chat", + "https://gambling-site-2.example.local/partners/affiliate", + "https://gambling-site-2.example.local/assets/style.css", + "https://gambling-site-2.example.local/assets/bootstrap-icons.css", + "https://gambling-site-2.example.local/promotions/cheat-sheet", + "https://gambling-site-2.example.local/support/licence", + "https://gambling-site-2.example.local/social/bot", + "https://gambling-site-2.example.local/assets/driver.css", + "https://gambling-site-2.example.local/amp/home", + "https://gambling-site-2.example.local/images/icons.png", + "https://gambling-site-2.example.local/spin", + "https://gambling-site-2.example.local/downloads/app.apk", + "https://gambling-site-2.example.local/scores", + "https://gambling-site-2.example.local/assets/bootstrap.min.css" + ], + "internal_links": [] + } + }, + { + "name": "GS-6VG22CF48", + "occurred": "2026-06-11T13:39:31+00:00", + "externalstatus": "new", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/gambling_sites/?filters=[]&searchQuery=GS-6VG22CF48&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Gambling Sites", + "cbs_module": "gambling_sites", + "cbs_coa": "", + "cbs_updated_date": "2026-06-11T13:39:31+00:00", + "finding_id": "GS-6VG22CF48", + "brand": "GT. Vespa Credit Malaysia Miru (CONDA)", + "timestamp": 1781185182623, + "url": "http://gambling-site-3.example.local", + "submitted_url": "http://gambling-site-3.example.local", + "landing_url": "https://gambling-site-3.example.local/landing", + "title": "Example Gambling Site Three", + "resolving_ip": "203.0.113.30", + "tags": [], + "status_code": 301, + "url_status": "active", + "scan_status": "completed", + "enrichment": { + "dns": { + "mx_record": [ + "mail.example.local" + ], + "a_record": [ + "203.0.113.30" + ], + "spf_record": null, + "ns_record": [ + "ns5.example.local", + "ns6.example.local" + ] + } + }, + "external_links": [ + "https://gambling-site-3.example.local/wp-json/wp/v2/pages/183", + "https://gambling-site-3.example.local/comments/feed/", + "https://gambling-site-3.example.local", + "https://cdn.example.local/amp", + "https://gambling-site-3.example.local/", + "https://images.example.local/asset-host", + "https://gambling-site-3.example.local/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fgambling-site-3.example.local%2F&format=xml", + "https://gambling-site-3.example.local/feed/", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/Lato-Regular.woff2", + "https://gambling-site-3.example.local/xmlrpc.php?rsd", + "https://chat.example.local/live", + "https://gambling-site-3.example.local/wp-content/themes/example/images/favicon.png", + "https://gambling-site-3.example.local/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fgambling-site-3.example.local%2F", + "https://gambling-site-3.example.local/wp-json/", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-brands-400.eot#iefix", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/digital_sans_ef_medium.woff2", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-regular-400.eot#iefix", + "https://gambling-site-3.example.local/wp-content/themes/example/webfonts/fa-solid-900.woff" + ], + "internal_links": [] + } + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_details_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_details_response_valid.json new file mode 100644 index 000000000000..1fff6166acf3 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_details_response_valid.json @@ -0,0 +1,33 @@ +{ + "id": "MM-C7FDB1092", + "money_mule_id": "MM-C7FDB1092", + "first_seen": "01-03-2022 01:02:00 PM", + "last_seen": "27-05-2025 08:06:48 AM", + "type": "Advance Fee Fraud", + "remarks": "MM-C7FDB1092", + "external_link": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-C7FDB1092&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": "2022-03-01T13:02:00", + "account_identifier": "123543351324124.0", + "status": "addressed", + "suspect_names": [ + "Suspect One", + "Suspect Two", + "Suspect Three" + ], + "suspect_emails": [ + "suspect.one@example.local", + "suspect.two@example.local" + ], + "suspect_phones": [ + "(555) 111-0001", + "(555) 111-0002", + "(555) 111-0003" + ], + "transfer_amount": null, + "transfer_currency": null, + "bank_account_holder_name": null, + "bank_name": "JAX Labs", + "bank_account_country": null, + "bic": null +} diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_list_cmd_result_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_list_cmd_result_valid.json new file mode 100644 index 000000000000..cbf152f2f3c4 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/money_mules_list_cmd_result_valid.json @@ -0,0 +1,104 @@ +[ + { + "name": "MM-C7FDB1092", + "occurred": "2022-03-01T13:02:00+00:00", + "externalstatus": "addressed", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-C7FDB1092&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Advance Fee Fraud", + "cbs_module": "money_mules", + "cbs_coa": "", + "cbs_updated_date": "2025-05-27T08:06:48+00:00", + "money_mule_id": "MM-C7FDB1092", + "brand": "JAX Labs", + "timestamp": 1646139720000, + "account_identifier": "123543351324124.0", + "suspect_names": [ + "Suspect One", + "Suspect Two", + "Suspect Three" + ], + "suspect_emails": [ + "suspect.one@example.local", + "suspect.two@example.local" + ], + "suspect_phones": [ + "(555) 111-0001", + "(555) 111-0002", + "(555) 111-0003" + ], + "transfer_amount": null, + "transfer_currency": null, + "bank_account_holder_name": null, + "bank_name": "JAX Labs", + "bank_account_country": null, + "bic": null + } + }, + { + "name": "MM-A12BC3456", + "occurred": "2023-06-15T09:30:00+00:00", + "externalstatus": "new", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-A12BC3456&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Advance Fee Fraud", + "cbs_module": "money_mules", + "cbs_coa": "", + "cbs_updated_date": "2024-08-20T14:15:00+00:00", + "money_mule_id": "MM-A12BC3456", + "brand": "JAX Labs", + "timestamp": 1686814200000, + "account_identifier": "987654321098765.0", + "suspect_names": [ + "Jane Doe" + ], + "suspect_emails": [ + "jane.doe@example.local" + ], + "suspect_phones": [ + "(555) 123-4567" + ], + "transfer_amount": 1500.0, + "transfer_currency": "USD", + "bank_account_holder_name": "Jane Doe", + "bank_name": "Example Bank", + "bank_account_country": "US", + "bic": "EXAMUS33" + } + }, + { + "name": "MM-D78EF9012", + "occurred": "2024-01-10T11:00:00+00:00", + "externalstatus": "monitoring", + "externallink": "https://platform.ctm360.com/cbs/online_anti_fraud/money_mules/?filters=[]&searchQuery=MM-D78EF9012&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Advance Fee Fraud", + "cbs_module": "money_mules", + "cbs_coa": "", + "cbs_updated_date": "2024-01-10T11:00:00+00:00", + "money_mule_id": "MM-D78EF9012", + "brand": "JAX Labs", + "timestamp": 1704884400000, + "account_identifier": "555123456789012.0", + "suspect_names": [ + "John Smith", + "Alice Brown" + ], + "suspect_emails": [ + "john@example.local" + ], + "suspect_phones": [ + "(555) 222-0199" + ], + "transfer_amount": null, + "transfer_currency": null, + "bank_account_holder_name": null, + "bank_name": "Regional Credit Union", + "bank_account_country": "GB", + "bic": null + } + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_details_response_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_details_response_valid.json new file mode 100644 index 000000000000..b81c62287f5e --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_details_response_valid.json @@ -0,0 +1,19 @@ +{ + "id": "BI-A81ET1215", + "first_seen": "19-01-2022 03:50:18 PM", + "last_seen": "19-01-2022 03:50:18 PM", + "type": "Brand Impersonation", + "remarks": "BI-A81ET1215", + "external_link": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-A81ET1215&selectedSearchField=finding_id", + "brand": "JAX Labs", + "timestamp": 1642607418000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-1", + "risk_score": 70, + "risks": [ + "some posts or followers", + "exact name detected" + ], + "status": "resolved", + "incident_status": "resolved" +} diff --git a/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_list_cmd_result_valid.json b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_list_cmd_result_valid.json new file mode 100644 index 000000000000..7766541b4534 --- /dev/null +++ b/Packs/CTM360-CyberBlindspot/Integrations/CyberBlindspot/test_data/social_media_fraud_list_cmd_result_valid.json @@ -0,0 +1,71 @@ +[ + { + "name": "BI-A81ET1215", + "occurred": "2022-01-19T15:50:18+00:00", + "externalstatus": "resolved", + "externallink": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-A81ET1215&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Brand Impersonation", + "cbs_module": "social_media_fraud", + "cbs_coa": "", + "cbs_updated_date": "2022-01-19T15:50:18+00:00", + "brand": "JAX Labs", + "timestamp": 1642607418000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-1", + "risk_score": 70, + "risks": [ + "some posts or followers", + "exact name detected" + ], + "incident_status": "resolved" + } + }, + { + "name": "BI-4389DS950", + "occurred": "2022-01-19T15:50:19+00:00", + "externalstatus": "resolved", + "externallink": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-4389DS950&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Brand Impersonation", + "cbs_module": "social_media_fraud", + "cbs_coa": "", + "cbs_updated_date": "2022-01-19T15:50:19+00:00", + "brand": "JAX Labs", + "timestamp": 1642607419000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-2", + "risk_score": 70, + "risks": [ + "many posts or followers", + "exact name detected" + ], + "incident_status": "resolved" + } + }, + { + "name": "BI-29EJK0A81", + "occurred": "2022-01-19T15:50:19+00:00", + "externalstatus": "resolved", + "externallink": "https://platform.ctm360.com/cbs/social_media_fraud/brand_impersonation/?filters=[]&searchQuery=BI-29EJK0A81&selectedSearchField=finding_id", + "severity": 1, + "CustomFields": { + "cbs_type": "Brand Impersonation", + "cbs_module": "social_media_fraud", + "cbs_coa": "", + "cbs_updated_date": "2022-01-19T15:50:19+00:00", + "brand": "JAX Labs", + "timestamp": 1642607419000, + "platform": "Twitter", + "subject": "https://social-fraud.example.local/profile-3", + "risk_score": 70, + "risks": [ + "limited posts or follower", + "exact name detected" + ], + "incident_status": "resolved" + } + } +] diff --git a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py index 49c78df54bc2..2a42dbd5c5d1 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py +++ b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.py @@ -1,14 +1,14 @@ +from typing import Any + import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa: F401 -from typing import Any - - """ IMPORTS """ from inspect import getfullargspec + import urllib3 # Disable insecure warnings @@ -28,42 +28,103 @@ HV_OUTGOING_DATE_FORMAT = "%d-%m-%Y %H:%M" HV_INCOMING_DATE_FORMAT = "%d-%m-%Y %H:%M:%S" HV_BASE_URL = "https://hackerview.ctm360.com" -HV_API_ENDPOINT = "/api/v2/issues" +HV_API_ENDPOINT = "/api/v2/issues" # Usual endpoint that also gets lightscan data API = { - "CHANGE_STATUS": "/progress_status/", + "DEEPSCAN": "/deepscan", # Deepscan endpoint + "CHANGE_STATUS": "/progress_status", } LOGGING_PREFIX = "[HACKERVIEW]" -HV_INCIDENT_FIELDS = [ - {"name": "id", "description": "Symbolic Incident ID."}, - {"name": "timestamp", "description": "DB timestamp."}, - {"name": "confidence", "description": "Confidence of report."}, - {"name": "cve_id", "description": "ID of associated CVE."}, - {"name": "cwe", "description": "List of associated CWEs."}, - {"name": "issue_category", "description": "Category of Incident"}, - {"name": "issue_name", "description": "Name of Incident."}, - {"name": "potential_attack_type", "description": "Potential attack to make use of incident."}, - {"name": "potential_impact", "description": "Potential impact of incident."}, - {"name": "status", "description": "Active status of Incident."}, - {"name": "progress_status", "description": "Progress of incident response."}, - {"name": "severity", "description": "Severity of incident."}, - {"name": "resolved_ip", "description": "IP resolved on affected asset."}, - {"name": "first_seen", "description": "Incident creation date."}, - {"name": "last_seen", "description": "Last discovery date for incident."}, - {"name": "last_updated", "description": "Last update date for incident."}, - {"name": "environments", "description": "env."}, - {"name": "ticket_id", "description": "Ticket ID"}, - {"name": "technologies", "description": "Technologies on affected asset."}, - {"name": "domain", "description": "domain of affected asset"}, - {"name": "host", "description": "host of affected asset"}, - {"name": "asset_type", "description": "affected asset type"}, - {"name": "asset", "description": "affected asset"}, +DEFAULT_FIELDS = [ + {"name": "brand", "description": "The organization brand the incident belongs to."}, + {"name": "ticket_id", "description": "The ticket ID."}, + {"name": "first_seen", "description": "The incident creation date."}, + {"name": "last_seen", "description": "The last discovery date for the incident."}, + {"name": "progress_status", "description": "The progress of incident response."}, + {"name": "severity", "description": "The severity of the incident."}, + {"name": "asset", "description": "The affected asset."}, + {"name": "issue_name", "description": "The name of the incident."}, + {"name": "cve_id", "description": "The associated CVE identifier(s)."}, +] + +HV_LIGHTSCAN_FIELDS = [ + {"name": "id", "description": "The symbolic incident ID."}, + {"name": "timestamp", "description": "The database timestamp."}, + {"name": "confidence", "description": "The confidence of the report."}, + {"name": "cwe", "description": "The list of associated CWEs."}, + {"name": "issue_category", "description": "The category of the incident."}, + {"name": "potential_attack_type", "description": "The potential attack type that can make use of the incident."}, + {"name": "potential_impact", "description": "The potential impact of the incident."}, + {"name": "status", "description": "The active status of the incident."}, + {"name": "resolved_ip", "description": "The IP resolved on the affected asset."}, + {"name": "last_updated", "description": "The last update date for the incident."}, + {"name": "environments", "description": "The environments associated with the incident."}, + {"name": "technologies", "description": "The technologies on the affected asset."}, + {"name": "domain", "description": "The domain of the affected asset."}, + {"name": "host", "description": "The host of the affected asset."}, + {"name": "asset_type", "description": "The affected asset type."}, + *DEFAULT_FIELDS, +] + +HV_DEEPSCAN_FIELDS = [ + {"name": "ip", "description": "The IP address associated with the finding."}, + {"name": "port", "description": "The network port associated with the finding."}, + {"name": "uri", "description": "The URI path or resource related to the finding."}, + {"name": "url", "description": "The base URL of the affected asset or service."}, + {"name": "issue_type", "description": "The type or classification of the issue."}, + {"name": "issue_description", "description": "The detailed description of the issue."}, + {"name": "cpe", "description": "The Common Platform Enumeration (CPE) identifier if applicable."}, + {"name": "cvss_metrics", "description": "The CVSS vector or metric string for the vulnerability."}, + {"name": "cvss_score", "description": "The CVSS base score for the vulnerability."}, + {"name": "epss_score", "description": "The Exploit Prediction Scoring System (EPSS) score."}, + {"name": "known_exploited", "description": "The indicator of whether the vulnerability is known to be exploited."}, + {"name": "hackerview_link", "description": "The link to the issue in the HackerView platform."}, + {"name": "evidence", "description": "The request/response evidence for the finding (e.g. request, response, curl_command)."}, + {"name": "domain", "description": "The domain of the affected asset."}, + {"name": "host", "description": "The host of the affected asset."}, + {"name": "asset_type", "description": "The affected asset type."}, + *DEFAULT_FIELDS, ] MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}.get( demisto.params().get("mirror_direction", "None") ) +HV_MODULE_DISPLAY_TO_TYPE = { + "Light Scan": "lightscan", + "Deep Scan": "deepscan", +} +HV_DEFAULT_MODULE_DISPLAY = "Light Scan" +HV_DEFAULT_MODULE_TYPE = "lightscan" + + +def resolve_hv_module(module_to_use: str | None = None) -> str: + """Resolve API module_type from instance config. + + Pre-upgrade instances may omit module_to_use; treat missing/blank/unknown as Light Scan. + """ + if module_to_use is None: + module_to_use = demisto.params().get("module_to_use", HV_DEFAULT_MODULE_DISPLAY) + if not module_to_use or not str(module_to_use).strip(): + return HV_DEFAULT_MODULE_TYPE + return HV_MODULE_DISPLAY_TO_TYPE.get(module_to_use, HV_DEFAULT_MODULE_TYPE) + + +class Instance: + def __init__(self, **kwargs) -> None: + self.module: str = kwargs.get("module", "lightscan") + match self.module: + case "lightscan": + self.mapping_fields = HV_LIGHTSCAN_FIELDS + case "deepscan": + self.mapping_fields = HV_DEEPSCAN_FIELDS + case _: + raise ValueError(f"Invalid module: {self.module}") + + +INSTANCE = Instance(module=resolve_hv_module()) + + INTEGRATION_INSTANCE = demisto.integrationInstance() @@ -151,13 +212,18 @@ def change_incident_status(self, args: dict[str, Any]) -> dict[str, Any]: :return: Response from the remote server carrying the result of the request :rtype: dict[str, Any] """ - log(DEBUG, f"status change params {args=}") + url_suffix = ( + f"{HV_API_ENDPOINT}{API['DEEPSCAN']}{API['CHANGE_STATUS']}" + if INSTANCE.module == "deepscan" + else f"{HV_API_ENDPOINT}{API['CHANGE_STATUS']}" + ) + log(DEBUG, f"status change params {args=} {url_suffix=}") response = self._http_request( method="POST", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], - url_suffix=HV_API_ENDPOINT + API["CHANGE_STATUS"], + url_suffix=url_suffix, data=args, params={"t": datetime.now().timestamp()}, ) @@ -235,8 +301,8 @@ def convert_time_string( """ output: datetime | None try: - if isinstance(time_string, int): - output = datetime.fromtimestamp(time_string / 1000) + if isinstance(time_string, int | float): + output = datetime.fromtimestamp(time_string / 1000, tz=timezone.utc) else: output = dateparser.parse(time_string, [input_format_string], **parser_args) if not isinstance(output, datetime): @@ -296,14 +362,16 @@ def map_and_create_incident(unmapped_incident: dict) -> dict: # Remove unnecessary fields unmapped_incident.pop("ticket_id", "") unmapped_incident.pop("last_updated", "") + mapped_severity = convert_to_demisto_severity(unmapped_incident.pop("severity", "low")) mapped_incident = { "name": unmapped_incident.pop("issue_name"), "occurred": convert_time_string( unmapped_incident.pop("first_seen", ""), HV_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True ), "external_status": unmapped_incident.pop("status", ""), - "severity": convert_to_demisto_severity(unmapped_incident.pop("severity", "low")), + "severity": mapped_severity, "CustomFields": { + "hv_module": INSTANCE.module, "updated_date": convert_time_string( unmapped_incident.pop("last_seen", ""), HV_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True ), @@ -383,6 +451,7 @@ def test_module(client: Client, params) -> str: if not api_key: log(INFO, 'Invalid "API Key" Value') raise DemistoException('Invalid "API Key" Value') + args["module_type"] = resolve_hv_module(params.get("module_to_use", HV_DEFAULT_MODULE_TYPE)) incidents = client.test_configuration(args) if max_fetch and len(incidents) > max_fetch: log(INFO, f"Incidents fetched exceed the limit, removing the excess {len(incidents) - max_fetch} incidents.") @@ -398,6 +467,7 @@ def test_module(client: Client, params) -> str: "Date From", "Date To", "API Key", + "Module", "does not match format '%d-%m-%Y %H:%M'", ] for word in expected_words: @@ -448,6 +518,31 @@ def fetch_incidents( return next_run, unique_incidents +def build_fetch_params(demisto_params: dict[str, Any], last_run: dict[str, Any]) -> dict[str, Any]: + """Build API params for fetch-incidents.""" + last_fetched_timestamp = last_run.get("last_fetched_timestamp", "") + first_fetch = demisto_params.get("first_fetch", "7 days") + try: + dateparser.parse(f"{first_fetch} UTC") + except Exception: + log(DEBUG, "first_fetch is not parsable, setting to `7 days`") + first_fetch = "7 days" + + if not last_fetched_timestamp: + log(DEBUG, f"Fetch is set to fetch from the {first_fetch} ago.") + + return { + "date_field": "@timestamp", + "order": "asc", + "max_hits": MAX_FETCH, + "module_type": INSTANCE.module, + "date_from": last_fetched_timestamp + if last_fetched_timestamp + else convert_time_string(f"{first_fetch} UTC", "", timestamp=True), + "t": datetime.now().timestamp() * 1000, + } + + def get_remote_data_command(client: Client, args: dict) -> GetRemoteDataResponse: """get-remote-data command: Returns an updated incident and error entry (if needed) @@ -471,7 +566,7 @@ def get_remote_data_command(client: Client, args: dict) -> GetRemoteDataResponse log(DEBUG, f"Performing get-remote-data command for the incident: {remote_incident_id}") - params = {"ticket_id": remote_incident_id, "t": datetime.now().timestamp() * 1000} + params = {"ticket_id": remote_incident_id, "module_type": INSTANCE.module, "t": datetime.now().timestamp() * 1000} updated_incident = client.fetch_incident(params) log(DEBUG, f"{updated_incident=}") @@ -560,6 +655,7 @@ def get_modified_remote_data_command(client: Client, args) -> GetModifiedRemoteD "order": "asc", "date_from": last_timestamp, "max_hits": ABSOLUTE_MAX_FETCH, + "module_type": INSTANCE.module, "t": datetime.now().timestamp() * 1000, } modified_incident_ids.extend(item["id"] for item in client.fetch_incidents(params)) @@ -615,7 +711,7 @@ def get_mapping_fields_command() -> GetMappingFieldsResponse: :rtype: GetMappingFieldsResponse """ incident_type_scheme = SchemeTypeMapping(type_name="HackerView Incident") - for field in HV_INCIDENT_FIELDS: + for field in INSTANCE.mapping_fields: incident_type_scheme.add_field(name=field["name"], description=field["description"]) return GetMappingFieldsResponse([incident_type_scheme]) @@ -636,7 +732,7 @@ def ctm360_hv_incident_list_command(client: Client, args: dict[str, Any]) -> Com if args.get("dateTo"): args["dateTo"] = convert_time_string(args["dateTo"], HV_OUTGOING_DATE_FORMAT, timestamp=True) params = {to_snake_case(key): v for key, v in args.items()} - params |= {"date_field": "@timestamp", "t": datetime.now().timestamp()} + params |= {"date_field": "@timestamp", "module_type": INSTANCE.module, "t": datetime.now().timestamp()} result = client.fetch_incidents(params) log(INFO, f"Received {len(result)} incidents") if len(result) > 0: @@ -663,7 +759,7 @@ def ctm360_hv_incident_details_command(client: Client, args: dict[str, Any]) -> :rtype: CommandResults """ params = {to_snake_case(key): v for key, v in args.items()} - params["t"] = datetime.now().timestamp() + params |= {"module_type": INSTANCE.module, "t": datetime.now().timestamp()} result = client.fetch_incident(params) log(INFO, f"Received {result}") @@ -734,46 +830,17 @@ def main() -> None: } if demisto_command == "fetch-incidents": - log(DEBUG, "at fetch command") + log(DEBUG, "at fetch-incidents command") last_run = demisto.getLastRun() - last_fetched_timestamp = last_run.get("last_fetched_timestamp", "") last_fetch_ids = last_run.get("last_fetch_ids", []) - first_fetch = demisto_params.get("first_fetch", "7 days") - - try: - dateparser.parse(f"{first_fetch} UTC") - except Exception: - log(DEBUG, "first_fetch is not parsable, setting to `7 days`") - first_fetch = "7 days" - - if not last_fetched_timestamp: - log(DEBUG, f"Fetch is set to fetch incidents from the {first_fetch} ago.") - - params = { - "date_field": "@timestamp", - "order": "asc", - "max_hits": MAX_FETCH, - "date_from": last_fetched_timestamp - if last_fetched_timestamp - else convert_time_string(f"{first_fetch} UTC", "", timestamp=True), - "t": datetime.now().timestamp() * 1000, - } - - log(DEBUG, f'{demisto_params.get("date_from")=}') - - log(INFO, f"Will be fetching {MAX_FETCH} incidents.") - - log(DEBUG, f'LastRun was {last_fetched_timestamp if last_fetched_timestamp else "NOT FOUND"}') - log(DEBUG, f'last run\'s ids were {last_run.get("last_fetch_ids")}') + params = build_fetch_params(demisto_params, last_run) + log(INFO, f"Will be fetching up to {MAX_FETCH} records.") log(DEBUG, f"Calling fetch with the following: {params=}") log(DEBUG, f"Mirroring set as: {MIRROR_DIRECTION}") next_run, incidents = fetch_incidents(client, last_fetch_ids, params, last_run) - - log(DEBUG, "Setting incidents and last run") - log(DEBUG, f"{next_run=}") - log(DEBUG, f"Fetched {len(incidents)} incidents") + log(DEBUG, f"Fetched {len(incidents)} incidents, {next_run=}") demisto.setLastRun(next_run) demisto.incidents(incidents) elif demisto_command == "test-module": diff --git a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml index 1593119794fa..3eae38a51a0e 100644 --- a/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml +++ b/Packs/CTM360-CyberBlindspot/Integrations/HackerView/HackerView.yml @@ -1,3 +1,5 @@ +name: CTM360_HackerView +display: CTM360 HackerView category: Network Security provider: CTM360 commonfields: @@ -18,45 +20,65 @@ configuration: - marketplacev2 - platform required: false -- defaultvalue: 7 days - display: First fetch (