import demistomock as demisto # noqa: F401 from COOCApiModule import * # noqa: E402 from CommonServerPython import * # noqa: F401 from http import HTTPStatus from datetime import date, datetime, timedelta, UTC from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from botocore.client import BaseClient as BotoClient from botocore.config import Config from botocore.exceptions import ClientError, WaiterError import boto3 from boto3 import Session from xml.sax.saxutils import escape import re import copy DEFAULT_MAX_RETRIES: int = 5 DEFAULT_MAX_WORKERS: int = 5 DEFAULT_SESSION_NAME = "cortex-session" DEFAULT_PROXYDOME_CERTFICATE_PATH = os.getenv("EGRESSPROXY_CA_PATH") or "/etc/certs/egress.crt" DEFAULT_PROXYDOME = os.getenv("CRTX_HTTP_PROXY") or "10.181.0.100:11117" TIMEOUT_CONFIG = Config(connect_timeout=60, read_timeout=60) DEFAULT_REGION = "us-east-1" MAX_FILTERS = 50 MAX_TAGS = 50 MAX_FILTER_VALUES = 200 MAX_TARGET_VALUES = 5 MAX_TRIPLE_FILTER_VALUE = 5 MAX_CHAR_LENGTH_FOR_FILTER_VALUE = 255 MAX_LIMIT_VALUE = 1000 DEFAULT_LIMIT_VALUE = 50 DEFAULT_INTERVAL_IN_SECONDS = 30 # Interval for polling commands. DEFAULT_TIMEOUT_POLLING_COMMAND = 600 # Default timeout for polling commands. TERMINAL_COMMAND_STATUSES = { # the status for run command command "Success": "The command completed successfully.", "Failed": "The command failed to complete successfully on the managed node.", "Delivery Timed Out": "The command wasn't delivered to the managed node before the total timeout expired.", "Incomplete": "The command was attempted on all managed nodes and one or more of the invocations " "doesn't have a value of Success. However, not enough invocations failed for the status to be Failed.", "Cancelled": "The command was canceled before it was completed.", "Canceled": "The command was canceled before it was completed.", # AWS typo, British English (canceled) "Rate Exceeded": "The number of managed nodes targeted by the command exceeded the account quota for pending invocations. " "The system has canceled the command before executing it on any node.", "Access Denied": "The user or role initiating the command doesn't have access to the targeted resource group. AccessDenied " "doesn't count against the parent command's max-errors limit, " "but does contribute to whether the parent command status is Success or Failed.", "No Instances In Tag": "The tag key-pair value or resource group targeted by the command doesn't match any managed nodes. ", "TimedOut": "A step or approval wasn't completed before the specified timeout period.", } def handle_port_range(args: dict) -> tuple: """ Parse and extract port range information from command arguments. Handles port specification in multiple formats: - Individual from_port and to_port arguments - Single port argument that can be a port number or range (e.g., "80" or "80-443") Args: args (dict): Command arguments dictionary containing port specifications Returns: tuple: A tuple containing (from_port, to_port) as integers, or (None, None) if no ports specified """ from_port = arg_to_number(args.get("from_port")) to_port = arg_to_number(args.get("to_port")) def parse_port_range(port: str) -> tuple[Optional[int], Optional[int]]: """Parse port argument which can be a single port or range (min-max).""" if not port: return None, None if "-" in port: from_port, to_port = port.split("-", 1) return int(from_port.strip()), int(to_port.strip()) else: _port: int = int(port.strip()) return _port, _port if args.get("port") and (not from_port and not to_port): from_port, to_port = parse_port_range(args.get("port", "")) return from_port, to_port def serialize_response_with_datetime_encoding(response: Dict[str, Any]) -> Dict[str, Any]: """ Serialize AWS API response with proper datetime encoding for JSON compatibility. Args: response (Dict[str, Any]): Raw AWS API response containing datetime objects Returns: Dict[str, Any]: Serialized response with datetime objects converted to strings Raises: DemistoException: If serialization fails """ try: # Use DatetimeEncoder to handle datetime objects serialized_json = json.dumps(response, cls=DatetimeEncoder) return json.loads(serialized_json) except (ValueError, TypeError) as e: demisto.error(f"Failed to serialize response with datetime encoding: {str(e)}") except Exception as e: demisto.error(f"Unexpected error during response serialization: {str(e)}") return response def process_instance_data(instance: Dict[str, Any]) -> Dict[str, Any]: """ Process and extract relevant data from a single EC2 instance. Args: instance (Dict[str, Any]): Raw instance data from AWS API Returns: Dict[str, Any]: Processed instance data """ instance_data = { "InstanceId": instance.get("InstanceId"), "ImageId": instance.get("ImageId"), "State": instance.get("State", {}).get("Name"), "PublicIPAddress": instance.get("PublicIpAddress"), "PrivateIpAddress": instance.get("PrivateIpAddress"), "Type": instance.get("InstanceType"), "LaunchDate": instance.get("LaunchTime"), "PublicDNSName": instance.get("PublicDnsName"), "Monitoring": instance.get("Monitoring", {}).get("State"), "AvailabilityZone": instance.get("Placement", {}).get("AvailabilityZone"), } instance_data = remove_empty_elements(instance_data) return instance_data def build_pagination_kwargs( args: Dict[str, Any], minimum_limit: int = 1, max_limit: int = MAX_LIMIT_VALUE, next_token_name: str = "NextToken", limit_name: str = "MaxResults", ) -> Dict[str, Any]: """ Build pagination parameters for AWS API calls with proper validation and limits. Args: args (Dict[str, Any]): Command arguments containing pagination parameters minimum_limit (int): The minimum possible limit for the pagination command. max_limit (int): The maximum possible limit for the pagination command. next_token_name (str): The name of the next token argument in AWS. limit_name (str): The name of the limit argument in AWS. Returns: Dict[str, Any]: Validated pagination parameters for AWS API Raises: ValueError: If limit exceeds maximum allowed value or is invalid """ kwargs: Dict[str, Any] = {} limit_arg = args.get("limit") # Parse and validate limit try: if limit_arg is not None: limit = arg_to_number(limit_arg) else: limit = DEFAULT_LIMIT_VALUE # Default limit except (ValueError, TypeError) as e: raise ValueError(f"Invalid limit parameter: {limit_arg}. Must be a valid number.") from e # Validate limit lower constraints if limit is not None and limit < minimum_limit: raise ValueError(f"Limit must be at least {minimum_limit}.") # AWS API upper constraints if limit is not None and limit > max_limit: demisto.debug(f"Requested limit {limit} exceeds maximum {max_limit}, using {max_limit}") limit = max_limit # Handle pagination with next_token (for continuing previous requests) if next_token := args.get("next_token"): if (not isinstance(next_token, str)) or (len(next_token.strip()) == 0): raise ValueError("next_token must be a non-empty string") kwargs[next_token_name] = next_token.strip() kwargs.update({limit_name: limit}) return kwargs def validate_iso8601_date(dt: str | None) -> str | None: """ Validates that a date string matches the AWS UTC timestamp format and returns it unchanged. AWS requires timestamps in UTC format: YYYY-MM-DDTHH:MM:SSZ (e.g. '2024-01-15T10:30:00Z'). Args: dt (str | None): A date string expected in AWS UTC format (YYYY-MM-DDTHH:MM:SSZ). Returns: str | None: The original date string if valid, or None if dt is falsy. Raises: DemistoException: If the date string does not match the required AWS UTC format. """ if not dt: return None try: datetime.strptime(dt, "%Y-%m-%dT%H:%M:%SZ") except ValueError as e: raise DemistoException( f"Invalid date format: '{dt}'. " f"Expected AWS UTC format: YYYY-MM-DDTHH:MM:SSZ (e.g. '2024-01-15T10:30:00Z').\n{e}" ) return dt def parse_resource_ids(resource_id: str | None) -> list[str]: if resource_id is None: raise ValueError("Resource ID cannot be empty") id_list = resource_id.replace(" ", "") resource_ids = id_list.split(",") return resource_ids def parse_filter_field(filter_string: str | None): """ Parses a list representation of name and values with the form of 'name=,values=. You can specify up to 50 filters and up to 200 values per filter in a single request. Filter strings can be up to 255 characters in length. Args: filter_string: The name and values list Returns: A list of dicts with the form {"Name": , "Values": []} """ filters = [] list_filters = argToList(filter_string, separator=";") if len(list_filters) > MAX_FILTERS: list_filters = list_filters[0:50] demisto.debug("Number of filter is larger then 50, parsing only first 50 filters.") regex = re.compile( r"^name=([\w:.-]+),values=([ \w@,.*-\/:]+)", flags=re.I, ) for filter in list_filters: match_filter = regex.match(filter) if match_filter is None: raise ValueError( f"Could not parse field: {filter}. Please make sure you provided " "like so: name=,values=;name=,values=,..." ) demisto.debug( f'Number of filter values for filter {match_filter.group(1)} is {len(match_filter.group(2).split(","))}' f' if larger than {MAX_FILTER_VALUES},' f' parsing only first {MAX_FILTER_VALUES} values.' ) filters.append({"Name": match_filter.group(1), "Values": match_filter.group(2).split(",")[0:MAX_FILTER_VALUES]}) return filters def parse_target_field(target_string: str | None): """ Parses a list representation of key and values with the form of 'key=,values=. the maximum number of values for a key might be lower than the global maximum of 50. Key minimum length of 1, maximum length of 163. Args: target_string (str): The key and values list Returns: A list of dicts with the form {"Key": , "Values": []} """ targets = [] list_targets = argToList(target_string, separator=";") regex = re.compile( r"^key=(^[\\p{L}\\p{Z}\\p{N}_.:/=\-@]*$|resource-groups:ResourceTypeFilters|resource-groups:Name)," r"values=([ \w@,.*-\/:]+)", flags=re.I, ) for target in list_targets: match_target = regex.match(target) if match_target is None: raise ValueError( f"Could not parse target: {target}. Please make sure you provided " "like so: key=,values=;key=,values=,... And the key matches the regex " "pattern required by AWS documentation. For more information see " "https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_Target.html#API_Target_Contents" ) demisto.debug( f'Number of target values for {match_target.group(1)} is {len(match_target.group(2).split(","))}' f' if larger than {MAX_TARGET_VALUES},' f' parsing only first {MAX_TARGET_VALUES} values.' ) targets.append({"Key": match_target.group(1), "Values": match_target.group(2).split(",")[0:MAX_TARGET_VALUES]}) return targets def parse_key_values_2_dict(parameters_str: str) -> dict: """ Parses a list representation of key and values 'key=,values=,;key=,values=,. Args: parameters_str (str): The key and values list Returns: A dictionary containing the parameters {"key1" : [ "string", "string"], "key2" : [ "string", "string"]} """ if not parameters_str: return {} parameters = {} list_parameters = argToList(parameters_str, separator=";") regex = re.compile( r"^key=([\w:.-]+),values=([ \w@,.*-\/:]+)", flags=re.I, ) for param in list_parameters: match_param = regex.match(param) if match_param is None: raise ValueError( f"Could not parse the parameter: {param}. Please make sure you provided " "like so: key=,values=;key=,values=,..." ) demisto.debug(f'Number of parameter values for filter {match_param.group(1)} is {len(match_param.group(2).split(","))}') parameters[match_param.group(1)] = match_param.group(2).split(",") return parameters def parse_key_1_value_to_dict(key_value_str: str) -> dict: """ Parses a list representation of key and value 'key=,value=;key=,value=. Args: key_value_str (str): The key and values list Returns: A dictionary containing the parameters {"key1" : "value", "key2" : "value"} """ if not key_value_str: return {} results = {} list_variables = argToList(key_value_str, separator=";") regex = re.compile( r"^key=(\b[^0-9][\w]+),value=([ \w@,.*-\/:]+)", flags=re.I, ) for var in list_variables: match_var = regex.match(var) if match_var is None: raise ValueError( f"Could not parse the parameter: {var}. Please make sure you provided " "like so: key=,value=;key=,value=..." ) results[match_var.group(1)] = match_var.group(2) return results def parse_name_value_type_format_filter(filter_string: str | None): """ Parses a list representation of key, values and type with the form of 'key=,values=,type='. You can specify up to 50 filters, up to 200 values, and 1 type per filter in a single request. Args: filter_string: The key, values and type list Returns: A list of dicts with the form {"Key": , "Values": [], "Type": } """ filters = [] list_filters = argToList(filter_string, separator=";") if len(list_filters) > MAX_TRIPLE_FILTER_VALUE: list_filters = list_filters[0:MAX_TRIPLE_FILTER_VALUE] demisto.debug( f"Number of filter is larger then {MAX_TRIPLE_FILTER_VALUE}, parsing only first {MAX_TRIPLE_FILTER_VALUE} filters." ) regex = re.compile( r"^key=([\w:.-]+),values=([ \w@,.*-\/:]+),type=([\w:.-]+)", flags=re.I, ) for f in list_filters: match_filter = regex.match(f) if match_filter is None: raise ValueError( f"Could not parse field: {f}. Please make sure you provided " "like so: key=,values=,type=;key=,values=,,type=..." ) demisto.debug( f'Number of filter values for filter {match_filter.group(1)} is {len(match_filter.group(2).split(","))}' f' if larger than {MAX_FILTER_VALUES},' f' parsing only first {MAX_FILTER_VALUES} values.' ) filters.append( { "Key": match_filter.group(1), "Values": match_filter.group(2).split(",")[0:MAX_FILTER_VALUES], "Type": match_filter.group(3), } ) return filters def parse_tag_field(tags_string: str | None) -> list: """ Parses a list representation of key and value with the form of 'key=,value=. You can specify up to 50 tags per resource. Args: tags_string: The name and value list Returns: A list of dicts with the form {"key": , "value": } """ tags = [] list_tags = argToList(tags_string, separator=";") if len(list_tags) > MAX_TAGS: list_tags = list_tags[0:50] demisto.debug("Number of tags is larger then 50, parsing only first 50 tags.") # According to the AWS Tag restrictions docs. regex = re.compile(r"^key=([a-zA-Z0-9\s+\-=._:/@]{1,128}),value=(.{0,256})$", flags=re.UNICODE) for tag in list_tags: match_tag = regex.match(tag) if match_tag is None: raise ValueError( f"Could not parse field: {tag}. Please make sure you provided like so: key=abc,value=123;key=fed,value=456" ) tags.append({"Key": match_tag.group(1), "Value": match_tag.group(2)}) return tags def parse_resource_arn_priority_field(refs_string: str | None) -> list: """ Parses a list representation of stateless rule group references with the form of 'ResourceArn=,Priority=;ResourceArn=,Priority='. Args: refs_string: The references list string. Returns: A list of dicts with the form {"ResourceArn": , "Priority": }. """ references: list = [] list_refs = argToList(refs_string, separator=";") regex = re.compile(r"^ResourceArn=(arn:aws.*),Priority=(\d+)$", flags=re.UNICODE) for ref in list_refs: match_ref = regex.match(ref) if match_ref is None: raise ValueError( f"Could not parse field: {ref}. Please make sure you provided like so: " "ResourceArn=arn:aws1,Priority=priority1;ResourceArn=arn:aws2,Priority=priority2" ) references.append({"ResourceArn": match_ref.group(1), "Priority": int(match_ref.group(2))}) return references def parse_key_value_items_field(items_string: str | None, required_key: str, format_hint: str) -> list[dict]: """ Parses a list representation of items with the form of 'Key1=,Key2=;Key1=,Key2='. Each item is separated by ';' and its fields by ','. Every field must be provided as 'Key=Value', and the given required_key must be present in each item. Args: items_string: The items list string. required_key: The key that must be present in each parsed item. format_hint: An example of the expected format, appended to error messages. Returns: A list of dicts, each mapping the parsed field keys to their values. """ items: list[dict] = [] for item in argToList(items_string, separator=";"): fields: dict = {} for field in argToList(item, separator=","): key, sep, value = field.partition("=") if not sep or not value: raise ValueError(f"Could not parse field: {item}. Please make sure you provided like so: {format_hint}") fields[key.strip()] = value.strip() if required_key not in fields: raise ValueError(f"Could not parse field: {item}. {required_key} is required for each item.") items.append(fields) return items def parse_stateful_rule_group_references_field(refs_string: str | None) -> list: """ Parses a list representation of stateful rule group references with the form of 'ResourceArn=,Priority=,Override=,DeepThreatInspection=;ResourceArn=,Priority='. Each reference is separated by ';' and its fields by ','. Only ResourceArn is required; Priority, Override (mapped to {"Action": }) and DeepThreatInspection are optional and dropped when not provided. Args: refs_string: The references list string. Returns: A list of dicts with the form {"ResourceArn": , "Priority": , "Override": {"Action": }, "DeepThreatInspection": }. """ format_hint = ( "ResourceArn=arn:aws1,Priority=priority1,Override=action1,DeepThreatInspection=true;" "ResourceArn=arn:aws2,Priority=priority2" ) references: list = [] for fields in parse_key_value_items_field(refs_string, required_key="ResourceArn", format_hint=format_hint): if not re.match(r"^arn:aws", fields["ResourceArn"]): raise ValueError(f"Could not parse field: {fields}. ResourceArn must be a valid ARN starting with 'arn:aws'.") reference = { "ResourceArn": fields.get("ResourceArn"), "Priority": arg_to_number(fields.get("Priority")), "Override": {"Action": fields.get("Override")}, "DeepThreatInspection": arg_to_bool_or_none(fields.get("DeepThreatInspection")), } references.append(remove_empty_elements(reference)) return references def parse_subnet_mappings_field(mappings_string: str | None) -> list: """ Parses a list representation of subnet mappings with the form of 'SubnetId=,IPAddressType=;SubnetId=,IPAddressType='. Each mapping is separated by ';' and its fields by ','. Only SubnetId is required; IPAddressType is optional and dropped when not provided. Args: mappings_string: The subnet mappings list string. Returns: A list of dicts with the form {"SubnetId": , "IPAddressType": }. """ format_hint = "SubnetId=id1,IPAddressType=type1;SubnetId=id2,IPAddressType=type2" mappings: list = [] for fields in parse_key_value_items_field(mappings_string, required_key="SubnetId", format_hint=format_hint): subnet_mapping = { "SubnetId": fields.get("SubnetId"), "IPAddressType": fields.get("IPAddressType"), } mappings.append(remove_empty_elements(subnet_mapping)) return mappings def convert_datetimes_to_iso_safe(data): """ Converts datetime objects in a data structure to ISO 8601 strings by serializing to and then deserializing from JSON using a custom encoder. """ json_string = json.dumps(data, cls=ISOEncoder) return json.loads(json_string) def read_zip_to_bytes(filename: str) -> bytes: """ Reads the entire zip file into a bytes object in chunks. Args: filename: Path to the zip file. Returns: A bytes object containing the complete zip file content. Raises: DemistoException: If an error occurs while reading the file. """ try: with open(filename, "rb") as zip_file: data = b"" for chunk in iter(lambda: zip_file.read(1024), b""): data += chunk return data except Exception as e: demisto.error(f"Failed to read zip file '{filename}': {str(e)}") raise DemistoException(f"Failed to read zip file '{filename}': {str(e)}") def prepare_create_function_kwargs(args: Dict[str, Any]) -> Dict[str, Any]: """ Prepare arguments to be sent to the Lambda CreateFunction API. Args: args: Command arguments dictionary Returns: Dictionary of kwargs ready for create_function API call """ create_function_api_keys = ["FunctionName", "Runtime", "Role", "Handler", "Description", "PackageType"] if code_path := args.get("code"): file_path = demisto.getFilePath(code_path).get("path") method_code = read_zip_to_bytes(file_path) code = {"ZipFile": method_code} elif s3_bucket := args.get("s3_bucket"): code = {"S3Bucket": s3_bucket} else: raise DemistoException("code or s3_bucket must be provided.") # Parse environment variables using parse_tag_field and convert to dictionary env_vars = None if args.get("environment"): parsed_env = parse_tag_field(args.get("environment")) env_vars = {item["Key"]: item["Value"] for item in parsed_env} kwargs: Dict[str, Any] = { "Code": code, "TracingConfig": {"Mode": args.get("tracing_config") or "Active"}, "MemorySize": arg_to_number(args.get("memory_size")) or 128, "Timeout": arg_to_number(args.get("function_timeout")) or 3, "Publish": arg_to_bool_or_none(args.get("publish")), "Environment": {"Variables": env_vars} if env_vars else None, "Tags": parse_tag_field(args.get("tags")) if args.get("tags") else None, "Layers": argToList(args.get("layers")), "VpcConfig": { "SubnetIds": argToList(args.get("subnet_ids")), "SecurityGroupIds": argToList(args.get("security_group_ids")), "Ipv6AllowedForDualStack": arg_to_bool_or_none(args.get("ipv6_allowed_for_dual_stack")), }, } for key in create_function_api_keys: arg_name = camel_case_to_underscore(key) if arg_name in args: kwargs.update({key: args.get(arg_name)}) return remove_empty_elements(kwargs) def aws_ec2_block_device_mapping_args_builder(args: Dict[str, Any]) -> List[Dict[str, Any]]: """ Builds the BlockDeviceMappings list for EC2 launch template and fleet commands. Constructs a single block device mapping entry from the provided arguments, including EBS volume configuration and device naming options. Args: args (Dict[str, Any]): The command arguments containing block device mapping fields. Returns: List[Dict[str, Any]]: A list containing a single block device mapping dictionary. """ return [ { "DeviceName": args.get("device_name"), "Ebs": { "Encrypted": arg_to_bool_or_none(args.get("ebs_encrypted")), "DeleteOnTermination": arg_to_bool_or_none(args.get("ebs_delete_on_termination")), "Iops": arg_to_number(args.get("ebs_iops")), "KmsKeyId": args.get("ebs_kms_key_id"), "SnapshotId": args.get("ebs_snapshot_id"), "VolumeSize": arg_to_number(args.get("ebs_volume_size")), "VolumeType": args.get("ebs_volume_type"), "Throughput": arg_to_number(args.get("ebs_throughput")), "EbsCardIndex": arg_to_number(args.get("ebs_card_index")), "VolumeInitializationRate": arg_to_number(args.get("ebs_initialization_rate")), }, "NoDevice": args.get("block_device_mappings_no_device"), "VirtualName": args.get("block_device_mappings_virtual_name"), } ] def create_launch_template_kwargs_builder(args: Dict[str, Any]) -> Dict[str, Any]: """ Builds the kwargs dictionary for EC2 launch template create and modify commands. Constructs the full request payload including launch template metadata, block device mappings, IAM instance profile, instance market options (Spot), network interfaces, placement, security groups, monitoring, and tag specifications. Args: args (Dict[str, Any]): The command arguments containing launch template fields. Returns: Dict[str, Any]: A dictionary of kwargs. """ kwargs: Dict[str, Any] = { "LaunchTemplateName": args.get("launch_template_name"), "VersionDescription": args.get("version_description"), "LaunchTemplateData": { "BlockDeviceMappings": aws_ec2_block_device_mapping_args_builder(args), "DisableApiTermination": arg_to_bool_or_none(args.get("disable_api_termination")), "EbsOptimized": arg_to_bool_or_none(args.get("ebs_optimized")), "IamInstanceProfile": {"Arn": args.get("iam_instance_profile_arn"), "Name": args.get("iam_instance_profile_name")}, "ImageId": args.get("image_id"), "InstanceInitiatedShutdownBehavior": args.get("instance_initiated_shutdown_behavior"), "InstanceMarketOptions": { "MarketType": args.get("market_type"), "SpotOptions": { "InstanceInterruptionBehavior": args.get("spot_options_instance_interruption_behavior"), "MaxPrice": args.get("spot_options_max_price"), "SpotInstanceType": args.get("spot_options_instance_type"), }, }, "InstanceType": args.get("instance_type"), "KernelId": args.get("kernel_id"), "KeyName": args.get("key_name"), "Monitoring": {"Enabled": arg_to_bool_or_none(args.get("monitoring"))}, "NetworkInterfaces": [ { "AssociatePublicIpAddress": arg_to_bool_or_none(args.get("network_interfaces_associate_public_ip_address")), "DeleteOnTermination": arg_to_bool_or_none(args.get("network_interfaces_delete_on_termination")), "Description": args.get("network_interfaces_description"), "DeviceIndex": arg_to_number(args.get("network_interfaces_device_index")), "Groups": argToList(args.get("network_interface_groups")), "SubnetId": args.get("subnet_id"), "PrivateIpAddress": args.get("private_ip_address"), "Ipv6AddressCount": arg_to_number(args.get("ipv6_address_count")), "Ipv6Addresses": argToList(args.get("ipv6_addresses")), "NetworkInterfaceId": args.get("network_interface_id"), } ], "Placement": {"AvailabilityZone": args.get("availability_zone"), "Tenancy": args.get("placement_tenancy")}, "RamDiskId": args.get("ram_disk_id"), "SecurityGroups": argToList(args.get("security_groups")), "SecurityGroupIds": argToList(args.get("security_group_ids")), "UserData": args.get("user_data"), }, } if args.get("tags"): kwargs["TagSpecifications"] = [{"ResourceType": "launch-template", "Tags": parse_tag_field(args.get("tags"))}] return kwargs def build_kwargs_network_interface_attribute(args: dict, network_interface_id: str) -> dict: """ Build the kwargs for network_interface_attribute_modify_command. Args: args (dict): The command arguments. network_interface_id (str): the network interface id. Returns: A dictionary with the relevant values. """ attachment_id = args.get("attachment_id") delete_on_termination = arg_to_bool_or_none(args.get("delete_on_termination")) if (attachment_id and delete_on_termination is None) or (not attachment_id and delete_on_termination is not None): raise DemistoException( "If one of the arguments 'attachment_id' or 'delete_on_termination' is given, the other one must be given as well." ) kwargs = { "EnaSrdSpecification": { "EnaSrdEnabled": arg_to_bool_or_none(args.get("ena_srd_enabled")), "EnaSrdUdpSpecification": {"EnaSrdUdpEnabled": arg_to_bool_or_none(args.get("ena_srd_udp_enabled"))}, }, "EnablePrimaryIpv6": arg_to_bool_or_none(args.get("enable_primary_ipv6")), "ConnectionTrackingSpecification": { "TcpEstablishedTimeout": arg_to_number(args.get("tcp_established_timeout")), "UdpStreamTimeout": arg_to_number(args.get("udp_stream_timeout")), "UdpTimeout": arg_to_number(args.get("udp_timeout")), }, "AssociatePublicIpAddress": arg_to_bool_or_none(args.get("associate_public_ip_address")), "AssociatedSubnetIds": argToList(args.get("associated_subnet_ids")), "NetworkInterfaceId": network_interface_id, "Groups": argToList(args.get("groups")), "Description": {"Value": args.get("description")}, "SourceDestCheck": { "Value": arg_to_bool_or_none(args.get("source_dest_check")), }, "Attachment": { "DefaultEnaQueueCount": arg_to_bool_or_none(args.get("default_ena_queue_count")), "EnaQueueCount": arg_to_number(args.get("ena_queue_count")), "AttachmentId": attachment_id, "DeleteOnTermination": delete_on_termination, }, } kwargs = remove_empty_elements(kwargs) demisto.debug(f"After remove_empty_elements: {kwargs}") return kwargs def build_kwargs_lambda_function_config_update(args: dict) -> dict: """Build kwargs for aws-lambda-update-function-configuration command. Args: args: A dict of arguments for the command. Returns: A dict of kwargs. """ function_name = args.get("function_name") execution_env_memory_per_cpu = args.get("execution_env_memory_per_cpu") kwargs = { "FunctionName": function_name, "Role": args.get("role"), "Handler": args.get("handler"), "Description": args.get("description"), "Timeout": arg_to_number(args.get("timeout")), "MemorySize": arg_to_number(args.get("memory_size")), "VpcConfig": { "SubnetIds": argToList(args.get("subnet_ids")), "SecurityGroupIds": argToList(args.get("security_group_ids")), "Ipv6AllowedForDualStack": arg_to_bool_or_none(args.get("ipv6_allowed_for_dualstack")), }, "Environment": {"Variables": parse_key_1_value_to_dict(args.get("environment", ""))}, "Runtime": args.get("runtime"), "DeadLetterConfig": {"TargetArn": args.get("target_arn")}, "KMSKeyArn": args.get("kms_key_arn"), "TracingConfig": { "Mode": args.get("tracing_config_mode"), }, "RevisionId": args.get("revision_id"), "Layers": argToList(args.get("layers")), "ImageConfig": { "EntryPoint": argToList(args.get("image_config_entry_point")), "Command": argToList(args.get("image_config_command")), "WorkingDirectory": args.get("image_config_working_directory"), }, "EphemeralStorage": {"Size": arg_to_number(args.get("ephemeral_storage_size"))}, "SnapStart": {"ApplyOn": args.get("snap_start_apply_on")}, "LoggingConfig": { "LogFormat": args.get("log_format"), "ApplicationLogLevel": args.get("application_log_level"), "SystemLogLevel": args.get("system_log_level"), "LogGroup": args.get("log_group"), }, "CapacityProviderConfig": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": args.get("capacity_provider_arn"), "PerExecutionEnvironmentMaxConcurrency": arg_to_number(args.get("per_execution_env_max_concurrency")), "ExecutionEnvironmentMemoryGiBPerVCpu": float(execution_env_memory_per_cpu) if execution_env_memory_per_cpu else None, } }, "DurableConfig": { "RetentionPeriodInDays": arg_to_number(args.get("durable_retention_period")), "ExecutionTimeout": arg_to_number(args.get("durable_execution_timeout")), }, } if "file_system_configs" in args: key_value_list = parse_tag_field(args.get("file_system_configs")) arn_local_mount_path_list = [] for key_value in key_value_list: arn_local_mount_path_list.append({"Arn": key_value["Key"], "LocalMountPath": key_value["Value"]}) kwargs["FileSystemConfigs"] = arn_local_mount_path_list return kwargs def aws_ec2_fleet_command_launch_templates_config_args_builder(args: Dict[str, Any]) -> List[Dict[str, Any]]: """ Builds the LaunchTemplateConfigs list for EC2 Fleet create/modify commands. For the Overrides list, each override field (availability_zone, instance_type, etc.) is read as a single value. Multiple overrides are not supported via this builder. Args: args (Dict[str, Any]): The command arguments containing launch template specification fields and override fields. Returns: List[Dict[str, Any]]: A list containing a single LaunchTemplateConfig dict with ``LaunchTemplateSpecification`` and ``Overrides``, with empty/None values removed. """ override = [ { "AvailabilityZone": args.get("availability_zone"), "AvailabilityZoneId": args.get("availability_zone_id"), "ImageId": args.get("image_id"), "InstanceType": args.get("instance_type"), "MaxPrice": args.get("max_price"), "Placement": { "GroupId": args.get("placement_group_id"), "GroupName": args.get("placement_group_name"), }, "Priority": arg_to_number(args.get("priority")), "SubnetId": args.get("subnet_id"), "WeightedCapacity": arg_to_number(args.get("weighted_capacity")), "BlockDeviceMappings": aws_ec2_block_device_mapping_args_builder(args), } ] return [ remove_empty_elements( { "LaunchTemplateSpecification": { "LaunchTemplateId": args.get("launch_template_id"), "LaunchTemplateName": args.get("launch_template_name"), "Version": args.get("launch_template_version"), }, "Overrides": override, } ) ] def aws_ec2_fleet_create_args_builder(args: Dict[str, Any]) -> Dict[str, Any]: """ Builds the full kwargs dictionary for the EC2 Fleet create command. Constructs the complete request payload including fleet type, capacity termination policies, validity window, launch template configurations, Spot options, On-Demand options, target capacity specification, and tag specifications. Args: args (Dict[str, Any]): The command arguments containing fleet configuration. Returns: Dict[str, Any]: A dictionary of kwargs. """ return { "ExcessCapacityTerminationPolicy": args.get("excess_capacity_termination_policy"), "ReplaceUnhealthyInstances": arg_to_bool_or_none(args.get("replace_unhealthy_instances")), "TerminateInstancesWithExpiration": arg_to_bool_or_none(args.get("terminate_instances_with_expiration")), "Type": args.get("type"), "ValidFrom": validate_iso8601_date(args.get("valid_from")), "ValidUntil": validate_iso8601_date(args.get("valid_until")), "LaunchTemplateConfigs": aws_ec2_fleet_command_launch_templates_config_args_builder(args), "SpotOptions": { "AllocationStrategy": args.get("spot_allocation_strategy"), "InstanceInterruptionBehavior": args.get("instance_interruption_behavior"), "InstancePoolsToUseCount": arg_to_number(args.get("instance_pools_to_use_count")), "SingleInstanceType": arg_to_bool_or_none(args.get("spot_single_instance_type")), "SingleAvailabilityZone": arg_to_bool_or_none(args.get("single_availability_zone")), "MinTargetCapacity": arg_to_number(args.get("min_target_capacity")), "MaxTotalPrice": args.get("max_total_price"), "MaintenanceStrategies": { "CapacityRebalance": { "ReplacementStrategy": args.get("capacity_rebalance_replacement_strategy"), "TerminationDelay": arg_to_number(args.get("capacity_rebalance_termination_delay")), } }, }, "OnDemandOptions": { "AllocationStrategy": args.get("on_demand_allocation_strategy"), "SingleInstanceType": arg_to_bool_or_none(args.get("on_demand_single_instance_type")), "SingleAvailabilityZone": arg_to_bool_or_none(args.get("on_demand_single_availability_zone")), "MinTargetCapacity": arg_to_number(args.get("on_demand_min_target_capacity")), "MaxTotalPrice": args.get("on_demand_max_total_price"), "CapacityReservationOptions": {"UsageStrategy": args.get("capacity_reservation_strategy")}, }, "TargetCapacitySpecification": { "TotalTargetCapacity": arg_to_number(args.get("total_target_capacity")), "DefaultTargetCapacityType": args.get("default_target_capacity_type"), "OnDemandTargetCapacity": arg_to_number(args.get("on_demand_target_capacity")), "SpotTargetCapacity": arg_to_number(args.get("spot_target_capacity")), "TargetCapacityUnitType": args.get("target_capacity_unit"), }, "TagSpecifications": [{"ResourceType": "fleet", "Tags": parse_tag_field(args.get("tags"))}] if args.get("tags") else None, } def validate_network_firewall_identifier(args: dict, obj: str): """ Validates that at least one of the network firewall identifiers is provided and raises a DemistoException otherwise. Args: args (dict): The command arguments dictionary containing the firewall identifiers. obj (str): The identifier object (firewall, firewall policy, etc.). """ if f"{obj}_name" not in args and f"{obj}_arn" not in args: raise DemistoException("Please enter at least one of the network firewall identifier arguments.") def create_network_firewall_policy_obj(args: dict) -> dict: """ Builds an AWS Network Firewall policy object from the provided command arguments. Parses the JSON-string arguments (stateless custom actions and policy rule variables), the comma/semicolon-separated stateful rule group references, assembles the firewall policy structure, and removes any empty elements before returning it. Args: args (dict): The command arguments containing the firewall policy configuration. Returns: dict: A dictionary representing the Network Firewall policy, with empty elements removed. """ stateless_custom_actions_raw = args.get("stateless_custom_actions") rule_variables_raw = args.get("policy_rule_variables") stateless_custom_actions = parse_json_string(stateless_custom_actions_raw) if stateless_custom_actions_raw else None rule_variables = parse_json_string(rule_variables_raw) if rule_variables_raw else None firewall_policy_object = remove_empty_elements( { "StatelessRuleGroupReferences": parse_resource_arn_priority_field(args.get("stateless_rule_group_references")), "StatelessDefaultActions": argToList(args.get("stateless_default_actions")), "StatelessFragmentDefaultActions": argToList(args.get("stateless_fragment_default_actions")), "StatelessCustomActions": stateless_custom_actions, "StatefulRuleGroupReferences": parse_stateful_rule_group_references_field(args.get("stateful_rule_group_references")), "StatefulDefaultActions": argToList(args.get("stateful_default_actions")), "StatefulEngineOptions": { "RuleOrder": args.get("stateful_engine_options_rule_order"), "StreamExceptionPolicy": args.get("stateful_engine_options_stream_exception_policy"), "FlowTimeouts": { "TcpIdleTimeoutSeconds": arg_to_number(args.get("stateful_engine_options_tcp_idle_timeout")), }, }, "TLSInspectionConfigurationArn": args.get("tls_inspection_configuration_arn"), "PolicyVariables": {"RuleVariables": rule_variables}, "EnableTLSSessionHolding": arg_to_bool_or_none(args.get("enable_tls_session_holding")), } ) if not firewall_policy_object: raise DemistoException("Please specify at least one of the characterize firewall policy arguments.") return firewall_policy_object def parse_json_arg(args: dict, arg_name: str) -> dict | list | None: """ Parses a single JSON-string command argument into its corresponding Python object, raising a clear per-argument error when the value is not valid JSON so the user knows exactly which argument to fix. Args: args (dict): The command arguments dictionary to read the value from. arg_name (str): The name of the command argument to read from ``args`` and parse as JSON. Returns: dict | list | None: The parsed Python object (typically a dict or list), or None if the argument is missing or empty. Raises: DemistoException: If the argument value is present but is not valid JSON. """ raw_value = args.get(arg_name) if not raw_value: return None try: return json.loads(raw_value) except json.JSONDecodeError as error: raise DemistoException(f"Invalid JSON in '{arg_name}': {error}") def create_rule_group_common_kwargs(args: dict) -> dict: """ Builds the common keyword arguments shared by the AWS Network Firewall rule group create and update API calls. Parses the JSON-string arguments (IP sets, port sets, IP set references, and rules source) and assembles the rule group structure, including rule variables, reference sets, encryption configuration, and source metadata. Args: args (dict): The command arguments containing the rule group configuration. Returns: dict: A dictionary of keyword arguments representing the Network Firewall rule group. """ encryption_configuration_key_id = args.get("encryption_configuration_key_id") encryption_configuration_key_type = args.get("encryption_configuration_key_type") if encryption_configuration_key_id and not encryption_configuration_key_type: raise DemistoException( "When configuring encryption_configuration_key_id a encryption_configuration_key_type must be supplied as well." ) ip_sets = parse_json_arg(args, "ip_sets") port_sets = parse_json_arg(args, "port_sets") ip_sets_references = parse_json_arg(args, "ip_sets_references") rules_source = parse_json_arg(args, "rules_source") rule_group_object = remove_empty_elements( { "RuleGroupName": args.get("rule_group_name"), "Type": args.get("type"), "RuleGroup": { "RuleVariables": { "IPSets": ip_sets, "PortSets": port_sets, }, "ReferenceSets": {"IPSetReferences": ip_sets_references}, "RulesSource": rules_source, "StatefulRuleOptions": {"RuleOrder": args.get("stateful_rule_options_rule_order")}, }, "Rules": args.get("rules"), "Description": args.get("description"), "EncryptionConfiguration": { "KeyId": args.get("encryption_configuration_key_id"), "Type": args.get("encryption_configuration_key_type"), }, "SourceMetadata": { "SourceArn": args.get("source_metadata_arn"), "SourceUpdateToken": args.get("source_metadata_update_token"), }, "AnalyzeRuleGroup": arg_to_bool_or_none(args.get("analyze_rule_group")), "SummaryConfiguration": {"RuleOptions": argToList(args.get("summary_configuration_rule_options"))}, } ) if ("Rules" in rule_group_object and "RuleGroup" in rule_group_object) or ( "Rules" not in rule_group_object and "RuleGroup" not in rule_group_object ): raise DemistoException( "You must provide either 'rules' argument or at least one of the 'rule_group' arguments (ip_sets, port_sets, " "ip_sets_references, rules_source, stateful_rule_options_rule_order), but not both" ) return rule_group_object class AWSErrorHandler: """ Centralized error handling for AWS boto3 client errors. Provides specialized handling for permission errors and general AWS API errors. """ # Permission-related error codes that should be handled specially PERMISSION_ERROR_CODES = [ "AccessDenied", "UnauthorizedOperation", "Forbidden", "AccessDeniedException", "UnauthorizedOperationException", "InsufficientPrivilegesException", "NotAuthorized", ] @classmethod def handle_response_error(cls, response: dict, account_id: str | None = None) -> None: """ Handle boto3 response errors. For permission errors, returns a structured error entry using return_error. For other errors, raises DemistoException with informative error message. Args: err (ClientError): The boto3 ClientError exception account_id (str, optional): AWS account ID. If not provided, will try to get from demisto.args() """ # Create informative error message detailed_error = ( f"AWS API Error occurred while executing: {demisto.command()} with arguments: {list(demisto.args().keys())}\n" f"Request Id: {response.get('ResponseMetadata',{}).get('RequestId', 'N/A')}\n" f"HTTP Status Code: {response.get('ResponseMetadata',{}).get('HTTPStatusCode', 'N/A')}" ) return_error(detailed_error) @classmethod def handle_client_error(cls, err: ClientError, account_id: str | None = None) -> None: """ Handle boto3 client errors with special handling for permission issues. For permission errors, returns a structured error entry using return_error. For other errors, raises DemistoException with informative error message. Args: err (ClientError): The boto3 ClientError exception account_id (str, optional): AWS account ID. If not provided, will try to get from demisto.args() """ try: demisto.debug(f"The original error message: {err}") error_code = err.response.get("Error", {}).get("Code") error_message = err.response.get("Error", {}).get("Message") http_status_code = err.response.get("ResponseMetadata", {}).get("HTTPStatusCode") demisto.debug(f"[AWSErrorHandler] Got a client error: {error_message}") if not error_code or not error_message or not http_status_code: return_error(err) # Check if this is a permission-related error if (error_code in cls.PERMISSION_ERROR_CODES) or (http_status_code in [401, 403]): cls._handle_permission_error(err, error_code, error_message, account_id) else: cls._handle_general_error(err, error_code, error_message) except Exception as e: demisto.debug(f"[AWSErrorHandler] Unhandled error: {str(e)}") return_error(err) @classmethod def _handle_permission_error( cls, err: ClientError, error_code: str, error_message: str, account_id: str | None = None ) -> None: """ Handle permission-related errors by returning structured error entry. Args: err (ClientError): The boto3 ClientError exception error_code (str): The AWS error code error_message (str): The AWS error message account_id (str, optional): AWS account ID """ # Get account_id from args if not provided if not account_id: account_id = demisto.args().get("account_id", "unknown") action = cls._extract_action_from_message(error_message) # When encountering an unauthorized error, an encoded authorization message may be returned with different # encoding each time. This will create different error entries for each unauthorized error and will confuse the user. # Therefore we will omit the actual encoded message. demisto.info(f"Original error message: {error_message}") error_entry = { "account_id": account_id, "message": cls.remove_encoded_authorization_message(error_message), "name": action, } demisto.debug(f"Permission error detected: {error_entry}") return_multiple_permissions_error([error_entry]) @classmethod def remove_encoded_authorization_message(cls, message: str) -> str: """ Remove encoded authorization messages from AWS error responses. Args: message (str): Original error message Returns: str: Cleaned error message without encoded authorization details """ index = message.lower().find("encoded authorization failure message:") if index != -1: # substring found return message[:index] else: return message @classmethod def _handle_general_error(cls, err: ClientError, error_code: str, error_message: str) -> None: """ Handle general (non-permission) errors with informative error messages. Args: err (ClientError): The boto3 ClientError exception error_code (str): The AWS error code error_message (str): The AWS error message """ # Get additional error details request_id = err.response.get("ResponseMetadata", {}).get("RequestId", "N/A") http_status = err.response.get("ResponseMetadata", {}).get("HTTPStatusCode", "N/A") # Create informative error message detailed_error = ( f"AWS API Error occurred while executing: {demisto.command()} with arguments: {list(demisto.args().keys())}\n" f"Error Code: {error_code}\n" f"Error Message: {error_message}\n" f"HTTP Status Code: {http_status}\n" f"Request ID: {request_id}" ) demisto.error(f"AWS API Error: {detailed_error}") return_error(detailed_error) @classmethod def _extract_action_from_message(cls, error_message: str) -> str: """ Extract AWS permission name from error message using regex patterns. Args: error_message (str): The AWS error message Returns: str: The extracted permission name or 'unknown' if not found """ # Sanitize input to prevent regex injection if not error_message or not isinstance(error_message, str): return "unknown" for action in REQUIRED_ACTIONS: try: match = re.search(action, error_message, re.IGNORECASE) if match and match.group(0) == action: return action except re.error: pass return "unknown" class AWSServices(str, Enum): S3 = "s3" IAM = "iam" EC2 = "ec2" RDS = "rds" EKS = "eks" LAMBDA = "lambda" CloudTrail = "cloudtrail" ECS = "ecs" ACM = "acm" KMS = "kms" ELB = "elb" CostExplorer = "ce" BUDGETS = "budgets" SSM = "ssm" Redshift = "redshift" CloudWatchLogs = "logs" NetworkFirewall = "network-firewall" STS = "sts" class DatetimeEncoder(json.JSONEncoder): # pylint: disable=method-hidden def default(self, obj): if isinstance(obj, datetime): return obj.strftime("%Y-%m-%dT%H:%M:%S") elif isinstance(obj, date): return obj.strftime("%Y-%m-%d") # Let the base class default method raise the TypeError return json.JSONEncoder.default(self, obj) class S3: service = AWSServices.S3 @staticmethod def put_public_access_block_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Create or Modify the PublicAccessBlock configuration for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including bucket name and access block settings Returns: CommandResults: Results of the operation with success/failure message """ kwargs: dict[str, Union[bool, None]] = { "BlockPublicAcls": argToBoolean(args.get("block_public_acls")) if args.get("block_public_acls") else None, "IgnorePublicAcls": argToBoolean(args.get("ignore_public_acls")) if args.get("ignore_public_acls") else None, "BlockPublicPolicy": argToBoolean(args.get("block_public_policy")) if args.get("block_public_policy") else None, "RestrictPublicBuckets": ( argToBoolean(args.get("restrict_public_buckets")) if args.get("restrict_public_buckets") else None ), } remove_nulls_from_dictionary(kwargs) response = client.put_public_access_block(Bucket=args.get("bucket"), PublicAccessBlockConfiguration=kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults(readable_output=f"Successfully applied public access block to the {args.get('bucket')} bucket") raise DemistoException(f"Couldn't apply public access block to the {args.get('bucket')} bucket. {json.dumps(response)}") @staticmethod def delete_bucket_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Delete an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket Returns: CommandResults: Results of the command execution. """ bucket = args.get("bucket") print_debug_logs(client, f"Deleting bucket: {bucket}") response = client.delete_bucket(Bucket=bucket) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.NO_CONTENT: return CommandResults(readable_output=f"Successfully deleted bucket '{bucket}'") else: return AWSErrorHandler.handle_response_error(response) @staticmethod def list_bucket_objects_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ List objects in an Amazon S3 bucket (up to 1000 objects). Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket - prefix (str): Limits the response to keys that begin with the specified prefix - delimiter (str): A delimiter is a character you use to group keys - limit (str): Sets the maximum number of keys returned in the response (default is 1000). - next_token (str): The marker for the next set of results (used for pagination). Returns: CommandResults: Results of the command execution including the list of objects and their metadata """ bucket = args.get("bucket") prefix = args.get("prefix") delimiter = args.get("delimiter") print_debug_logs(client, f"Listing objects from bucket: {bucket}") pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=1000, next_token_name="Marker", limit_name="MaxKeys" ) print_debug_logs(client, f"Created those pagination parameters {pagination_kwargs=}") kwargs = { "Bucket": bucket, "Prefix": prefix, "Delimiter": delimiter, } kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) try: response = client.list_objects(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response) serialized_response = serialize_response_with_datetime_encoding(response) contents = serialized_response.get("Contents", []) if not contents: return CommandResults(readable_output=f"No objects found in bucket {bucket}.") table_data = [] for obj in contents: table_data.append( { "Key": obj.get("Key"), "Size (Bytes)": obj.get("Size"), "LastModified": obj.get("LastModified"), "StorageClass": obj.get("StorageClass"), } ) human_readable = tableToMarkdown( f"AWS S3 Bucket Object for Bucket: {bucket}", table_data, headers=["Key", "Size (Bytes)", "LastModified", "StorageClass"], removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( outputs_prefix="AWS.S3.Buckets", outputs_key_field="BucketName", outputs={"BucketName": bucket, "ObjectsNextToken": serialized_response.get("NextMarker"), "Objects": contents}, readable_output=human_readable, ) except Exception as e: raise DemistoException(f"Failed to list objects for bucket {bucket}. Error: {str(e)}") @staticmethod def list_bucket_objects_v2_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ List objects in an Amazon S3 bucket (up to 1000 objects) using the ListObjectsV2 API. Unlike the legacy ListObjects API, ListObjectsV2 consistently returns a NextContinuationToken whenever the result set is truncated (regardless of whether a delimiter was supplied), resolving the truncated-result pagination issue when listing large buckets. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket - prefix (str): Limits the response to keys that begin with the specified prefix - delimiter (str): A delimiter is a character you use to group keys - start_after (str): The key to start listing after (all keys are listed alphabetically after it) - limit (str): Sets the maximum number of keys returned in the response (default is 50). - next_token (str): The continuation token for the next set of results (used for pagination). Returns: CommandResults: Results of the command execution including the list of objects and their metadata """ bucket = args.get("bucket") prefix = args.get("prefix") delimiter = args.get("delimiter") start_after = args.get("start_after") print_debug_logs(client, f"Listing objects (V2) from bucket: {bucket}") pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=1000, next_token_name="ContinuationToken", limit_name="MaxKeys" ) print_debug_logs( client, f"Pagination parameters: MaxKeys={pagination_kwargs.get('MaxKeys')}, " f"has_continuation_token={bool(pagination_kwargs.get('ContinuationToken'))}", ) kwargs = { "Bucket": bucket, "Prefix": prefix, "Delimiter": delimiter, "StartAfter": start_after, } kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) try: response = client.list_objects_v2(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response) serialized_response = serialize_response_with_datetime_encoding(response) contents = serialized_response.get("Contents", []) if not contents: return CommandResults(readable_output=f"No objects found in bucket {bucket}.") table_data = [] for obj in contents: table_data.append( { "Key": obj.get("Key"), "Size (Bytes)": obj.get("Size"), "LastModified": obj.get("LastModified"), "StorageClass": obj.get("StorageClass"), } ) human_readable = tableToMarkdown( f"AWS S3 Bucket Object for Bucket: {bucket}", table_data, headers=["Key", "Size (Bytes)", "LastModified", "StorageClass"], removeNull=True, headerTransform=pascalToSpace, ) outputs = { "AWS.S3.Buckets(val.BucketName && val.BucketName == obj.BucketName)": { "BucketName": bucket, "ObjectsV2": contents, "ObjectsV2NextToken": serialized_response.get("NextContinuationToken"), }, } return CommandResults( outputs=remove_empty_elements(outputs), readable_output=human_readable, raw_response=serialized_response, ) except Exception as e: raise DemistoException(f"Failed to list objects for bucket {bucket}. Error: {str(e)}") @staticmethod def put_bucket_versioning_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Set the versioning state of an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket - status (str): The versioning state of the bucket (Enabled or Suspended) - mfa_delete (str): Specifies whether MFA delete is enabled (Enabled or Disabled) Returns: CommandResults: Results of the command execution """ bucket: str = args.get("bucket", "") status: str = args.get("status", "") mfa_delete: str = args.get("mfa_delete", "") versioning_configuration = {"Status": status, "MFADelete": mfa_delete} remove_nulls_from_dictionary(versioning_configuration) try: response = client.put_bucket_versioning(Bucket=bucket, VersioningConfiguration=versioning_configuration) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully {status.lower()} versioning configuration for bucket `{bucket}`" ) raise DemistoException( f"Request completed but received unexpected status code: " f"{response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Failed to update versioning configuration for bucket {bucket}. Error: {str(e)}") @staticmethod def put_bucket_logging_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Enables/configures logging for an S3 bucket. Args: args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket to configure logging for - bucket-logging-status (str, optional): JSON string containing logging configuration - target-bucket (str, optional): The target bucket where logs will be stored - target_prefix (str, optional): The prefix for log objects in the target bucket Returns: CommandResults: Results of the command execution """ bucket = args["bucket"] try: if target_bucket := args.get("target_bucket"): # Build logging configuration. bucket_logging_status = { "LoggingEnabled": {"TargetBucket": target_bucket, "TargetPrefix": args.get("target_prefix", "")} } else: # If neither full config nor target bucket provided, disable logging bucket_logging_status = {} response = client.put_bucket_logging(Bucket=bucket, BucketLoggingStatus=bucket_logging_status) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: if bucket_logging_status.get("LoggingEnabled"): target_bucket = bucket_logging_status["LoggingEnabled"].get("TargetBucket", "") target_prefix = bucket_logging_status["LoggingEnabled"].get("TargetPrefix", "") return CommandResults( readable_output=( f"Successfully enabled logging for bucket '{bucket}'. " f"Logs will be stored in '{target_bucket}/{target_prefix}'." ) ) else: return CommandResults(readable_output=f"Successfully disabled logging for bucket '{bucket}'") raise DemistoException( f"Couldn't apply bucket policy to {args.get('bucket')} bucket. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}." f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Failed to configure logging for bucket '{bucket}'. Error: {str(e)}") @staticmethod def put_bucket_acl_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Set the Access Control List (ACL) permissions for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the bucket - acl (str): The canned ACL to apply (e.g., 'private', 'public-read', 'public-read-write') Returns: CommandResults: Results of the operation with success/failure message """ acl, bucket = args.get("acl"), args.get("bucket") response = client.put_bucket_acl(Bucket=bucket, ACL=acl) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults(readable_output=f"Successfully updated ACL for bucket {bucket} to '{acl}'") raise DemistoException( f"Request completed but received unexpected status code: " f"{response['ResponseMetadata']['HTTPStatusCode']}. {json.dumps(response)}" ) @staticmethod def put_bucket_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds or updates a bucket policy for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the S3 bucket to apply the policy to - policy (dict): The JSON policy document to be applied to the bucket - confirmRemoveSelfBucketAccess (str, optional): Confirms removal of self bucket access if set to "True" Returns: CommandResults: - On success: A result indicating the bucket policy was successfully applied - On failure: An error result with details about why the policy application failed Raises: Exception: If there's an error while applying the bucket policy """ kwargs = {"Bucket": args.get("bucket", ""), "Policy": json.dumps(args.get("policy"))} try: demisto.debug(f"calling put_bucket_policy with {kwargs=}") response = client.put_bucket_policy(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return CommandResults(readable_output=f"Successfully applied bucket policy to {args.get('bucket')} bucket") raise DemistoException( f"Couldn't apply bucket policy to {args.get('bucket')} bucket. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}." f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Couldn't apply bucket policy to {args.get('bucket')} bucket. Error: {str(e)}") @staticmethod def delete_bucket_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Delete the bucket policy from an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the S3 bucket Returns: CommandResults: Results of the delete operation with success/failure message """ bucket = args.get("bucket") print_debug_logs(client, f"Deleting bucket policy for bucket: {bucket}") response = client.delete_bucket_policy(Bucket=bucket) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.NO_CONTENT: return CommandResults(readable_output=f"Successfully deleted bucket policy from bucket '{bucket}'") else: return AWSErrorHandler.handle_response_error(response) @staticmethod def get_public_access_block_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Get the Public Access Block configuration for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the S3 bucket - expected_bucket_owner (Str): TThe account ID of the expected bucket owner. Returns: CommandResults: Results containing the Public Access Block configuration """ bucket_name = args.get("bucket") kwargs = {"Bucket": bucket_name, "ExpectedBucketOwner": args.get("expected_bucket_owner")} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Gets public access block for bucket: {bucket_name}") response = client.get_public_access_block(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( outputs_prefix="AWS.S3.Buckets", outputs_key_field="BucketName", outputs={"BucketName": bucket_name, "PublicAccessBlock": response.get("PublicAccessBlockConfiguration", {})}, readable_output=tableToMarkdown( "Public Access Block configuration", t=response.get("PublicAccessBlockConfiguration", {}), removeNull=True, headerTransform=pascalToSpace, ), ) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def get_bucket_encryption_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Get the encryption configuration for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the S3 bucket - expected_bucket_owner (Str): TThe account ID of the expected bucket owner. Returns: CommandResults: Results containing the bucket encryption configuration """ bucket_name = args.get("bucket") kwargs = {"Bucket": bucket_name, "ExpectedBucketOwner": args.get("expected_bucket_owner")} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Gets encryption configuration for an Amazon S3 bucket: {bucket_name}") response = client.get_bucket_encryption(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: server_side_encryption_rules = response.get("ServerSideEncryptionConfiguration", {}).get("Rules", []) outputs = { "BucketName": bucket_name, "ServerSideEncryptionConfiguration": response.get("ServerSideEncryptionConfiguration", {}), } return CommandResults( outputs_prefix="AWS.S3.Buckets", outputs_key_field="BucketName", outputs=outputs, readable_output=tableToMarkdown( f"Server Side Encryption Configuration for Bucket '{bucket_name}'", t=server_side_encryption_rules, removeNull=True, headerTransform=pascalToSpace, ), ) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def get_bucket_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Get the policy configuration for an Amazon S3 bucket. Args: client (BotoClient): The boto3 client for S3 service args (Dict[str, Any]): Command arguments including: - bucket (str): The name of the S3 bucket - expected_bucket_owner (Str): TThe account ID of the expected bucket owner. Returns: CommandResults: Results containing the bucket policy configuration """ bucket_name = args.get("bucket") kwargs = {"Bucket": bucket_name, "ExpectedBucketOwner": args.get("expected_bucket_owner")} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Gets bucket policy for an Amazon S3 bucket: {bucket_name}") response = client.get_bucket_policy(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: json_response = json.loads(response.get("Policy", "{}")) json_statement = json_response.get("Statement", []) return CommandResults( outputs_prefix="AWS.S3.Buckets", outputs_key_field="BucketName", outputs={ "BucketName": bucket_name, "Policy": json_response, }, readable_output=tableToMarkdown( f"Bucket Policy ID: {json_response.get('Id','N/A')} Version: {json_response.get('Version','N/A')}", t=json_statement, removeNull=True, headerTransform=pascalToSpace, ), ) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def delete_bucket_website_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Deletes the static website configuration from the specified S3 bucket. Executes the DeleteBucketWebsite API operation. If successful, the website configuration is removed, but the bucket itself remains intact. Args: client (BotoClient): The initialized Boto3 S3 client. args (Dict[str, Any]): Command arguments, typically containing: - 'bucket' (str): The name of the S3 bucket. (Required) Returns: CommandResults: A CommandResults object with a success message on status 200/204. """ kwargs = {"Bucket": args.get("bucket")} remove_nulls_from_dictionary(kwargs) try: response = client.delete_bucket_website(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return CommandResults( readable_output=f"Successfully removed the static website configuration from {args.get('bucket')} bucket." ) raise DemistoException(f"Failed to delete bucket website for {args.get('bucket')}.") except Exception as e: raise DemistoException(f"Error: {str(e)}") @staticmethod def put_bucket_ownership_controls_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Specifies the rule that determines ownership of newly uploaded objects and manages the use of Access Control Lists (ACLs). Requires a validated JSON structure for 'the ownership_controls' argument. the role of Access Control Lists (ACLs). This operation requires a specific, validated JSON structure for the 'ownership_controls' argument. Args: client (BotoClient): The initialized Boto3 S3 client. args (Dict[str, Any]): Command arguments, typically containing: - 'bucket' (str): The name of the S3 bucket. (Required) - 'ownership_controls_rule' (str): A predefined rule specifying the desired ownership behavior. Must be one of the following: BucketOwnerPreferred, ObjectWriter, BucketOwnerEnforced Returns: CommandResults: A CommandResults object with a success message on status 200/204. """ ownership_controls = {"Rules": [{"ObjectOwnership": args.get("ownership_controls_rule")}]} kwargs = {"Bucket": args.get("bucket"), "OwnershipControls": ownership_controls} remove_nulls_from_dictionary(kwargs) try: demisto.debug(f"calling put_bucket_ownership_controls with {kwargs=}") response = client.put_bucket_ownership_controls(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return CommandResults(readable_output=f"Bucket Ownership Controls successfully updated for {args.get('bucket')}") raise DemistoException(f"Failed to set Bucket Ownership Controls for {args.get('bucket')}.") except Exception as e: raise DemistoException(f"Error: {str(e)}") @staticmethod def file_download_command(client: BotoClient, args: Dict[str, Any]): """ Download a file from an S3 bucket. Args: client (BotoClient): The initialized Boto3 S3 client. args (Dict[str, Any]): Command arguments, typically containing: - 'bucket' (str): The name of the S3 bucket. (Required) - 'key' (str): The key of the file to download. (Required) Returns: fileResult: fileResult object """ bucket = args.get("bucket") key = args.get("key", "") print_debug_logs(client, f"downloading bucket={bucket}, key={key}") try: resp = client.get_object(Bucket=bucket, Key=key) body = resp["Body"] try: data = body.read() finally: try: body.close() except Exception: pass filename = key.rsplit("/", 1)[-1] return fileResult(filename, data) except ClientError as err: return AWSErrorHandler.handle_client_error(err) except Exception as e: raise DemistoException(f"Error: {str(e)}") @staticmethod def file_upload_command(client: BotoClient, args: Dict[str, Any]): """ Upload a file to an S3 bucket. Args: client (BotoClient): The initialized Boto3 S3 client. args (Dict[str, Any]): Command arguments, typically containing: - 'bucket' (str): The name of the S3 bucket. (Required) - 'key' (str): The key of the file to upload. (Required) - 'entryID' (str): The ID of the file to upload. (Required) Returns: CommandResults: A CommandResults object with a success/fail message. """ bucket = args.get("bucket") key = args.get("key") entry_id = args.get("entryID") path = get_file_path(entry_id) print_debug_logs(client, f"uploading entryID={entry_id} to bucket={bucket}, key={key}") try: with open(path["path"], "rb") as data: client.upload_fileobj(data, bucket, key) return CommandResults(readable_output=f"File {key} was uploaded successfully to {bucket}") except ClientError as err: AWSErrorHandler.handle_client_error(err) except Exception as e: raise DemistoException(f"Error: {str(e)}") return CommandResults(readable_output="Failed to upload file") @staticmethod def get_bucket_website_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the website configuration for a specified Amazon S3 bucket. The function calls the AWS S3 'get_bucket_website' API to check if the bucket is configured for static website hosting and, if so, what its configuration is. Args: client (BotoClient): The initialized Boto3 S3 client. args: A dictionary containing arguments, expected to include 'bucket' (the name of the S3 bucket). Returns: CommandResults: An object containing the raw website configuration as outputs, and a md table summarizing the configuration details (IndexDocument, ErrorDocument, RedirectAllRequestsTo, RoutingRules). """ kwargs = {"Bucket": args.get("bucket")} response = client.get_bucket_website(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response["WebsiteConfiguration"] = { "ErrorDocument": response.get("ErrorDocument"), "IndexDocument": response.get("IndexDocument"), "RedirectAllRequestsTo": response.get("RedirectAllRequestsTo"), "RoutingRules": response.get("RoutingRules"), } readable_output = tableToMarkdown( name="Bucket Website Configuration", t=response.get("WebsiteConfiguration", {}), removeNull=True, headers=["ErrorDocument", "IndexDocument", "RedirectAllRequestsTo", "RoutingRules"], headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.S3.Buckets.BucketWebsite", outputs=response.get("WebsiteConfiguration", {}), raw_response=response.get("WebsiteConfiguration", {}), ) @staticmethod def get_bucket_acl_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the Access Control List (ACL) of a specified Amazon S3 bucket. The function calls the AWS S3 'get_bucket_acl' API to determine the permissions granted to specific users or groups on the bucket. Args: client (BotoClient): The initialized Boto3 S3 client. args: A dictionary containing arguments, expected to include 'bucket' (the name of the S3 bucket). Returns: CommandResults: An object containing the raw Access Control Policy details as outputs, and a md table summarizing the ACL configuration (Grants and Owner). """ kwargs = {"Bucket": args.get("bucket")} response = client.get_bucket_acl(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response["AccessControlPolicy"] = { "Grants": response.get("Grants"), "Owner": response.get("Owner"), } readable_output = tableToMarkdown( name="Bucket Acl", t=response.get("AccessControlPolicy", {}), removeNull=True, headers=["Grants", "Owner"], headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.S3.Buckets.BucketAcl", outputs=response.get("AccessControlPolicy", {}), raw_response=response.get("AccessControlPolicy", {}), ) @staticmethod def bucket_create_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a new S3 bucket. Args: client (BotoClient): The initialized Boto3 S3 client. args (dict): A dictionary containing the arguments entered to the command. Returns: CommandResults: A success message and information on the newly created bucket. """ bucket_name = args.get("bucket_name") location = args.get("location_constraint") or args.get("region", "") kwargs = { "ACL": args.get("acl"), "Bucket": bucket_name, "GrantFullControl": args.get("grant_full_control"), "GrantRead": args.get("grant_read"), "GrantReadACP": args.get("grant_read_acp"), "GrantWrite": args.get("grant_write"), "GrantWriteACP": args.get("grant_write_acp"), } # The "us-east-1" is the default value for LocationConstraint, when added to the request the S3 API views that # specific string as an invalid/unsupported value for the constraint. if location != "us-east-1": kwargs["CreateBucketConfiguration"] = {"LocationConstraint": location} remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.create_bucket(**kwargs) demisto.debug(f"{response=}") if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) output = {"Location": response.get("Location"), "BucketArn": response.get("BucketArn"), "BucketName": bucket_name} return CommandResults( readable_output=f"The bucket {bucket_name}, was created successfully", outputs=output, outputs_prefix="AWS.S3.Buckets", outputs_key_field="BucketName", raw_response=response, ) @staticmethod def buckets_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns a list of all buckets owned by the authenticated sender of the request. Args: client (BotoClient): The initialized Boto3 S3 client. args (dict): A dictionary containing the following arguments: account_id, region, limit, next_page_token, prefix. Returns: CommandResults: Containing the list of buckets. """ kwargs = {"Prefix": args.get("prefix"), "BucketRegion": args.get("filter_by_region")} kwargs.update( build_pagination_kwargs(args, max_limit=10000, next_token_name="ContinuationToken", limit_name="MaxBuckets") ) remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.list_buckets(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) buckets = response.get("Buckets", []) for bucket in buckets: bucket["CreationDate"] = datetime.strftime(bucket["CreationDate"], "%Y-%m-%dT%H:%M:%S") bucket["BucketName"] = bucket.get("Name", "") del bucket["Name"] readable_output = tableToMarkdown("The list of buckets", buckets, removeNull=True, headerTransform=pascalToSpace) outputs = { "AWS.S3.Buckets(val.BucketArn && val.BucketArn == obj.BucketArn)": buckets, "AWS.S3(true)": { "BucketsOwner": response.get("Owner"), "BucketsNextPageToken": response.get("ContinuationToken"), "BucketsPrefix": response.get("Prefix"), }, } return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=response) class IAM: service = AWSServices.IAM @staticmethod def get_account_password_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Get AWS account password policy. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including account ID Returns: CommandResults: Results containing the current password policy configuration """ response = client.get_account_password_policy() data = json.loads(json.dumps(response["PasswordPolicy"], cls=DatetimeEncoder)) human_readable = tableToMarkdown("AWS IAM Account Password Policy", data) return CommandResults( outputs=data, readable_output=human_readable, outputs_prefix="AWS.IAM.PasswordPolicy", outputs_key_field="AccountId" ) @staticmethod def update_account_password_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Create or Update AWS account password policy. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including password policy parameters Returns: CommandResults: Results of the operation with success/failure message """ try: response = client.get_account_password_policy() kwargs = response["PasswordPolicy"] except Exception: raise DemistoException(f"Couldn't check current account password policy for account: {args.get('account_id')}") # ExpirePasswords is part of the response but cannot be included in the request if "ExpirePasswords" in kwargs: kwargs.pop("ExpirePasswords") command_args: dict[str, Union[int, bool, None]] = { "MinimumPasswordLength": arg_to_number(args.get("minimum_password_length")), "RequireSymbols": argToBoolean(args.get("require_symbols")) if args.get("require_symbols") else None, "RequireNumbers": argToBoolean(args.get("require_numbers")) if args.get("require_numbers") else None, "RequireUppercaseCharacters": ( argToBoolean(args.get("require_uppercase_characters")) if args.get("require_uppercase_characters") else None ), "RequireLowercaseCharacters": ( argToBoolean(args.get("require_lowercase_characters")) if args.get("require_lowercase_characters") else None ), "AllowUsersToChangePassword": ( argToBoolean(args.get("allow_users_to_change_password")) if args.get("allow_users_to_change_password") else None ), "MaxPasswordAge": arg_to_number(args.get("max_password_age")), "PasswordReusePrevention": arg_to_number(args.get("password_reuse_prevention")), "HardExpiry": argToBoolean(args.get("hard_expiry")) if args.get("hard_expiry") else None, } remove_nulls_from_dictionary(command_args) for arg_key, arg_value in command_args.items(): kwargs[arg_key] = arg_value response = client.update_account_password_policy(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully updated account password policy for account: {args.get('account_id')}" ) else: raise DemistoException( f"Couldn't updated account password policy for account: {args.get('account_id')}. {json.dumps(response)}" ) @staticmethod def put_role_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds or updates an inline policy document that is embedded in the specified IAM role. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including policy_document, policy_name, and role_name Returns: CommandResults: Results of the operation with success/failure message """ policy_document: str = args.get("policy_document", "") policy_name: str = args.get("policy_name", "") role_name: str = args.get("role_name", "") kwargs = {"PolicyDocument": policy_document, "PolicyName": policy_name, "RoleName": role_name} try: client.put_role_policy(**kwargs) human_readable = f"Policy '{policy_name}' was successfully added to role '{role_name}'" return CommandResults(readable_output=human_readable) except Exception as e: raise DemistoException(f"Failed to add policy '{policy_name}' to role '{role_name}'. Error: {str(e)}") @staticmethod def delete_login_profile_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Deletes the password for the specified IAM user, which terminates the user's ability to access AWS services through the AWS Management Console. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including: - user_name (str): The name of the user whose password you want to delete Returns: CommandResults: Results of the operation with success/failure message """ user_name = args.get("user_name", "") try: demisto.debug(f"calling delete_login_profile with {user_name=}") response = client.delete_login_profile(UserName=user_name) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults(readable_output=f"Successfully deleted login profile for user '{user_name}'") else: raise DemistoException( f"Failed to delete login profile for user '{user_name}'. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error deleting login profile for user '{user_name}': {str(e)}") @staticmethod def put_user_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds or updates an inline policy document that is embedded in the specified IAM user. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including: - user_name (str): The name of the user to associate the policy with - policy_name (str): The name of the policy document - policy_document (str): The policy document in JSON format Returns: CommandResults: Results of the operation with success/failure message """ user_name = args.get("user_name", "") policy_name = args.get("policy_name", "") policy_document = args.get("policy_document", "") try: demisto.debug(f"calling put_user_policy with {user_name=}, {policy_name=}, {policy_document=}") response = client.put_user_policy( UserName=user_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy_document) if isinstance(policy_document, dict) else policy_document, ) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults(readable_output=f"Successfully added/updated policy '{policy_name}' for user '{user_name}'") else: raise DemistoException( f"Failed to add/update policy '{policy_name}' for user '{user_name}'. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error adding/updating policy '{policy_name}' for user '{user_name}': {str(e)}") @staticmethod def remove_role_from_instance_profile_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Removes the specified IAM role from the specified EC2 instance profile. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including: - instance_profile_name (str): The name of the instance profile to update - role_name (str): The name of the role to remove Returns: CommandResults: Results of the operation with success/failure message """ instance_profile_name = args.get("instance_profile_name", "") role_name = args.get("role_name", "") try: demisto.debug(f"calling remove_role_from_instance_profile with {instance_profile_name=}, {role_name=}") response = client.remove_role_from_instance_profile(InstanceProfileName=instance_profile_name, RoleName=role_name) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully removed role '{role_name}' from instance profile '{instance_profile_name}'" ) else: raise DemistoException( f"Failed to remove role '{role_name}' from instance profile '{instance_profile_name}'. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error removing role '{role_name}' from instance profile '{instance_profile_name}': {str(e)}") @staticmethod def update_access_key_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Changes the status of the specified access key from Active to Inactive, or vice versa. This operation can be used to disable a user's access key as part of a key rotation workflow. Args: client (BotoClient): The boto3 client for IAM service args (Dict[str, Any]): Command arguments including: - access_key_id (str): The access key ID of the secret access key you want to update - status (str): The status you want to assign to the secret access key (Active/Inactive) - user_name (str, optional): The name of the user whose key you want to update Returns: CommandResults: Results of the operation with success/failure message """ access_key_id = args.get("access_key_id", "") status = args.get("status", "") user_name = args.get("user_name") kwargs = {"AccessKeyId": access_key_id, "Status": status} if user_name: kwargs["UserName"] = user_name try: demisto.debug(f"calling update_access_key with {kwargs=}") response = client.update_access_key(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: user_info = f" for user '{user_name}'" if user_name else "" return CommandResults( readable_output=f"Successfully updated access key '{access_key_id}' status to '{status}'{user_info}" ) else: raise DemistoException( f"Failed to update access key '{access_key_id}' status. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error updating access key '{access_key_id}' status: {str(e)}") class EC2: service = AWSServices.EC2 @staticmethod def modify_instance_metadata_options_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modify the EC2 instance metadata parameters on a running or stopped instance. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including instance ID and metadata options Returns: CommandResults: Results of the operation with success/failure message """ kwargs = { "InstanceId": args.get("instance_id"), "HttpTokens": args.get("http_tokens"), "HttpEndpoint": args.get("http_endpoint"), } remove_nulls_from_dictionary(kwargs) response = client.modify_instance_metadata_options(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults(readable_output=f"Successfully updated EC2 instance metadata for {args.get('instance_id')}") else: raise DemistoException(f"Couldn't updated public EC2 instance metadata for {args.get('instance_id')}") @staticmethod def modify_instance_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modify an EC2 instance attribute. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including instance attribute modifications Returns CommandResults: Results of the operation with success/failure message """ def parse_security_groups(csv_list): if csv_list is None: return None security_groups_str = csv_list.replace(" ", "") security_groups_list = security_groups_str.split(",") return security_groups_list kwargs = { "InstanceId": args.get("instance_id"), "Attribute": args.get("attribute"), "Value": args.get("value"), "DisableApiStop": arg_to_bool_or_none(args.get("disable_api_stop")), "Groups": parse_security_groups(args.get("groups")), } remove_nulls_from_dictionary(kwargs) response = client.modify_instance_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully modified EC2 instance `{args.get('instance_id')}` attribute `{kwargs.popitem()}" ) raise DemistoException(f"Unexpected response from AWS - \n{response}") @staticmethod def modify_snapshot_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds or removes permission settings for the specified snapshot. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - snapshot_id (str): The ID of the snapshot - attribute (str): The snapshot attribute to modify - operation_type (str): The operation to perform (add or remove) - user_ids (str, optional): Comma-separated list of AWS account IDs - group (str, optional): The group to add/remove (e.g., 'all') Returns: CommandResults: Results of the operation with success message """ # Parse user IDs from comma-separated string user_ids_list = None if user_ids := args.get("user_ids"): user_ids_list = argToList(user_ids) # Parse group parameter group_names_list = None if group := args.get("group"): group_names_list = [group.strip()] # Build accounts parameter using assign_params to handle None values accounts = assign_params(GroupNames=group_names_list, UserIds=user_ids_list) response = client.modify_snapshot_attribute( Attribute=args.get("attribute"), SnapshotId=args.get("snapshot_id"), OperationType=args.get("operation_type"), **accounts, ) if response["ResponseMetadata"]["HTTPStatusCode"] != HTTPStatus.OK: raise DemistoException(f"Unexpected response from AWS - EC2:\n{response}") return CommandResults(readable_output=f"Snapshot {args.get('snapshot_id')} permissions was successfully updated.") @staticmethod def modify_image_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modify the specified attribute of an Amazon Machine Image (AMI). """ kwargs = { "Attribute": args.get("attribute"), "ImageId": args.get("image_id"), "OperationType": args.get("operation_type"), } if desc := args.get("description"): kwargs["Description"] = {"Value": desc} # Map snake_case arg names → CapitalCase boto3 params resource_mapping = { "user_ids": "UserIds", "user_groups": "UserGroups", "product_codes": "ProductCodes", } for snake, capital in resource_mapping.items(): if ids := args.get(snake): kwargs[capital] = parse_resource_ids(ids) # Build LaunchPermission block from snake_case args launch_perm: dict[str, list[dict[str, str]]] = {"Add": [], "Remove": []} perm_config = [ ("launch_permission_add_group", "Group", "Add"), ("launch_permission_add_user_id", "UserId", "Add"), ("launch_permission_remove_group", "Group", "Remove"), ("launch_permission_remove_user_id", "UserId", "Remove"), ] for arg_key, perm_key, action in perm_config: if val := args.get(arg_key): launch_perm[action].append({perm_key: val}) if launch_perm["Add"] or launch_perm["Remove"]: kwargs["LaunchPermission"] = launch_perm remove_nulls_from_dictionary(kwargs) response = client.modify_image_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] != HTTPStatus.OK: raise DemistoException(f"Unexpected response from AWS - EC2:\n{response}") return CommandResults(readable_output="Image attribute successfully modified") @staticmethod def revoke_security_group_ingress_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Revokes an ingress rule from a security group. The command supports two modes: 1. Simple mode: using protocol, port, and cidr arguments 2. Full mode: using ip_permissions for complex configurations """ kwargs = {"GroupId": args.get("group_id"), "IpProtocol": args.get("protocol"), "CidrIp": args.get("cidr")} kwargs["FromPort"], kwargs["ToPort"] = handle_port_range(args) if ip_permissions := args.get("ip_permissions"): try: kwargs["IpPermissions"] = json.loads(ip_permissions) except json.JSONDecodeError as e: raise DemistoException(f"Received invalid `ip_permissions` JSON object: {e}") remove_nulls_from_dictionary(kwargs) try: response = client.revoke_security_group_ingress(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK and response["Return"]: if "UnknownIpPermissions" in response: raise DemistoException("Security Group ingress rule not found.") return CommandResults(readable_output="The Security Group ingress rule was revoked") else: raise DemistoException(f"Unexpected response from AWS - EC2:\n{response}") except Exception as e: if "InvalidGroup.NotFound" in str(e): raise DemistoException(f"Security group {kwargs['GroupId']} not found") elif "InvalidGroupId.NotFound" in str(e): raise DemistoException(f"Invalid security group ID: {kwargs['GroupId']}") else: raise DemistoException(f"Failed to revoke security group ingress rule: {str(e)}") @staticmethod def authorize_security_group_ingress_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds an inbound rule to a security group. The command supports two modes: 1. Simple mode: using protocol, port, and cidr arguments 2. Full mode: using ip_permissions for complex configurations """ kwargs = {"GroupId": args.get("group_id"), "IpProtocol": args.get("protocol"), "CidrIp": args.get("cidr")} kwargs["FromPort"], kwargs["ToPort"] = handle_port_range(args) if ip_permissions := args.get("ip_permissions"): try: kwargs["IpPermissions"] = json.loads(ip_permissions) except json.JSONDecodeError as e: raise DemistoException(f"Received invalid `ip_permissions` JSON object: {e}") remove_nulls_from_dictionary(kwargs) try: response = client.authorize_security_group_ingress(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK and response["Return"]: return CommandResults(readable_output="The Security Group ingress rule was authorized") else: raise DemistoException(f"Unexpected response from AWS - EC2:\n{response}") except Exception as e: if "InvalidGroup.NotFound" in str(e): raise DemistoException(f"Security group {kwargs['GroupId']} not found") elif "InvalidGroupId.NotFound" in str(e): raise DemistoException(f"Invalid security group ID: {kwargs['GroupId']}") elif "InvalidPermission.Duplicate" in str(e): raise DemistoException("The specified rule already exists in the security group") else: raise DemistoException(f"Failed to authorize security group ingress rule: {str(e)}") @staticmethod def revoke_security_group_egress_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Revokes an egress rule from a security group. Modes: 1) Full mode: use `ip_permissions` JSON 2) Simple mode: protocol, port, cidr → build IpPermissions """ group_id = args.get("group_id") ip_permissions_arg = args.get("ip_permissions") if ip_permissions_arg: # Full mode: user provided the entire IpPermissions JSON try: ip_perms = json.loads(ip_permissions_arg) except json.JSONDecodeError as e: raise DemistoException(f"Invalid `ip_permissions` JSON: {e}") else: # Simple mode: build a single rule descriptor proto = args.get("protocol") from_port, to_port = handle_port_range(args) cidr = args.get("cidr") ip_perms = [ {"IpProtocol": proto, "FromPort": from_port, "ToPort": to_port, "IpRanges": [{"CidrIp": cidr}] if cidr else None} ] ip_perms = [remove_empty_elements(ip_perms[0])] kwargs = {"GroupId": group_id, "IpPermissions": ip_perms} try: resp = client.revoke_security_group_egress(**kwargs) status = resp.get("Return") if resp.get("ResponseMetadata", {}).get("HTTPStatusCode") == 200 and status: readable_output = ( "Egress rule revoked successfully." if resp.get("RevokedSecurityGroupRules") else "No egress rules were revoked." ) return CommandResults(readable_output=readable_output, raw_response=resp) else: # If no exception but Return is False, AWS may report unknown perms unknown = resp.get("UnknownIpPermissions") if unknown: raise DemistoException("Specified egress rule not found.") raise DemistoException(f"Unexpected response: {resp}") except ClientError as e: code = e.response["Error"]["Code"] if code in ("InvalidGroup.NotFound", "InvalidGroupId.NotFound"): raise DemistoException(f"Security group {group_id} not found.") raise DemistoException(f"Failed to revoke egress rule: {e}") @staticmethod def create_snapshot_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Creates a snapshot of an Amazon EBS volume. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - volume_id (str): The ID of the volume to snapshot - description (str, optional): Description for the snapshot - tag_specifications (str, optional): Tag specifications for the snapshot Returns: CommandResults: Results of the snapshot creation operation """ kwargs = {"VolumeId": args.get("volume_id")} if args.get("description") is not None: kwargs.update({"Description": args.get("description")}) if args.get("tags") is not None: kwargs.update({"TagSpecifications": [{"ResourceType": "snapshot", "Tags": parse_tag_field(args.get("tags", ""))}]}) response = client.create_snapshot(**kwargs) try: start_time = datetime.strftime(response["StartTime"], "%Y-%m-%dT%H:%M:%SZ") except ValueError as e: raise DemistoException(f"Date could not be parsed. Please check the date again.\n{e}") data = { "Description": response["Description"], "Encrypted": response["Encrypted"], "Progress": response["Progress"], "SnapshotId": response["SnapshotId"], "State": response["State"], "VolumeId": response["VolumeId"], "VolumeSize": response["VolumeSize"], "StartTime": start_time, "Region": args.get("region"), } if "Tags" in response: for tag in response["Tags"]: data.update({tag["Key"]: tag["Value"]}) try: output = json.dumps(response, cls=DatetimeEncoder) raw = json.loads(output) raw.update({"Region": args.get("region")}) except ValueError as err_msg: raise DemistoException(f"Could not decode/encode the raw response - {err_msg}") return CommandResults( outputs=raw, outputs_prefix="AWS.EC2.Snapshot", readable_output=tableToMarkdown("AWS EC2 Snapshot", data), raw_response=raw, ) @staticmethod def modify_snapshot_permission_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """Modifies the permissions of a snapshot. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - snapshot_id (str): The ID of the snapshot to modify - group_names (str, optional): The names of the security groups to add or remove permissions for - user_ids (str, optional): The IDs of the AWS accounts to add or remove permissions for - operation_type (str): The type of operation to perform (add | remove) Raises: DemistoException: If both group_names and user_ids are provided or if neither is provided Returns: CommandResults: _description_ """ group_names = argToList(args.get("group_names")) user_ids = argToList(args.get("user_ids")) if (group_names and user_ids) or not (group_names or user_ids): raise DemistoException('Please provide either "group_names" or "user_ids"') accounts = assign_params(GroupNames=group_names, UserIds=user_ids) operation_type = args.get("operation_type") client.modify_snapshot_attribute( Attribute="createVolumePermission", SnapshotId=args.get("snapshot_id"), OperationType=operation_type, DryRun=argToBoolean(args.get("dry_run", False)), **accounts, ) return CommandResults(readable_output=f"Snapshot {args.get('snapshot_id')} permissions were successfully updated.") @staticmethod def describe_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves detailed information about EC2 instances including status, configuration, and metadata. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing account_id, region, instance_ids, filters, etc. Returns: CommandResults: Formatted results with instance information """ # Build API parameters kwargs = {} # Add instance IDs if provided if instance_ids := args.get("instance_ids"): kwargs["InstanceIds"] = argToList(instance_ids) # Add filters if provided if filters_arg := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters_arg) if not instance_ids: pagination_kwargs = build_pagination_kwargs(args) kwargs.update(pagination_kwargs) print_debug_logs(client, f"Describing instances with parameters: {kwargs}") remove_nulls_from_dictionary(kwargs) response = client.describe_instances(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) # Extract instances from reservations reservations = response.get("Reservations", []) if not reservations: return CommandResults( readable_output="No instances found matching the specified criteria.", ) readable_outputs = [] instances_list = [] for reservation in reservations: instances_list.extend(reservation.get("Instances", [])) for instance in reservation.get("Instances", []): readable_outputs.append(process_instance_data(instance)) outputs = { "AWS.EC2.Instances(val.InstanceId && val.InstanceId == obj.InstanceId)": instances_list, "AWS.EC2(true)": {"InstancesNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown("AWS EC2 Instances", readable_outputs, removeNull=True), raw_response=response, ) @staticmethod def run_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Runs one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing instance configuration parameters Returns: CommandResults: Results of the operation with instance launch information Raises: DemistoException: If required parameters are missing or API call fails """ # Validate required parameters count = arg_to_number(args.get("count", 1)) if not count or count <= 0: raise DemistoException("count parameter must be a positive integer") # Build base parameters kwargs: Dict[str, Any] = {"MinCount": count, "MaxCount": count} # Handle image specification - either direct AMI ID or launch template image_id = args.get("image_id") launch_template_id = args.get("launch_template_id") launch_template_name = args.get("launch_template_name") if launch_template_id or launch_template_name: # Using launch template launch_template = {} if launch_template_id: launch_template["LaunchTemplateId"] = launch_template_id elif launch_template_name: launch_template["LaunchTemplateName"] = launch_template_name if launch_template_version := args.get("launch_template_version"): launch_template["Version"] = launch_template_version kwargs["LaunchTemplate"] = launch_template # Image ID is optional when using launch template if image_id: kwargs["ImageId"] = image_id else: # Direct AMI specification kwargs["ImageId"] = image_id # Add optional basic parameters if instance_type := args.get("instance_type"): kwargs["InstanceType"] = instance_type if key_name := args.get("key_name"): kwargs["KeyName"] = key_name if subnet_id := args.get("subnet_id"): kwargs["SubnetId"] = subnet_id # Handle security groups if security_group_ids := args.get("security_group_ids"): kwargs["SecurityGroupIds"] = argToList(security_group_ids) if security_groups_names := args.get("security_groups_names"): kwargs["SecurityGroups"] = argToList(security_groups_names) # Handle user data with base64 encoding if user_data := args.get("user_data"): kwargs["UserData"] = user_data # Handle boolean parameters kwargs["DisableApiTermination"] = arg_to_bool_or_none(args.get("disable_api_termination")) kwargs["EbsOptimized"] = arg_to_bool_or_none(args.get("ebs_optimized")) # Handle IAM instance profile kwargs["IamInstanceProfile"] = { "Arn": args.get("iam_instance_profile_arn"), "Name": args.get("iam_instance_profile_name"), } ebs_config = remove_empty_elements( { "VolumeSize": arg_to_number(args.get("ebs_volume_size")), "SnapshotId": args.get("ebs_snapshot_id"), "VolumeType": args.get("ebs_volume_type"), "Iops": arg_to_number(args.get("ebs_iops")), "DeleteOnTermination": arg_to_bool_or_none(args.get("ebs_delete_on_termination")), "KmsKeyId": args.get("ebs_kms_key_id"), "Encrypted": arg_to_bool_or_none(args.get("ebs_encrypted")), } ) kwargs["BlockDeviceMappings"] = [{"DeviceName": args.get("device_name"), "Ebs": ebs_config}] kwargs["Monitoring"] = {"Enabled": arg_to_bool_or_none(args.get("enabled_monitoring"))} kwargs["Placement"] = {"HostId": args.get("host_id")} tags = args.get("tags") if tags: kwargs["TagSpecifications"] = [{"ResourceType": "instance", "Tags": parse_tag_field(tags)}] # Remove null values to clean up API call kwargs = remove_empty_elements(kwargs) response = client.run_instances(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) instances = response.get("Instances", []) if not instances: return CommandResults(readable_output="No instances were launched.") # Format output data instances_data = [] for instance in instances: instances_data.append(process_instance_data(instance)) readable_output = tableToMarkdown( f"Launched {len(instances)} EC2 Instance(s)", instances_data, headers=[ "InstanceId", "ImageId", "State", "Type", "PublicIPAddress", "PrivateIpAddress", "LaunchDate", "AvailabilityZone", "PublicDNSName", "Monitoring", ], headerTransform=string_to_table_header, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Instances", outputs=instances, readable_output=readable_output, raw_response=response ) @staticmethod def _manage_instances_command( client: BotoClient, args: Dict[str, Any], action: str, additional_params: Optional[Dict[str, str]] = None ) -> CommandResults | None: """ General function to manage EC2 instances (start, stop, terminate). Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing instance_ids and other parameters action (str): The action to perform ('start', 'stop', 'terminate') additional_params (Optional[Dict[str, str]]): Additional parameter names to extract from args Returns: CommandResults: Results of the operation with instance management information Raises: DemistoException: If instance_ids parameter is missing or invalid action provided """ # Validate action valid_actions = {"start", "stop", "terminate"} if action not in valid_actions: raise DemistoException(f"Invalid action '{action}'. Must be one of: {valid_actions}") # Validate and extract instance IDs instance_ids = argToList(args.get("instance_ids", [])) if not instance_ids: raise DemistoException("instance_ids parameter is required") # Build base parameters base_params = {"InstanceIds": instance_ids} # Add action-specific parameters if additional_params: for param_name, arg_key in additional_params.items(): if arg_key in args: base_params[param_name] = argToBoolean(args.get(arg_key, False)) base_params = remove_empty_elements(base_params) # Define action configuration action_config = { "start": { "method_name": "start_instances", "response_key": "StartingInstances", "success_message": "started", "failure_message": "Failed to start instances", }, "stop": { "method_name": "stop_instances", "response_key": "StoppingInstances", "success_message": "stopped", "failure_message": "Failed to stop instances", }, "terminate": { "method_name": "terminate_instances", "response_key": "TerminatingInstances", "success_message": "terminated", "failure_message": "Failed to terminate instances", }, } config = action_config[action] print_debug_logs(client, f"{action.title()}ing instances: {instance_ids}") # Get the appropriate client method dynamically client_method = getattr(client, config["method_name"]) response = client_method(**base_params) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: readable_output = ( f"The instances have been {config['success_message']} successfully." if response.get(config["response_key"]) else f"No instances were {config['success_message']}." ) return CommandResults(readable_output=readable_output, raw_response=response) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def stop_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Stops one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing instance_ids and other parameters Returns: CommandResults: Results of the operation with instance stop information """ additional_params = {"Force": "force", "Hibernate": "hibernate"} return EC2._manage_instances_command(client, args, "stop", additional_params) @staticmethod def terminate_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Terminates one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing instance_ids and other parameters Returns: CommandResults: Results of the operation with instance termination information """ return EC2._manage_instances_command(client, args, "terminate") @staticmethod def start_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Starts one or more stopped Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing instance_ids and other parameters Returns: CommandResults: Results of the operation with instance start information """ return EC2._manage_instances_command(client, args, "start") @staticmethod def modify_subnet_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modifies a single attribute on a specified Amazon EC2 subnet. This command performs the 'ModifySubnetAttribute' API operation. Args: client (BotoClient): The initialized Boto3 EC2 client. args (Dict[str, Any]): Command arguments, typically containing: - 'subnet_id' (str): The ID of the subnet to modify. (Required) - 'map_public_ip_on_launch' (str): Boolean value to control auto-assign public IPv4. - 'assign_ipv6_address_on_creation' (str): Boolean value to control auto-assign IPv6 address. - 'enable_dns64' (str): Boolean value to enable DNS64 resolution. - 'enable_resource_name_dns_a_record_on_launch' (str): Boolean value to enable DNS A records based on instance resource name. - 'enable_resource_name_dns_aaaa_record_on_launch' (str): Boolean value to enable DNS AAAA records based on instance resource name. - 'private_dns_hostname_type_on_launch' (str): String value for private DNS hostname generation. - 'customer_owned_ipv4_pool' (str): The ID of the Customer Owned IPv4 Pool (CoIP) to associate with the subnet. - 'map_customer_owned_ip_on_launch' (str): Boolean value to auto-assign CoIPs to instances. - 'enable_lni_at_device_index' (str): Integer (1-15) to set the device index for LNI assignment. - 'disable_lni_at_device_index' (str): Boolean value to disable LNI assignment at a device index. Returns: CommandResults: A CommandResults object with a success message. """ kwargs = { "SubnetId": args.get("subnet_id"), "AssignIpv6AddressOnCreation": arg_to_bool_or_none(args.get("assign_ipv6_address_on_creation")), "CustomerOwnedIpv4Pool": args.get("customer_owned_ipv4_pool"), "DisableLniAtDeviceIndex": arg_to_bool_or_none(args.get("disable_lni_at_device_index")), "EnableDns64": arg_to_bool_or_none(args.get("enable_dns64")), "EnableLniAtDeviceIndex": arg_to_number(args.get("enable_lni_at_device_index")), "EnableResourceNameDnsAAAARecordOnLaunch": arg_to_bool_or_none( args.get("enable_resource_name_dns_aaaa_record_on_launch") ), "EnableResourceNameDnsARecordOnLaunch": arg_to_bool_or_none(args.get("enable_resource_name_dns_a_record_on_launch")), "MapCustomerOwnedIpOnLaunch": arg_to_bool_or_none(args.get("map_customer_owned_ip_on_launch")), "MapPublicIpOnLaunch": arg_to_bool_or_none(args.get("map_public_ip_on_launch")), "PrivateDnsHostnameTypeOnLaunch": args.get("private_dns_hostname_type_on_launch"), } remove_nulls_from_dictionary(kwargs) try: response = client.modify_subnet_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: demisto.debug(f"RequestId={response.get('ResponseMetadata').get('RequestId')}") return CommandResults(readable_output="Subnet configuration successfully updated.") raise DemistoException("Modification could not be performed.") except Exception as e: raise DemistoException(f"Error: {str(e)}") @staticmethod def describe_vpcs_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Describes one or more of your VPCs. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including VPC IDs Returns: CommandResults: Results of the operation with VPC information """ kwargs = {} data = [] if args.get("filters"): kwargs.update({"Filters": parse_filter_field(args.get("filters"))}) if args.get("vpc_ids"): kwargs.update({"VpcIds": parse_resource_ids(args.get("vpc_ids"))}) response = client.describe_vpcs(**kwargs) if len(response["Vpcs"]) == 0: return CommandResults(readable_output="No VPCs were found.") for i, vpc in enumerate(response["Vpcs"]): data.append( { "CidrBlock": vpc["CidrBlock"], "DhcpOptionsId": vpc["DhcpOptionsId"], "State": vpc["State"], "VpcId": vpc["VpcId"], "InstanceTenancy": vpc["InstanceTenancy"], "IsDefault": vpc["IsDefault"], "Region": args["region"], } ) if "Tags" in vpc: for tag in vpc["Tags"]: data[i].update({tag["Key"]: tag["Value"]}) try: output = json.dumps(response["Vpcs"], cls=DatetimeEncoder) raw = json.loads(output) raw[0].update({"Region": args["region"]}) except ValueError as e: raise DemistoException(f"Could not decode/encode the raw response - {e}") return CommandResults( outputs=raw, outputs_prefix="AWS.EC2.Vpcs", outputs_key_field="VpcId", readable_output=tableToMarkdown( "AWS EC2 Vpcs", data, headers=["VpcId", "IsDefault", "CidrBlock", "DhcpOptionsId", "State", "InstanceTenancy", "Region"], removeNull=True, ), raw_response=response, ) @staticmethod def describe_subnets_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Describes one or more of your subnets. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including subnet IDs Returns: CommandResults: Results of the operation with subnet information """ kwargs = {} data = [] if args.get("filters"): kwargs.update({"Filters": parse_filter_field(args.get("filters"))}) if args.get("subnet_ids"): kwargs.update({"SubnetIds": parse_resource_ids(args.get("subnet_ids"))}) response = client.describe_subnets(**kwargs) if len(response["Subnets"]) == 0: return CommandResults(readable_output="No subnets were found.") for i, subnet in enumerate(response["Subnets"]): data.append( { "AvailabilityZone": subnet["AvailabilityZone"], "AvailableIpAddressCount": subnet["AvailableIpAddressCount"], "CidrBlock": subnet.get("CidrBlock", ""), "DefaultForAz": subnet["DefaultForAz"], "State": subnet["State"], "SubnetId": subnet["SubnetId"], "VpcId": subnet["VpcId"], "Region": args["region"], } ) if "Tags" in subnet: for tag in subnet["Tags"]: data[i].update({tag["Key"]: tag["Value"]}) try: output = json.dumps(response["Subnets"], cls=DatetimeEncoder) raw = json.loads(output) raw[0].update({"Region": args["region"]}) except ValueError as e: raise DemistoException(f"Could not decode/encode the raw response - {e}") return CommandResults( outputs=raw, outputs_prefix="AWS.EC2.Subnets", outputs_key_field="SubnetId", readable_output=tableToMarkdown( "AWS EC2 Subnets", data, headers=[ "SubnetId", "AvailabilityZone", "AvailableIpAddressCount", # noqa: E501 "CidrBlock", "DefaultForAz", "State", "VpcId", "Region", ], removeNull=True, ), raw_response=response, ) @staticmethod def describe_ipam_resource_discoveries_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """Describes one or more IPAM resource discoveries. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including filters, max results, next token, and IPAM resource discovery IDs Returns: CommandResults: Results of the operation with IPAM resource discovery information """ kwargs = { "Filters": parse_filter_field(args.get("filters")), "IpamResourceDiscoveryIds": argToList(args.get("ipam_resource_discovery_ids")), } if not args.get("ipam_resource_discovery_ids"): pagination_kwargs = build_pagination_kwargs(args, minimum_limit=5) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) response = client.describe_ipam_resource_discoveries(**kwargs) if len(response["IpamResourceDiscoveries"]) == 0: return CommandResults(readable_output="No Ipam Resource Discoveries were found.") human_readable = tableToMarkdown("Ipam Resource Discoveries", response["IpamResourceDiscoveries"], removeNull=True) command_results = CommandResults( outputs_prefix="AWS.EC2.IpamResourceDiscoveries", outputs_key_field="IpamResourceDiscoveryId", outputs=response["IpamResourceDiscoveries"], raw_response=response, readable_output=human_readable, ) return command_results @staticmethod def describe_ipam_resource_discovery_associations_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """Describes one or more IPAM resource discovery associations. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including filters, max results, next token, and IPAM resource discovery IDs Returns: CommandResults: Results of the operation with IPAM resource discovery association information """ kwargs = { "Filters": parse_filter_field(args.get("filters")), "IpamResourceDiscoveryAssociationIds": argToList(args.get("ipam_resource_discovery_association_ids")), } if not args.get("ipam_resource_discovery_association_ids"): pagination_kwargs = build_pagination_kwargs(args, minimum_limit=5) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) response = client.describe_ipam_resource_discovery_associations(**kwargs) if len(response["IpamResourceDiscoveryAssociations"]) == 0: return CommandResults(readable_output="No Ipam Resource Discovery Associations were found.") human_readable = tableToMarkdown( "Ipam Resource Discovery Associations", response["IpamResourceDiscoveryAssociations"], removeNull=True ) # noqa: E501 command_results = CommandResults( outputs_prefix="AWS.EC2.IpamResourceDiscoveryAssociations", outputs_key_field="IpamResourceDiscoveryId", outputs=response["IpamResourceDiscoveryAssociations"], raw_response=response, readable_output=human_readable, ) return command_results @staticmethod def get_latest_ami_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves information about the latest Amazon Machine Image (AMI) based on provided filters. The function calls the AWS EC2 'describe_images' API, sorts the results by 'CreationDate' in descending order, and returns the first image in the list. Args: client (BotoClient): The initialized Boto3 EC2 client. args (Dict[str, Any]): Command arguments, typically containing: Expected keys include 'executable_by', 'filters', 'owners', 'image_id', 'include_deprecated', and 'include_disabled'. Returns: CommandResults: An object containing the raw image details as outputs, and a md table with a concise summary of the latest AMI. """ kwargs = { "ExecutableUsers": parse_resource_ids(args.get("executable_users")) if args.get("executable_users") else None, "Filters": parse_filter_field(args.get("filters")), "Owners": parse_resource_ids(args.get("owners")) if args.get("owners") else None, "ImageIds": parse_resource_ids(args.get("image_ids")) if args.get("image_ids") else None, "IncludeDeprecated": arg_to_bool_or_none(args.get("include_deprecated")), "IncludeDisabled": arg_to_bool_or_none(args.get("include_disabled")), } remove_nulls_from_dictionary(kwargs) response = client.describe_images(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) amis = response.get("Images", []) iterates = 1 while response.get("nextToken"): demisto.info(f"iterate #{iterates}") kwargs["NextToken"] = response.get("nextToken") response = client.describe_images(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) amis.extend(response.get("Images", [])) iterates += 1 # return CommandResults(readable_output=f"Fetched {len(amis)} AMIs") demisto.info(f"Fetched {len(amis)} AMIs") if not amis: return CommandResults(readable_output="No AMIs found.") sorted_amis = sorted(amis, key=lambda x: x["CreationDate"], reverse=True) image = sorted_amis[0] data = { "CreationDate": image.get("CreationDate"), "ImageId": image.get("ImageId"), "Public": image.get("Public"), "Name": image.get("Name"), "State": image.get("State"), "Region": args.get("region"), "Description": image.get("Description"), } data.update({tag["Key"]: tag["Value"] for tag in image["Tags"]}) if "Tags" in image else None remove_nulls_from_dictionary(data) return CommandResults( outputs=image, outputs_prefix="AWS.EC2.Images", readable_output=tableToMarkdown("AWS EC2 latest Image", data, headerTransform=pascalToSpace, removeNull=True), outputs_key_field="ImageId", ) @staticmethod def create_network_acl_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a Network Access Control List (Network ACL) for the specified VPC. The function calls the AWS EC2 'create_network_acl' API. It requires the ID of the VPC where the Network ACL will be created. Args: client (BotoClient): The initialized Boto3 EC2 client. args: A dictionary containing arguments for creating the Network ACL. Expected keys include 'vpc_id' (required), 'client_token', and 'tag_specification'. Returns: CommandResults: An object containing the raw Network ACL details as outputs, and a md table with a summary of the created Network ACL and its default entries. """ kwargs = { "VpcId": args.get("vpc_id"), "ClientToken": args.get("client_token"), "TagSpecifications": parse_tag_field(args.get("tag_specifications")), } remove_nulls_from_dictionary(kwargs) if tag_specifications := kwargs.get("TagSpecifications"): kwargs["TagSpecifications"] = [{"ResourceType": "network-acl", "Tags": tag_specifications}] response = client.create_network_acl(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) network_acl = response.get("NetworkAcl") readable_data = { "Associations": network_acl.get("Associations"), "IsDefault": network_acl.get("IsDefault"), "NetworkAclId": network_acl.get("NetworkAclId"), "Tags": network_acl.get("Tags"), "VpcId": network_acl.get("VpcId"), } return CommandResults( outputs=network_acl, outputs_prefix="AWS.EC2.VpcId.NetworkAcl", outputs_key_field="VpcId", readable_output=( tableToMarkdown( "The AWS EC2 Instance ACL", readable_data, removeNull=True, headerTransform=pascalToSpace, ) + tableToMarkdown( f"The Entries of AWS EC2 ACL {network_acl.get('NetworkAclId')}", [entry for entry in network_acl.get("Entries")], # noqa: C416 removeNull=True, headerTransform=pascalToSpace, ) ), ) @staticmethod def get_ipam_discovered_public_addresses_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ aws-ec2-get-ipam-discovered-public-addresses: Gets the public IP addresses that have been discovered by IPAM. Args: client (BotoClient): The initialized Boto3 EC2 client. args (dict): all command arguments, usually passed from ``demisto.args()``. Returns: CommandResults: A ``CommandResults`` object that is then passed to ``return_results``, that contains public IP addresses that have been discovered by IPAM. """ kwargs = { "IpamResourceDiscoveryId": args.get("ipam_resource_discovery_id"), "AddressRegion": args.get("address_region"), "Filters": parse_filter_field(args.get("filters")), "MaxResults": arg_to_number(args.get("limit", 1000)), "NextToken": args.get("next_token"), } remove_nulls_from_dictionary(kwargs) response = client.get_ipam_discovered_public_addresses(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) if not response.get("IpamDiscoveredPublicAddresses"): return CommandResults(readable_output="No Ipam Discovered Public Addresses were found.") output = json.loads(json.dumps(response, cls=DatetimeEncoder)) human_readable = tableToMarkdown( "Ipam Discovered Public Addresses", output.get("IpamDiscoveredPublicAddresses"), headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.IpamDiscoveredPublicAddresses", outputs_key_field="Address", outputs=output.get("IpamDiscoveredPublicAddresses"), raw_response=output, readable_output=human_readable, ) @staticmethod def create_tags_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Adds or overwrites one or more tags for the specified AWS resources. The function calls the AWS EC2 'create_tags' API. It requires a list of resource IDs to tag and a list of key-value pairs representing the tags to apply. Args: client (BotoClient): The initialized Boto3 EC2 client. args: A dictionary containing arguments for creating the tags. Expected keys are 'resources' (list of resource IDs) and 'tags' (list of key-value tag dictionaries). Returns: CommandResults: An object confirming the successful tagging operation via a readable output message. The command has no explicit outputs. """ kwargs = { "Resources": parse_resource_ids(args.get("resources")), "Tags": parse_tag_field(args.get("tags")), } response = client.create_tags(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output="The resources where tagged successfully") @staticmethod def network_interface_attribute_modify_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the specified network interface attribute. Args: client (BotoClient): The initialized Boto3 EC2 client. args (dict): A dictionary of the command arguments. Returns: CommandResults: A success message in case the modification was successful. """ network_interface_id = args.get("network_interface_id", "") kwargs = build_kwargs_network_interface_attribute(args, network_interface_id) demisto.debug(f"{kwargs=}") response = client.modify_network_interface_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) demisto.debug(f"{response=}") outputs = { "Attribute": { "ModifyResponseMetadata": {"HTTPStatusCode": response.get("ResponseMetadata", {}).get("HTTPStatusCode", "")}, }, "NetworkInterfaceId": network_interface_id, } return CommandResults( readable_output=f"The Network Interface attribute {network_interface_id} was modified successfully.", raw_response=response, outputs=outputs, outputs_prefix="AWS.EC2.NetworkInterfaces", outputs_key_field="NetworkInterfaceId", ) @staticmethod def regions_describe_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the Regions that are enabled for your account, or all Regions. Args: client (BotoClient): The initialized Boto3 EC2 client. args (dict): A dictionary containing arguments for creating the tags. account_id (str): The account id region_names (str): The names of the regions to retrieve all_regions (bool): Indicates whether to display all Regions. filters (str): A filter name and value pair that is used to return a more specific list of results. name=,values=;name=,values= Returns: CommandResults: A list of Regions. """ region_names = argToList(args.get("region_names", "")) all_regions = arg_to_bool_or_none(args.get("all_regions")) if region_names and all_regions is not None: raise DemistoException("Only one of the arguments 'region_name' and 'all_regions' should be provided.") kwargs = {"RegionNames": region_names, "AllRegions": all_regions, "Filters": parse_filter_field(args.get("filters"))} remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.describe_regions(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] not in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) regions = response.get("Regions") readable_output = tableToMarkdown( "The regions information:", regions, removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EC2.Regions", outputs=regions, raw_response=response, outputs_key_field="RegionName", ) @staticmethod def create_security_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a new security group in the specified VPC or EC2-Classic. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - group_name (str): Name of the security group. - description (str): Description of the security group. - vpc_id (str, optional): VPC ID where security group will be created. Returns: CommandResults: Results of the operation with security group creation details """ group_name = args.get("group_name") description = args.get("description") vpc_id = args.get("vpc_id") kwargs = {"Description": description, "GroupName": group_name, "VpcId": vpc_id} remove_nulls_from_dictionary(kwargs) response = client.create_security_group(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK and (group_id := response.get("GroupId")): return CommandResults( readable_output=f'The security group "{group_id}" was created successfully.', raw_response=response, ) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def delete_security_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes a security group. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - group_id (str, optional): ID of the security group to delete. - group_name (str, optional): Name of the security group to delete. Returns: CommandResults: Results of the operation with deletion confirmation """ group_id = args.get("group_id") group_name = args.get("group_name") if not group_id and not group_name: raise DemistoException("Either group_id or group_name must be provided") if group_id and group_name: raise DemistoException("Cannot specify both group_id and group_name. Please provide only one.") kwargs = {} if group_id: kwargs["GroupId"] = group_id else: kwargs["GroupName"] = group_name remove_nulls_from_dictionary(kwargs) response = client.delete_security_group(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK and response.get("GroupId"): return CommandResults( readable_output=f"Successfully deleted security group: {response.get('GroupId')}", raw_response=response, ) else: # If group_id was not found or no GroupId in response, raise an exception return AWSErrorHandler.handle_response_error(response) @staticmethod def describe_security_groups_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more security groups in your account. Returns detailed information about security groups including their rules, tags, and associated VPC information. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - group_ids (str, optional): Comma-separated list of security group IDs - group_names (str, optional): Comma-separated list of security group names - filters (str, optional): Custom filters to apply Returns: CommandResults: Results containing security group details """ kwargs = {} data = [] if args.get("filters") is not None: kwargs.update({"Filters": parse_filter_field(args.get("filters"))}) if args.get("group_ids") is not None: kwargs.update({"GroupIds": argToList(args.get("group_ids", []))}) if args.get("group_names") is not None: kwargs.update({"GroupNames": argToList(args.get("group_names", []))}) # Can't add limit when specify GroupIds or GroupNames if not args.get("group_ids") and not args.get("group_names"): kwargs.update(build_pagination_kwargs(args)) remove_nulls_from_dictionary(kwargs) response = client.describe_security_groups(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: if len(response["SecurityGroups"]) == 0: return CommandResults(readable_output="No security groups were found.") for _, sg in enumerate(response["SecurityGroups"]): data.append( { "Description": sg["Description"], "GroupName": sg["GroupName"], "OwnerId": sg["OwnerId"], "GroupId": sg["GroupId"], "VpcId": sg["VpcId"], "tags": sg.get("Tags"), } ) output = json.dumps(response["SecurityGroups"], cls=DatetimeEncoder) outputs = { "AWS.EC2.SecurityGroups(val.GroupId && val.GroupId == obj.GroupId)": json.loads(output), "AWS.EC2(true)": {"SecurityGroupsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown("AWS EC2 SecurityGroups", data, removeNull=True), raw_response=response, ) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def describe_addresses_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more Elastic IP addresses. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - public_ips (str, optional): Comma-separated list of public IP addresses - allocation_ids (str, optional): Comma-separated list of allocation IDs Returns: CommandResults: Results containing Elastic IP address information """ kwargs = {} # Add filters if provided if filters_arg := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters_arg) # Add public IPs if provided if public_ips := args.get("public_ips"): kwargs["PublicIps"] = parse_resource_ids(public_ips) # Add allocation IDs if provided if allocation_ids := args.get("allocation_ids"): kwargs["AllocationIds"] = parse_resource_ids(allocation_ids) print_debug_logs(client, f"Describing addresses with parameters: {kwargs}") remove_nulls_from_dictionary(kwargs) response = client.describe_addresses(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) addresses = response.get("Addresses", []) if not addresses: return CommandResults(readable_output="No Elastic IP addresses were found.") # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) addresses = response.get("Addresses", []) return CommandResults( outputs_prefix="AWS.EC2.ElasticIPs", outputs_key_field="AllocationId", outputs=addresses, readable_output=tableToMarkdown( "AWS EC2 Elastic IP Addresses", addresses, headers=[ "PublicIp", "AllocationId", "Domain", "InstanceId", "AssociationId", "NetworkInterfaceId", "PrivateIpAddress", ], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def allocate_address_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Allocates an Elastic IP address to your AWS account. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - address (str, optional): The Elastic IP address to recover - public_ipv4_pool (str, optional): The ID of an address pool - network_border_group (str, optional): A unique set of Availability Zones, Local Zones, or Wavelength Zones - customer_owned_ipv4_pool (str, optional): The ID of a customer-owned address pool - tag_specifications (str, optional): Tags to assign to the Elastic IP address Returns: CommandResults: Results containing the allocated Elastic IP information """ kwargs = { "Address": args.get("address"), "PublicIpv4Pool": args.get("public_ipv4_pool"), "NetworkBorderGroup": args.get("network_border_group"), "CustomerOwnedIpv4Pool": args.get("customer_owned_ipv4_pool"), } if tag_specifications := args.get("tag_specifications"): kwargs["TagSpecifications"] = [{"ResourceType": "elastic-ip", "Tags": parse_tag_field(tag_specifications)}] remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Allocating address with parameters: {kwargs}") response = client.allocate_address(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} return CommandResults( outputs_prefix="AWS.EC2.ElasticIPs", outputs_key_field="AllocationId", outputs=outputs, readable_output=tableToMarkdown( "AWS EC2 Allocated Elastic IP", outputs, headers=["PublicIp", "AllocationId", "Domain", "PublicIpv4Pool", "NetworkBorderGroup"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def associate_address_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Associates an Elastic IP address with an instance or a network interface. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - allocation_id (str): The allocation ID (required for VPC) - instance_id (str, optional): The ID of the instance - network_interface_id (str, optional): The ID of the network interface - private_ip_address (str, optional): The primary or secondary private IP address - allow_reassociation (str, optional): Whether to allow reassociation Returns: CommandResults: Results containing the association information """ kwargs = { "AllocationId": args.get("allocation_id"), "InstanceId": args.get("instance_id"), "NetworkInterfaceId": args.get("network_interface_id"), "PrivateIpAddress": args.get("private_ip_address"), "AllowReassociation": arg_to_bool_or_none(args.get("allow_reassociation")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Associating address with parameters: {kwargs}") response = client.associate_address(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Build output data output_data = { "AllocationId": args.get("allocation_id"), "AssociationId": response.get("AssociationId"), } output_data = remove_empty_elements(output_data) return CommandResults( outputs_prefix="AWS.EC2.ElasticIPs", outputs_key_field="AllocationId", outputs=output_data, readable_output=tableToMarkdown( "AWS EC2 Elastic IP Association", output_data, headers=["AllocationId", "AssociationId"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def disassociate_address_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Disassociates an Elastic IP address from the instance or network interface it's associated with. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - association_id (str): The association ID (required for VPC) Returns: CommandResults: Results of the disassociation operation """ kwargs = {"AssociationId": args.get("association_id")} print_debug_logs(client, f"Disassociating address with parameters: {kwargs}") response = client.disassociate_address(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Successfully disassociated Elastic IP address (Association ID: {args.get('association_id')})", raw_response=response, ) @staticmethod def release_address_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Releases the specified Elastic IP address. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - allocation_id (str): The allocation ID (required for VPC) - network_border_group (str, optional): The set of Availability Zones, Local Zones, or Wavelength Zones Returns: CommandResults: Results of the release operation """ kwargs = { "AllocationId": args.get("allocation_id"), "NetworkBorderGroup": args.get("network_border_group"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Releasing address with parameters: {kwargs}") response = client.release_address(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Successfully released Elastic IP address (Allocation ID: {args.get('allocation_id')})", raw_response=response, ) @staticmethod def authorize_security_group_egress_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Adds the specified outbound (egress) rules to a security group. The command supports two modes: 1. Simple mode: using protocol, port, and cidr arguments 2. Full mode: using ip_permissions for complex configurations """ kwargs: Dict[str, Any] = {"GroupId": args.get("group_id")} from_port = arg_to_number(args.get("from_port")) to_port = arg_to_number(args.get("to_port")) if ip_permissions := args.get("ip_permissions"): try: kwargs["IpPermissions"] = json.loads(ip_permissions) except json.JSONDecodeError as e: raise DemistoException(f"Received invalid `ip_permissions` JSON object: {e}") else: kwargs["IpPermissions"] = [ { "IpProtocol": args.get("protocol"), "FromPort": from_port, "ToPort": to_port, "IpRanges": [{"CidrIp": args.get("cidr")}] if args.get("cidr") else None, } ] remove_nulls_from_dictionary(kwargs["IpPermissions"][0]) response = client.authorize_security_group_egress(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK and response["Return"]: readable_output = ( "The Security Group egress rule was authorized" if response.get("SecurityGroupRules") else "No Security Group egress rule was authorized" ) return CommandResults(readable_output=readable_output, raw_response=response) else: return AWSErrorHandler.handle_response_error(response) @staticmethod def describe_images_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more Amazon Machine Images (AMIs) available to you. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - image_ids (str, optional): Comma-separated list of image IDs - owners (str, optional): Comma-separated list of image owners - executable_users (str, optional): Comma-separated list of users with explicit launch permissions - include_deprecated (str, optional): Whether to include deprecated AMIs - include_disabled (str, optional): Whether to include disabled AMIs - limit (int, optional): Maximum number of AMIs to return - next_token (str, optional): The token for the next set of AMIs to return. Returns: CommandResults: Results containing AMI information """ kwargs = {} # Add filters if provided if filters_arg := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters_arg) # Add image IDs if provided if image_ids := args.get("image_ids"): kwargs["ImageIds"] = parse_resource_ids(image_ids) # Add owners if provided if owners := args.get("owners"): kwargs["Owners"] = parse_resource_ids(owners) # Add executable users if provided if executable_users := args.get("executable_users"): kwargs["ExecutableUsers"] = parse_resource_ids(executable_users) # Add include_deprecated if provided if include_deprecated := args.get("include_deprecated"): kwargs["IncludeDeprecated"] = argToBoolean(include_deprecated) # Add include_disabled if provided if include_disabled := args.get("include_disabled"): kwargs["IncludeDisabled"] = argToBoolean(include_disabled) pagination_kwargs = build_pagination_kwargs(args, minimum_limit=5) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing images with parameters: {kwargs}") response = client.describe_images(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) images = response.get("Images", []) if not images: return CommandResults(readable_output="No images were found.") # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) images = response.get("Images", []) outputs = { "AWS.EC2.Images(val.ImageId && val.ImageId == obj.ImageId)": images, "AWS.EC2(true)": { "ImagesNextToken": response.get("NextToken"), }, } next_token = response.get("NextToken") next_token_text = f"ImagesNextToken: {escape(next_token)}" if next_token else "" return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS EC2 Images", images, headers=["ImageId", "Name", "CreationDate", "State", "Public", "Description"], removeNull=True, headerTransform=pascalToSpace, metadata=next_token_text, ), raw_response=response, ) @staticmethod def create_image_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates an Amazon EBS-backed AMI from an Amazon EBS-backed instance. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - name (str): A name for the new image (required) - instance_id (str): The ID of the instance (required) - description (str, optional): A description for the new image - no_reboot (boolean, optional): By default, Amazon EC2 attempts to shut down and reboot the instance before creating the image. If set to true, Amazon EC2 won't shut down the instance - block_device_mappings (str, optional): JSON string of block device mappings - tag_specifications (str, optional): Tags to apply to the AMI and snapshots Returns: CommandResults: Results containing the created AMI information """ kwargs = { "Name": args.get("name"), "InstanceId": args.get("instance_id"), "Description": args.get("description"), "NoReboot": arg_to_bool_or_none(args.get("no_reboot")), } # Handle block device mappings if provided if block_device_mappings := args.get("block_device_mappings"): try: kwargs["BlockDeviceMappings"] = ( json.loads(block_device_mappings) if isinstance(block_device_mappings, str) else block_device_mappings ) except json.JSONDecodeError as e: raise DemistoException(f"Invalid block_device_mappings JSON: {e}") # Handle tag specifications if provided if tag_specifications := args.get("tag_specifications"): kwargs["TagSpecifications"] = [{"ResourceType": "image", "Tags": parse_tag_field(tag_specifications)}] remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Creating image with parameters: {kwargs}") response = client.create_image(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) # Build output data output_data = { "ImageId": response.get("ImageId"), "Name": args.get("name"), "InstanceId": args.get("instance_id"), "Region": args.get("region"), } output_data = remove_empty_elements(output_data) return CommandResults( outputs_prefix="AWS.EC2.Images", outputs_key_field="ImageId", outputs=output_data, readable_output=tableToMarkdown( "AWS EC2 Image Created", output_data, headers=["ImageId", "Name", "InstanceId", "Region"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def deregister_image_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deregisters the specified Amazon Machine Image (AMI). After you deregister an AMI, it can't be used to launch new instances. However, it doesn't affect any instances that you've already launched from the AMI. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - image_id (str): The ID of the AMI to deregister (required) Returns: CommandResults: Results of the deregistration operation """ image_id = args.get("image_id") print_debug_logs(client, f"Deregistering image: {image_id}") response = client.deregister_image(ImageId=image_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Successfully deregistered AMI: {image_id}", raw_response=response, ) @staticmethod def copy_image_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Copy an Amazon Machine Image (AMI) from a source region to the current region. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - name (str): Name for the new AMI in the destination region (required) - source_image_id (str): ID of the AMI to copy (required) - source_region (str): Region that contains the AMI to copy (required) - description (str, optional): Description for the new AMI - encrypted (boolean, optional): Whether destination snapshots should be encrypted - kms_key_id (str, optional): KMS key ID for encryption - client_token (str, optional): Idempotency token Returns: CommandResults: Results containing the new ImageId and Region """ # Validate required parameters name = args.get("name", "") source_image_id = args.get("source_image_id", "") source_region = args.get("source_region", "") print_debug_logs(client, f"Copying image {source_image_id} from region {source_region}") # Build API parameters kwargs: Dict[str, Any] = { "Name": name, "SourceImageId": source_image_id, "SourceRegion": source_region, "Description": args.get("description"), "Encrypted": arg_to_bool_or_none(args.get("encrypted")), "KmsKeyId": args.get("kms_key_id"), "ClientToken": args.get("client_token"), } # Remove None values remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Copying image with parameters: {kwargs}") response = client.copy_image(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) region = args.get("region", "") # Prepare outputs outputs = { "ImageId": response["ImageId"], "Name": name, "SourceImageId": source_image_id, "SourceRegion": source_region, "Region": region, } # Prepare human-readable output readable_output = tableToMarkdown( "AWS EC2 Image Copy", outputs, headers=["ImageId", "Name", "SourceImageId", "SourceRegion", "Region"], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Images", outputs_key_field="ImageId", outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def image_available_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Waits until an Amazon Machine Image (AMI) becomes available. This command uses AWS EC2's built-in waiter functionality to poll the image state until it reaches the 'available' state. The waiter will check the image status at regular intervals (configurable via waiter_delay) up to a maximum number of attempts (configurable via waiter_max_attempts). Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - image_ids (str, optional): Comma-separated list of image IDs to wait for - owners (str, optional): Comma-separated list of image owners - executable_users (str, optional): Comma-separated list of users with explicit launch permissions - waiter_delay (str, optional): Time in seconds to wait between polling attempts (default: 15) - waiter_max_attempts (str, optional): Maximum number of polling attempts (default: 40) Returns: CommandResults: Results with success message when image becomes available Raises: WaiterError: If the waiter times out or encounters an error """ kwargs: Dict[str, Any] = {} # Add optional filters if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) # Add optional image IDs if image_ids := args.get("image_ids"): kwargs["ImageIds"] = parse_resource_ids(image_ids) # Add optional executable users if executable_users := args.get("executable_users"): kwargs["ExecutableUsers"] = parse_resource_ids(executable_users) # Add optional owners if owners := args.get("owners"): kwargs["Owners"] = parse_resource_ids(owners) # Configure waiter settings waiter_config: Dict[str, int] = {} if waiter_delay := arg_to_number(args.get("waiter_delay")): waiter_config["Delay"] = waiter_delay if waiter_max_attempts := arg_to_number(args.get("waiter_max_attempts")): waiter_config["MaxAttempts"] = waiter_max_attempts if waiter_config: kwargs["WaiterConfig"] = waiter_config remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Waiting for image to become available with parameters: {kwargs}") try: waiter = client.get_waiter("image_available") waiter.wait(**kwargs) return CommandResults(readable_output="Image is now available.") except Exception as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def monitor_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Enables detailed monitoring for one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str): Comma-separated list of instance IDs to monitor Returns: CommandResults: Results of the operation with monitoring status information """ instance_ids = parse_resource_ids(args.get("instance_ids")) print_debug_logs(client, f"Monitoring instance(s): {instance_ids}") response = client.monitor_instances(InstanceIds=instance_ids) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) instance_monitorings = response.get("InstanceMonitorings", []) if not instance_monitorings: return CommandResults(readable_output="No instances were monitored.") # Format output data readable_data = [] for monitoring in instance_monitorings: readable_data.append( {"InstanceId": monitoring.get("InstanceId"), "MonitoringState": monitoring.get("Monitoring", {}).get("State")} ) readable_output = tableToMarkdown( "Successfully enabled monitoring for instances", readable_data, headers=["InstanceId", "MonitoringState"], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Instances", outputs_key_field="InstanceId", outputs=instance_monitorings, readable_output=readable_output, raw_response=response, ) @staticmethod def unmonitor_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Disables detailed monitoring for one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str): Comma-separated list of instance IDs to unmonitor Returns: CommandResults: Results of the operation with monitoring status information """ instance_ids = parse_resource_ids(args.get("instance_ids")) print_debug_logs(client, f"Unmonitoring instance(s): {instance_ids}") response = client.unmonitor_instances(InstanceIds=instance_ids) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) instance_monitorings = response.get("InstanceMonitorings", []) if not instance_monitorings: return CommandResults(readable_output="No instances were unmonitored.") # Format output data readable_data = [] for monitoring in instance_monitorings: readable_data.append( {"InstanceId": monitoring.get("InstanceId"), "MonitoringState": monitoring.get("Monitoring", {}).get("State")} ) readable_output = tableToMarkdown( "Successfully disabled monitoring for instances", readable_data, headers=["InstanceId", "MonitoringState"], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Instances", outputs_key_field="InstanceId", outputs=instance_monitorings, readable_output=readable_output, raw_response=response, ) @staticmethod def reboot_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Requests a reboot of one or more Amazon EC2 instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str): Comma-separated list of instance IDs to reboot Returns: CommandResults: Results of the operation with reboot confirmation """ instance_ids = parse_resource_ids(args.get("instance_ids")) print_debug_logs(client, f"Rebooting instance(s): {instance_ids}") response = client.reboot_instances(InstanceIds=instance_ids) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Successfully initiated reboot for instances: {', '.join(instance_ids)}", raw_response=response ) @staticmethod def instance_running_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Waits until EC2 instances are in the 'running' state. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str, optional): Comma-separated list of instance IDs - filters (str, optional): Filters for instances - waiter_delay (int, optional): Delay between attempts in seconds (default: 15) - waiter_max_attempts (int, optional): Maximum number of attempts (default: 40) Returns: CommandResults: Results indicating instances are running """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if instance_ids := args.get("instance_ids"): kwargs["InstanceIds"] = parse_resource_ids(instance_ids) waiter_config = { "Delay": arg_to_number(args.get("waiter_delay", "15")), "MaxAttempts": arg_to_number(args.get("waiter_max_attempts", "40")), } kwargs["WaiterConfig"] = waiter_config try: waiter = client.get_waiter("instance_running") waiter.wait(**kwargs) return CommandResults(readable_output="Instance(s) are now running.") except Exception as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def instance_status_ok_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Waits until EC2 instance status checks pass. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str, optional): Comma-separated list of instance IDs - filters (str, optional): Filters for instances - include_all_instances (bool, optional): Specifies whether to include the health status for all instances or only for those currently running. - waiter_delay (int, optional): Delay between attempts in seconds (default: 15) - waiter_max_attempts (int, optional): Maximum number of attempts (default: 40) Returns: CommandResults: Results indicating instance status is OK """ kwargs = {"IncludeAllInstances": arg_to_bool_or_none(args.get("include_all_instances"))} # IncludeAllInstances if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if instance_ids := args.get("instance_ids"): kwargs["InstanceIds"] = parse_resource_ids(instance_ids) waiter_config = { "Delay": arg_to_number(args.get("waiter_delay", "15")), "MaxAttempts": arg_to_number(args.get("waiter_max_attempts", "40")), } kwargs["WaiterConfig"] = waiter_config remove_nulls_from_dictionary(kwargs) try: waiter = client.get_waiter("instance_status_ok") waiter.wait(**kwargs) return CommandResults(readable_output="Instance status is now OK.") except Exception as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def instance_stopped_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Waits until EC2 instances are in the 'stopped' state. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str, optional): Comma-separated list of instance IDs - filters (str, optional): Filters for instances - waiter_delay (int, optional): Delay between attempts in seconds (default: 15) - waiter_max_attempts (int, optional): Maximum number of attempts (default: 40) Returns: CommandResults: Results indicating instances are stopped """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if instance_ids := args.get("instance_ids"): kwargs["InstanceIds"] = parse_resource_ids(instance_ids) waiter_config = { "Delay": arg_to_number(args.get("waiter_delay", "15")), "MaxAttempts": arg_to_number(args.get("waiter_max_attempts", "40")), } kwargs["WaiterConfig"] = waiter_config try: waiter = client.get_waiter("instance_stopped") waiter.wait(**kwargs) return CommandResults(readable_output="Instance(s) are now stopped.") except Exception as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def instance_terminated_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Waits until EC2 instances are in the 'terminated' state. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_ids (str, optional): Comma-separated list of instance IDs - filters (str, optional): Filters for instances - waiter_delay (int, optional): Delay between attempts in seconds (default: 15) - waiter_max_attempts (int, optional): Maximum number of attempts (default: 40) Returns: CommandResults: Results indicating instances are terminated """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if instance_ids := args.get("instance_ids"): kwargs["InstanceIds"] = parse_resource_ids(instance_ids) waiter_config = { "Delay": arg_to_number(args.get("waiter_delay", "15")), "MaxAttempts": arg_to_number(args.get("waiter_max_attempts", "40")), } kwargs["WaiterConfig"] = waiter_config try: waiter = client.get_waiter("instance_terminated") waiter.wait(**kwargs) return CommandResults(readable_output="Instance(s) are now terminated.") except Exception as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def describe_iam_instance_profile_associations_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes IAM instance profile associations. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - association_ids (str, optional): Comma-separated list of association IDs - filters (str, optional): Filters for associations - limit (int, optional): Maximum number of results - next_token (str, optional): Token for pagination Returns: CommandResults: Results containing IAM instance profile association information """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if association_ids := args.get("association_ids"): kwargs["AssociationIds"] = parse_resource_ids(association_ids) pagination_kwargs = build_pagination_kwargs(args) kwargs.update(pagination_kwargs) print_debug_logs(client, f"Describe IAM instance profile associations parameters: {kwargs}") response = client.describe_iam_instance_profile_associations(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) associations = response.get("IamInstanceProfileAssociations", []) if not associations: return CommandResults(readable_output="No IAM instance profile associations were found.") outputs = { "AWS.EC2.IamInstanceProfileAssociations(val.AssociationId && val.AssociationId == obj.AssociationId)": associations, "AWS.EC2(true)": {"IamInstanceProfileAssociationsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS IAM Instance Profile Associations", associations, headers=["AssociationId", "InstanceId", "State", "IamInstanceProfile"], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def get_password_data_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the encrypted administrator password for a running Windows instance. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - instance_id (str): The ID of the Windows instance Returns: CommandResults: Results containing the password data """ instance_id = args.get("instance_id") print_debug_logs(client, f"Get password data for instance {instance_id}") response = client.get_password_data(InstanceId=instance_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize datetime response = serialize_response_with_datetime_encoding(response) password_data = { "InstanceId": response.get("InstanceId"), "PasswordData": response.get("PasswordData"), "Timestamp": response.get("Timestamp"), } readable_output = tableToMarkdown( "AWS EC2 Instance Password Data", password_data, headers=["InstanceId", "PasswordData", "Timestamp"], headerTransform=pascalToSpace, removeNull=True, ) outputs = {"PasswordData": password_data, "InstanceId": password_data.get("InstanceId")} return CommandResults( outputs_prefix="AWS.EC2.Instances", outputs_key_field="InstanceId", outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def describe_reserved_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more Reserved Instances. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments containing: - reserved_instances_ids (str, optional): Comma-separated list of Reserved Instance IDs - filters (str, optional): Filters for Reserved Instances - offering_class (str, optional): The offering class (standard or convertible) - offering_type (str, optional): The offering type (Heavy Utilization | Medium Utilization | Light Utilization | No Upfront | Partial Upfront | All Upfront) Returns: CommandResults: Results containing Reserved Instance information """ kwargs = {"OfferingClass": args.get("offering_class"), "OfferingType": args.get("offering_type")} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if reserved_instances_ids := args.get("reserved_instances_ids"): kwargs["ReservedInstancesIds"] = parse_resource_ids(reserved_instances_ids) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing reserved instances with parameters: {kwargs}") response = client.describe_reserved_instances(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize datetime objects serialized_response = serialize_response_with_datetime_encoding(response) reserved_instances = serialized_response.get("ReservedInstances", []) print_debug_logs(client, f"Reserved Instances: {reserved_instances}") if not reserved_instances: return CommandResults(readable_output="No Reserved Instances were found.") readable_output = tableToMarkdown( "AWS EC2 Reserved Instances", reserved_instances, headers=[ "ReservedInstancesId", "InstanceType", "InstanceCount", "State", "Start", "End", "Duration", "OfferingClass", "Scope", ], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.ReservedInstances", outputs_key_field="ReservedInstancesId", outputs=reserved_instances, readable_output=readable_output, raw_response=response, ) @staticmethod def describe_volumes_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the specified EBS volumes or all of your EBS volumes. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including filters and volume IDs Returns: CommandResults: Results containing volume information """ kwargs = {} # Add filters if provided if filters_arg := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters_arg) # Add volume IDs if provided if volume_ids := args.get("volume_ids"): kwargs["VolumeIds"] = argToList(volume_ids) if not volume_ids: pagination_kwargs = build_pagination_kwargs(args, minimum_limit=5) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing volumes with parameters: {kwargs}") response = client.describe_volumes(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) volumes = response.get("Volumes", []) if not volumes: return CommandResults(readable_output="No EC2 volumes were found.") readable_output = tableToMarkdown( "AWS EC2 Volumes", volumes, headers=["VolumeId", "VolumeType", "AvailabilityZone", "Encrypted", "State", "CreateTime"], removeNull=True, ) outputs = { "AWS.EC2.Volumes(val.VolumeId && val.VolumeId == obj.VolumeId)": volumes, "AWS.EC2(true)": {"VolumesNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def modify_volume_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies several parameters of an existing EBS volume, including volume size, volume type, and IOPS capacity. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including volume ID and modification parameters Returns: CommandResults: Results containing volume modification information """ kwargs = { "VolumeId": args.get("volume_id"), "VolumeType": args.get("volume_type"), "MultiAttachEnabled": arg_to_bool_or_none(args.get("multi_attach_enabled")), "Iops": arg_to_number(args.get("iops")), "Size": arg_to_number(args.get("size")), "Throughput": arg_to_number(args.get("throughput")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Modifying volume with parameters: {kwargs}") response = client.modify_volume(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) volume_modification = response.get("VolumeModification", {}) outputs = { "VolumeId": volume_modification.pop("VolumeId", None), "Size": volume_modification.pop("TargetSize", None), "Iops": volume_modification.pop("TargetIops", None), "VolumeType": volume_modification.pop("TargetVolumeType", None), "Throughput": volume_modification.pop("TargetThroughput", None), "MultiAttachEnabled": volume_modification.pop("TargetMultiAttachEnabled", None), "Modification": volume_modification, } remove_nulls_from_dictionary(outputs) readable_output = tableToMarkdown( "AWS EC2 Volume Modification", outputs, headers=[ "VolumeId", "Size", "Iops", "VolumeType", "Throughput", "MultiAttachEnabled", ], removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Volumes", outputs_key_field="VolumeId", outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def create_volume_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates an EBS volume that can be attached to an instance in the same Availability Zone. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including availability zone and volume parameters Returns: CommandResults: Results containing created volume information """ kwargs = { "AvailabilityZone": args.get("availability_zone"), "Encrypted": arg_to_bool_or_none(args.get("encrypted")), "KmsKeyId": args.get("kms_key_id"), "OutpostArn": args.get("outpost_arn"), "SnapshotId": args.get("snapshot_id"), "VolumeType": args.get("volume_type"), "MultiAttachEnabled": arg_to_bool_or_none(args.get("multi_attach_enabled")), "ClientToken": args.get("client_token"), "Iops": arg_to_number(args.get("iops")), "Size": arg_to_number(args.get("size")), "Throughput": arg_to_number(args.get("throughput")), } if tags := args.get("tags"): kwargs["TagSpecifications"] = [{"ResourceType": "volume", "Tags": parse_tag_field(tags)}] remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Creating volume with parameters: {kwargs}") response = client.create_volume(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} readable_output = tableToMarkdown( "AWS EC2 Volumes", outputs, headers=["VolumeId", "VolumeType", "AvailabilityZone", "CreateTime", "Encrypted", "Size", "State", "Iops"], removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Volumes", outputs_key_field="VolumeId", outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def attach_volume_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Attaches an EBS volume to a running or stopped instance and exposes it to the instance with the specified device name. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including device, instance ID, and volume ID Returns: CommandResults: Results containing volume attachment information """ kwargs = { "Device": args.get("device"), "InstanceId": args.get("instance_id"), "VolumeId": args.get("volume_id"), } print_debug_logs(client, f"Attaching volume with parameters: {kwargs}") response = client.attach_volume(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} readable_output = tableToMarkdown( "AWS EC2 Volume Attachments", outputs, headers=["VolumeId", "InstanceId", "AttachTime", "Device", "State", "DeleteOnTermination"], removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Volumes", outputs_key_field="VolumeId", outputs={"Attachments": outputs, "VolumeId": response.get("VolumeId")}, readable_output=readable_output, raw_response=response, ) @staticmethod def detach_volume_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Detaches an EBS volume from an instance. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including volume ID and optional parameters Returns: CommandResults: Results containing volume detachment information """ kwargs = { "VolumeId": args.get("volume_id"), "Force": arg_to_bool_or_none(args.get("force")), "Device": args.get("device"), "InstanceId": args.get("instance_id"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Detaching volume with parameters: {kwargs}") response = client.detach_volume(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} readable_output = tableToMarkdown( "AWS EC2 Volume Attachments", outputs, headers=["VolumeId", "InstanceId", "AttachTime", "Device", "State", "DeleteOnTermination"], removeNull=True, ) return CommandResults( outputs_prefix="AWS.EC2.Volumes", outputs_key_field="VolumeId", outputs={"Attachments": outputs, "VolumeId": response.get("VolumeId")}, readable_output=readable_output, raw_response=response, ) @staticmethod def delete_volume_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified EBS volume. The volume must be in the available state (not attached to an instance). Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including volume ID Returns: CommandResults: Results with success message """ volume_id = args.get("volume_id") print_debug_logs(client, f"Deleting volume: {volume_id}") response = client.delete_volume(VolumeId=volume_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully deleted volume {volume_id}") @staticmethod def describe_snapshots_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more Amazon EBS snapshots available to you. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - owner_ids (str, optional): Comma-separated list of snapshot owner IDs - snapshot_ids (str, optional): Comma-separated list of snapshot IDs - restorable_by_user_ids (str, optional): Comma-separated list of user IDs that can create volumes from the snapshot - limit (int, optional): Maximum number of snapshots to return - next_token (str, optional): Token for pagination Returns: CommandResults: Results containing snapshot information including description, encryption status, owner, progress, state, and volume details """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if owner_ids := args.get("owner_ids"): kwargs["OwnerIds"] = parse_resource_ids(owner_ids) if snapshot_ids := args.get("snapshot_ids"): kwargs["SnapshotIds"] = parse_resource_ids(snapshot_ids) if restorable_by_user_ids := args.get("restorable_by_user_ids"): kwargs["RestorableByUserIds"] = parse_resource_ids(restorable_by_user_ids) if not snapshot_ids: pagination_kwargs = build_pagination_kwargs(args) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing snapshots with parameters: {kwargs}") response = client.describe_snapshots(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) snapshots = response.get("Snapshots", []) if not snapshots: return CommandResults(readable_output="No snapshots were found.") readable_output = tableToMarkdown( "AWS EC2 Snapshots", snapshots, headers=[ "SnapshotId", "Description", "VolumeId", "VolumeSize", "Encrypted", "OwnerId", "Progress", "StartTime", "State", ], removeNull=True, headerTransform=pascalToSpace, ) outputs = { "AWS.EC2.Snapshots(val.SnapshotId && val.SnapshotId == obj.SnapshotId)": snapshots, "AWS.EC2(true)": {"SnapshotsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def delete_snapshot_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified Amazon EBS snapshot. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - snapshot_id (str): The ID of the snapshot to delete (required) Returns: CommandResults: Results of the deletion operation with success message """ snapshot_id = args.get("snapshot_id") print_debug_logs(client, f"Deleting snapshot: {snapshot_id}") response = client.delete_snapshot(SnapshotId=snapshot_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully deleted snapshot {snapshot_id}", raw_response=response) @staticmethod def copy_snapshot_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Copies a point-in-time snapshot of an Amazon EBS volume and stores it in Amazon S3. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - source_snapshot_id (str): The ID of the snapshot to copy (required) - source_region (str): The region containing the source snapshot (required) - description (str, optional): Description for the new snapshot - destination_outpost_arn (str, optional): The ARN of the Outpost to which to copy the snapshot - encrypted (boolean, optional): Whether the destination snapshot should be encrypted - kms_key_id (str, optional): KMS key ID for encryption - presigned_url (str, optional): Pre-signed URL for the copy operation - tag_specifications (str, optional): Tags to apply to the new snapshot Returns: CommandResults: Results containing the new snapshot ID and region information """ kwargs = { "SourceSnapshotId": args.get("source_snapshot_id"), "SourceRegion": args.get("source_region"), "Description": args.get("description"), "DestinationOutpostArn": args.get("destination_outpost_arn"), "Encrypted": arg_to_bool_or_none(args.get("encrypted")), "KmsKeyId": args.get("kms_key_id"), "PresignedUrl": args.get("presigned_url"), } if tag_specifications := args.get("tag_specifications"): kwargs["TagSpecifications"] = [{"ResourceType": "snapshot", "Tags": parse_tag_field(tag_specifications)}] remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Copying snapshot with parameters: {kwargs}") response = client.copy_snapshot(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} readable_output = tableToMarkdown( "Copy AWS EC2 Snapshots", outputs, headers=["SnapshotId"], removeNull=True, headerTransform=pascalToSpace ) return CommandResults( outputs_prefix="AWS.EC2.Snapshots", outputs_key_field="SnapshotId", outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def snapshot_completed_waiter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Waits until an Amazon EBS snapshot reaches the completed state. This command uses AWS EC2's built-in waiter functionality to poll the snapshot state until it reaches the 'completed' state. The waiter will check the snapshot status at regular intervals (configurable via waiter_delay) up to a maximum number of attempts (configurable via waiter_max_attempts). Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - owner_ids (str, optional): Comma-separated list of snapshot owner IDs - snapshot_ids (str, optional): Comma-separated list of snapshot IDs to wait for - restorable_by_user_ids (str, optional): Comma-separated list of user IDs that can create volumes from the snapshot - waiter_delay (str, optional): Time in seconds to wait between polling attempts (default: 15) - waiter_max_attempts (str, optional): Maximum number of polling attempts (default: 40) Returns: CommandResults: Results with success message when snapshot is completed Raises: WaiterError: If the waiter times out or encounters an error """ kwargs = {} if filters := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters) if owner_ids := args.get("owner_ids"): kwargs["OwnerIds"] = parse_resource_ids(owner_ids) if snapshot_ids := args.get("snapshot_ids"): kwargs["SnapshotIds"] = parse_resource_ids(snapshot_ids) if restorable_by_user_ids := args.get("restorable_by_user_ids"): kwargs["RestorableByUserIds"] = parse_resource_ids(restorable_by_user_ids) # Configure waiter settings kwargs["WaiterConfig"] = { "Delay": arg_to_number(args.get("waiter_delay")), "MaxAttempts": arg_to_number(args.get("waiter_max_attempts")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Waiting for snapshot completion with parameters: {kwargs}") try: waiter = client.get_waiter("snapshot_completed") waiter.wait(**kwargs) return CommandResults(readable_output="Snapshot is now completed.") except WaiterError as e: raise DemistoException(f"Waiter error: {str(e)}") @staticmethod def describe_launch_templates_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more launch templates. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - filters (str, optional): One or more filters separated by ';' - launch_template_ids (str, optional): Comma-separated list of launch template IDs - launch_template_names (str, optional): Comma-separated list of launch template names - limit (int, optional): Maximum number of results to return - next_token (str, optional): Token for the next set of results Returns: CommandResults: Results containing launch template information """ kwargs: Dict[str, Any] = { "LaunchTemplateIds": argToList(args.get("launch_template_ids")), "LaunchTemplateNames": argToList(args.get("launch_template_names")), } # Add filters if provided if filters_arg := args.get("filters"): kwargs["Filters"] = parse_filter_field(filters_arg) pagination_kwargs = build_pagination_kwargs(args, minimum_limit=1, max_limit=200) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing launch templates with parameters: {kwargs}") response = client.describe_launch_templates(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) launch_templates = response.get("LaunchTemplates", []) if not launch_templates: return CommandResults(readable_output="No launch templates were found.") outputs = { "AWS.EC2.LaunchTemplates(val.LaunchTemplateId && val.LaunchTemplateId == obj.LaunchTemplateId)": launch_templates, "AWS.EC2(true)": {"LaunchTemplatesNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS EC2 LaunchTemplates", launch_templates, headers=[ "LaunchTemplateId", "LaunchTemplateName", "CreatedBy", "DefaultVersionNumber", "LatestVersionNumber", "CreateTime", ], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def create_launch_template_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a launch template. A launch template contains the parameters to launch an instance. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including launch template configuration Returns: CommandResults: Results containing the created launch template information """ kwargs: Dict[str, Any] = remove_empty_elements(create_launch_template_kwargs_builder(args)) print_debug_logs(client, f"Creating launch template with parameters: {kwargs.keys()}") response = client.create_launch_template(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) launch_template = response.get("LaunchTemplate", {}) return CommandResults( outputs_prefix="AWS.EC2.LaunchTemplates", outputs_key_field="LaunchTemplateId", outputs=launch_template, readable_output=tableToMarkdown( "The AWS Launch Template was created successfully", launch_template, headers=[ "LaunchTemplateId", "LaunchTemplateName", "CreateTime", "CreatedBy", "DefaultVersionNumber", "LatestVersionNumber", ], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def delete_launch_template_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes a launch template. Deleting a launch template deletes all of its versions. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including: - launch_template_id (str, optional): The ID of the launch template - launch_template_name (str, optional): The name of the launch template Returns: CommandResults: Results of the deletion operation """ kwargs: Dict[str, Any] = remove_empty_elements( { "LaunchTemplateId": args.get("launch_template_id"), "LaunchTemplateName": args.get("launch_template_name"), } ) if not kwargs or len(kwargs) > 1: raise DemistoException("Either launch_template_id or launch_template_name must be provided, but not both.") print_debug_logs(client, f"Deleting launch template with parameters: {kwargs}") response = client.delete_launch_template(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response to handle datetime objects response = serialize_response_with_datetime_encoding(response) deleted_template = response.get("LaunchTemplate", {}) return CommandResults( outputs_prefix="AWS.EC2.DeletedLaunchTemplates", outputs_key_field="LaunchTemplateId", outputs=deleted_template, readable_output=tableToMarkdown( "Successfully deleted the AWS Launch Template", deleted_template, headers=[ "LaunchTemplateId", "LaunchTemplateName", "CreateTime", "CreatedBy", "DefaultVersionNumber", "LatestVersionNumber", ], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def create_fleet_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Launches an EC2 Fleet. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including fleet configuration parameters Returns: CommandResults: Results containing the created fleet ID """ launch_template_info = remove_empty_elements([args.get("launch_template_id"), args.get("launch_template_name")]) if len(launch_template_info) != 1: raise DemistoException("Either launch_template_id or launch_template_name must be provided, but not both.") kwargs: Dict[str, Any] = remove_empty_elements(aws_ec2_fleet_create_args_builder(args)) print_debug_logs(client, f"Creating fleet with parameters: {kwargs}") response = client.create_fleet(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) outputs = {k: v for k, v in response.items() if k != "ResponseMetadata"} return CommandResults( outputs_prefix="AWS.EC2.Fleets", outputs_key_field="FleetId", outputs=outputs, readable_output=tableToMarkdown( "The AWS EC2 Fleet was created successfully", outputs, headers=["FleetId"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def delete_fleet_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified EC2 Fleet. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including fleet IDs and termination settings Returns: CommandResults: Results containing successful and unsuccessful fleet deletions """ kwargs = { "FleetIds": argToList(args.get("fleet_ids")), "TerminateInstances": arg_to_bool_or_none(args.get("terminate_instances")), } print_debug_logs(client, f"Deleting fleets with parameters: {kwargs}") response = client.delete_fleets(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) successful = response.get("SuccessfulFleetDeletions", []) unsuccessful = response.get("UnsuccessfulFleetDeletions", []) readable_data = [ { "FleetId": deletion.get("FleetId"), "CurrentFleetState": deletion.get("CurrentFleetState"), "PreviousFleetState": deletion.get("PreviousFleetState"), } for deletion in successful ] + [ { "FleetId": deletion.get("FleetId"), "ErrorCode": deletion.get("Error", {}).get("Code"), "ErrorMessage": deletion.get("Error", {}).get("Message"), } for deletion in unsuccessful ] if not readable_data: return CommandResults(readable_output="No fleets were deleted.") outputs = remove_empty_elements( { "SuccessfulFleetDeletions": successful, "UnsuccessfulFleetDeletions": unsuccessful, } ) return CommandResults( outputs_prefix="AWS.EC2.DeletedFleets", outputs=outputs, readable_output=tableToMarkdown( "AWS Deleted Fleets", readable_data, headers=["FleetId", "CurrentFleetState", "PreviousFleetState", "ErrorCode", "ErrorMessage"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def describe_fleets_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more of your EC2 Fleets. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including fleet IDs and filters Returns: CommandResults: Results containing fleet information """ kwargs = remove_empty_elements( { "Filters": parse_filter_field(args.get("filters")), "FleetIds": argToList(args.get("fleet_ids")), } ) # Add pagination if no fleet_ids specified if not kwargs.get("FleetIds"): kwargs.update(build_pagination_kwargs(args)) print_debug_logs(client, f"Describing fleets with parameters: {kwargs}") response = client.describe_fleets(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) fleets = response.get("Fleets", []) if not fleets: return CommandResults(readable_output="No fleets were found.") outputs = { "AWS.EC2.Fleets(val.FleetId && val.FleetId == obj.FleetId)": fleets, "AWS.EC2(true)": {"FleetsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS EC2 Fleets", fleets, headers=["FleetId", "FleetState", "ActivityStatus", "FulfilledCapacity", "TotalTargetCapacity"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def describe_fleet_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the running instances for the specified EC2 Fleet. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including fleet ID and filters Returns: CommandResults: Results containing fleet instance information """ kwargs = remove_empty_elements( { "FleetId": args.get("fleet_id"), "Filters": parse_filter_field(args.get("filters")), } ) kwargs.update(build_pagination_kwargs(args)) print_debug_logs(client, f"Describing fleet instances with parameters: {kwargs}") response = client.describe_fleet_instances(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) active_instances = response.get("ActiveInstances", []) if not active_instances: return CommandResults(readable_output="No active instances were found.") response["FleetInstancesNextToken"] = response.pop("NextToken", None) response_data = {k: v for k, v in response.items() if k != "ResponseMetadata"} return CommandResults( outputs_prefix="AWS.EC2.Fleets", outputs=response_data, outputs_key_field="FleetId", readable_output=tableToMarkdown( f"AWS EC2 Fleet {args.get('fleet_id')} Instances", active_instances, headers=["InstanceId", "InstanceType", "SpotInstanceRequestId", "InstanceHealth"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def modify_fleet_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the specified EC2 Fleet. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including fleet ID and modification parameters Returns: CommandResults: Results of the modification operation """ # LaunchTemplateConfigs is optional for modify — only include if a template is specified launch_template_configs = None if args.get("launch_template_id") or args.get("launch_template_name"): launch_template_configs = aws_ec2_fleet_command_launch_templates_config_args_builder(args) kwargs = remove_empty_elements( { "FleetId": args.get("fleet_id"), "ExcessCapacityTerminationPolicy": args.get("excess_capacity_termination_policy"), "TargetCapacitySpecification": { "TotalTargetCapacity": arg_to_number(args.get("total_target_capacity")), "OnDemandTargetCapacity": arg_to_number(args.get("on_demand_target_capacity")), "SpotTargetCapacity": arg_to_number(args.get("spot_target_capacity")), "DefaultTargetCapacityType": args.get("default_target_capacity_type"), "TargetCapacityUnitType": args.get("target_capacity_unit"), }, "LaunchTemplateConfigs": launch_template_configs, } ) print_debug_logs(client, f"Modifying fleet with parameters: {kwargs}") response = client.modify_fleet(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( outputs_prefix="AWS.EC2.Fleets", readable_output=f"Successfully modified EC2 Fleet {args.get('fleet_id')}" if response.get("Return", False) else f"Failed to modify EC2 Fleet {args.get('fleet_id')}", raw_response=response, ) @staticmethod def delete_vpc_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified VPC. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including VPC ID Returns: CommandResults: Results with success message """ vpc_id = args.get("vpc_id") print_debug_logs(client, f"Deleting VPC: {vpc_id}") response = client.delete_vpc(VpcId=vpc_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully deleted VPC {vpc_id}") @staticmethod def create_vpc_endpoint_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a VPC endpoint for a specified service. An endpoint enables you to create a private connection between your VPC and the service. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including vpc configuration. Returns: CommandResults: Results containing VPC endpoint information """ kwargs = remove_empty_elements( { "VpcId": args.get("vpc_id"), "ServiceName": args.get("service_name"), "ServiceNetworkArn": args.get("service_network_arn"), "ClientToken": args.get("client_token"), "ServiceRegion": args.get("service_region"), "VpcEndpointType": args.get("vpc_endpoint_type"), "PolicyDocument": args.get("policy_document"), "RouteTableIds": argToList(args.get("route_table_ids")), "SubnetIds": argToList(args.get("subnet_ids")), "SecurityGroupIds": argToList(args.get("security_group_ids")), "IpAddressType": args.get("ip_address_type"), "PrivateDnsEnabled": arg_to_bool_or_none(args.get("private_dns_enabled")), "ResourceConfigurationArn": args.get("resource_configuration_arn"), "DnsOptions": { "DnsRecordIpType": args.get("dns_options_dns_record_ip_type"), "PrivateDnsOnlyForInboundResolverEndpoint": arg_to_bool_or_none( args.get("dns_options_private_dns_only_for_inbound_resolver_endpoint") ), "PrivateDnsPreference": args.get("dns_options_private_dns_preference"), "PrivateDnsSpecifiedDomains": argToList(args.get("dns_options_private_dns_specified_domains")), }, "SubnetConfigurations": { "Ipv4": args.get("subnet_configuration_ipv4"), "Ipv6": args.get("subnet_configuration_ipv6"), "SubnetId": args.get("subnet_configuration_subnet_id"), }, "TagSpecifications": [{"ResourceType": "vpc-endpoint", "Tags": parse_tag_field(args.get("tags"))}], } ) print_debug_logs(client, f"Creating VPC endpoint with parameters: {kwargs}") response = client.create_vpc_endpoint(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) vpc_endpoint = response.get("VpcEndpoint", {}) return CommandResults( outputs_prefix="AWS.EC2.VpcEndpoints", outputs_key_field="VpcEndpointId", outputs=vpc_endpoint, readable_output=tableToMarkdown( "Successfully created VPC Endpoint", vpc_endpoint, headers=["VpcEndpointId", "State", "ServiceName", "VpcId", "VpcEndpointType"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def describe_internet_gateways_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes one or more of your internet gateways. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including internet gateway id and filters Returns: CommandResults: Results containing internet gateway information """ kwargs = remove_empty_elements( { "InternetGatewayIds": argToList(args.get("internet_gateway_ids")), "Filters": parse_filter_field(args.get("filters")), } ) if not kwargs.get("InternetGatewayIds"): kwargs.update(build_pagination_kwargs(args, minimum_limit=5)) print_debug_logs(client, f"Describing internet gateways with parameters: {kwargs}") response = client.describe_internet_gateways(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) internet_gateways = response.get("InternetGateways", []) if not internet_gateways: return CommandResults(readable_output="No internet gateways were found.") # Prepare readable data with flattened attachment info readable_data = [] for igw in internet_gateways: igw_data = { "InternetGatewayId": igw.get("InternetGatewayId"), "OwnerId": igw.get("OwnerId"), "State": igw.get("Attachments")[0].get("State") if igw.get("Attachments") else None, "VpcId": igw.get("Attachments")[0].get("VpcId") if igw.get("Attachments") else None, } readable_data.append(igw_data) readable_output = tableToMarkdown( "AWS EC2 Internet Gateways", readable_data, headers=["InternetGatewayId", "OwnerId", "State", "VpcId"], removeNull=True, headerTransform=pascalToSpace, ) outputs = { "AWS.EC2.InternetGateways(" "val.InternetGatewayId && val.InternetGatewayId == obj.InternetGatewayId)": internet_gateways, "AWS.EC2(true)": {"InternetGatewaysNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) @staticmethod def detach_internet_gateway_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Detaches an internet gateway from a VPC, disabling connectivity between the internet and the VPC. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including internet gateway id and vpc id Returns: CommandResults: Results with success message """ internet_gateway_id = args.get("internet_gateway_id") vpc_id = args.get("vpc_id") print_debug_logs(client, f"Detaching internet gateway {internet_gateway_id} from VPC {vpc_id}") response = client.detach_internet_gateway(InternetGatewayId=internet_gateway_id, VpcId=vpc_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully detached internet gateway {internet_gateway_id} from VPC {vpc_id}") @staticmethod def delete_internet_gateway_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified internet gateway. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including internet gateway ID Returns: CommandResults: Results with success message """ internet_gateway_id = args.get("internet_gateway_id") print_debug_logs(client, f"Deleting internet gateway: {internet_gateway_id}") response = client.delete_internet_gateway(InternetGatewayId=internet_gateway_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully deleted internet gateway {internet_gateway_id}") @staticmethod def delete_subnet_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified subnet. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including subnet id Returns: CommandResults: Results with success message """ subnet_id = args.get("subnet_id") print_debug_logs(client, f"Deleting subnet: {subnet_id}") response = client.delete_subnet(SubnetId=subnet_id) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully deleted subnet {subnet_id}") @staticmethod def create_network_acl_entry_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates an entry (a rule) in a network ACL with the specified rule number. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including network acl configuration Returns: CommandResults: Results with success message """ protocols_2_numbers = {"tcp": "6", "udp": "17", "icmp": "1", "icmpv6": "58", "-1": "-1"} proto = args.get("protocol") if proto and proto.lower() in protocols_2_numbers: proto = protocols_2_numbers[proto.lower()] kwargs = remove_empty_elements( { "NetworkAclId": args.get("network_acl_id"), "RuleNumber": arg_to_number(args.get("rule_number")), "Protocol": proto, "RuleAction": args.get("rule_action"), "Egress": arg_to_bool_or_none(args.get("egress")), "CidrBlock": args.get("cidr_block"), "Ipv6CidrBlock": args.get("ipv6_cidr_block"), "IcmpTypeCode": { "Type": arg_to_number(args.get("icmp_type_code_type")), "Code": arg_to_number(args.get("icmp_type_code_code")), }, "PortRange": { "From": arg_to_number(args.get("port_range_from")), "To": arg_to_number(args.get("port_range_to")), }, } ) print_debug_logs(client, f"Creating network ACL entry with parameters: {kwargs}") response = client.create_network_acl_entry(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults(readable_output=f"Successfully created network ACL entry for {args.get('network_acl_id')}") @staticmethod def describe_key_pairs_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the specified key pairs or all of your key pairs. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including key pair IDs, names, and filters. Returns: CommandResults: Results containing key pair information. """ kwargs = remove_empty_elements( { "KeyPairIds": argToList(args.get("key_pair_ids")), "KeyNames": argToList(args.get("key_names")), "Filters": parse_filter_field(args.get("filters")), "IncludePublicKey": arg_to_bool_or_none(args.get("include_public_key")), } ) print_debug_logs(client, f"Describing key pairs with parameters: {kwargs}") response = client.describe_key_pairs(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) key_pairs = response.get("KeyPairs", []) if not key_pairs: return CommandResults(readable_output="No key pairs were found.") return CommandResults( outputs_prefix="AWS.EC2.KeyPairs", outputs_key_field="KeyPairId", outputs=key_pairs, readable_output=tableToMarkdown( "AWS EC2 Key Pairs", key_pairs, headers=["KeyPairId", "KeyName", "KeyType", "KeyFingerprint", "CreateTime"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def allocate_hosts_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Allocates a Dedicated Host to your account. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including availability zone, quantity, and host configuration. Returns: CommandResults: Results containing the allocated host IDs. """ tags = parse_tag_field(args.get("tags")) kwargs = remove_empty_elements( { "AvailabilityZone": args.get("availability_zone"), "AvailabilityZoneId": args.get("availability_zone_id"), "Quantity": arg_to_number(args.get("quantity")), "ClientToken": args.get("client_token"), "InstanceType": args.get("instance_type"), "InstanceFamily": args.get("instance_family"), "AutoPlacement": args.get("auto_placement"), "HostRecovery": args.get("host_recovery"), "HostMaintenance": args.get("host_maintenance"), "OutpostArn": args.get("outpost_arn"), "AssetIds": argToList(args.get("asset_ids")), "TagSpecifications": [{"ResourceType": "dedicated-host", "Tags": tags}] if tags else [], } ) print_debug_logs(client, f"Allocating Dedicated Hosts with parameters: {kwargs}") response = client.allocate_hosts(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) host_ids = response.get("HostIds", []) return CommandResults( outputs_prefix="AWS.EC2.Hosts", outputs=host_ids, readable_output=f"Successfully allocated {args.get('quantity')} Dedicated Host(s). Host IDs: {', '.join(host_ids)}", raw_response=response, ) @staticmethod def release_hosts_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Releases the specified Dedicated Hosts. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including host IDs to release. Returns: CommandResults: Results containing successful and unsuccessful release information. """ host_ids = argToList(args.get("host_ids")) print_debug_logs(client, f"Releasing Dedicated Hosts: {host_ids}") response = client.release_hosts(HostIds=host_ids) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = remove_empty_elements( { "Successful": response.get("Successful", []), "Unsuccessful": response.get("Unsuccessful", []), } ) readable_parts = [] if outputs.get("Successful"): successful_ids = ", ".join(h.get("HostId", "") for h in outputs.get("Successful", [])) readable_parts.append(f"Successfully released: {successful_ids}") if outputs.get("Unsuccessful"): unsuccessful_ids = ", ".join(h.get("ResourceId", "") for h in outputs.get("Unsuccessful", [])) readable_parts.append(f"Failed to release: {unsuccessful_ids}") return CommandResults( outputs_prefix="AWS.EC2.ReleasedHosts", outputs=outputs, readable_output="\n".join(readable_parts) if readable_parts else "No hosts were released.", raw_response=response, ) @staticmethod def create_traffic_mirror_session_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a Traffic Mirror session. Args: client (BotoClient): The boto3 client for EC2 service args (Dict[str, Any]): Command arguments including network interface, target, filter, and session configuration. Returns: CommandResults: Results containing the created Traffic Mirror session information. """ kwargs = remove_empty_elements( { "NetworkInterfaceId": args.get("network_interface_id"), "TrafficMirrorTargetId": args.get("traffic_mirror_target_id"), "TrafficMirrorFilterId": args.get("traffic_mirror_filter_id"), "SessionNumber": arg_to_number(args.get("session_number")), "VirtualNetworkId": arg_to_number(args.get("virtual_network_id")), "PacketLength": arg_to_number(args.get("packet_length")), "Description": args.get("description"), "ClientToken": args.get("client_token"), "TagSpecifications": [{"ResourceType": "traffic-mirror-session", "Tags": parse_tag_field(args.get("tags"))}], } ) print_debug_logs(client, f"Creating Traffic Mirror session with parameters: {kwargs}") response = client.create_traffic_mirror_session(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) session = response.get("TrafficMirrorSession", {}) return CommandResults( outputs_prefix="AWS.EC2.TrafficMirrorSessions", outputs_key_field="TrafficMirrorSessionId", outputs=session, readable_output=f"Successfully created Traffic Mirror Session {session.get('TrafficMirrorSessionId')}", raw_response=response, ) class EKS: service = AWSServices.EKS @staticmethod def update_cluster_config_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Updates an Amazon EKS cluster configuration. Only a single type of update (logging / resources_vpc_config) is allowed per call. Args: client (BotoClient): The boto3 client for EKS service args (Dict[str, Any]): Command arguments including cluster name and configuration options Returns: CommandResults: Results of the operation with update information """ def validate_args(args: Dict[str, Any]) -> dict: """ Check that exactly one argument is passed, and if not raises a value error """ validated_args = {"name": args.get("cluster_name")} if resources_vpc_config := args.get("resources_vpc_config"): resources_vpc_config = ( json.loads(resources_vpc_config) if isinstance(resources_vpc_config, str) else resources_vpc_config ) validated_args["resourcesVpcConfig"] = resources_vpc_config # Convert specific string boolean values to actual boolean values if isinstance(resources_vpc_config, dict): if "endpointPublicAccess" in resources_vpc_config and isinstance( resources_vpc_config["endpointPublicAccess"], str ): resources_vpc_config["endpointPublicAccess"] = ( resources_vpc_config["endpointPublicAccess"].lower() == "true" ) if "endpointPrivateAccess" in resources_vpc_config and isinstance( resources_vpc_config["endpointPrivateAccess"], str ): resources_vpc_config["endpointPrivateAccess"] = ( resources_vpc_config["endpointPrivateAccess"].lower() == "true" ) if logging_arg := args.get("logging"): logging_arg = json.loads(logging_arg) if isinstance(logging_arg, str) else logging_arg validated_args["logging"] = logging_arg if logging_arg and resources_vpc_config: raise ValueError result = remove_empty_elements(validated_args) if isinstance(result, dict): return result else: raise ValueError("No valid configuration argument provided") validated_args: dict = validate_args(args) try: response = client.update_cluster_config(**validated_args) response_data = response.get("update", {}) response_data["clusterName"] = validated_args["name"] response_data["createdAt"] = datetime_to_string(response_data.get("createdAt")) headers = ["clusterName", "id", "status", "type", "params"] readable_output = tableToMarkdown( name="Updated Cluster Config Information", t=response_data, removeNull=True, headers=headers, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EKS.UpdateCluster", outputs=response_data, raw_response=response_data, outputs_key_field="id", ) except Exception as e: if "No changes needed" in str(e): return CommandResults(readable_output="No changes needed for the required update.") else: raise e @staticmethod def describe_cluster_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Describes an Amazon EKS cluster. Args: client(boto3 client): The configured AWS session. args: command arguments Returns: A Command Results object """ cluster_name = args.get("cluster_name") print_debug_logs(client, f"Describing clusters with parameters: {cluster_name}") response = client.describe_cluster(name=cluster_name) response_data = response.get("cluster", {}) response_data["createdAt"] = datetime_to_string(response_data.get("createdAt")) activation_expiry = response_data.get("connectorConfig", {}).get("activationExpiry") if activation_expiry: response_data.get("connectorConfig", {})["activationExpiry"] = datetime_to_string(activation_expiry) headers = ["name", "id", "status", "arn", "createdAt", "version"] readable_output = tableToMarkdown( name="Describe Cluster Information", t=response_data, removeNull=True, headers=headers, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EKS.Cluster", outputs=response_data, raw_response=response_data, outputs_key_field="name", ) @staticmethod def associate_access_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Associates an access policy and its scope to an access entry. Args: client(boto3 client): The configured AWS session. args: command arguments Returns: A Command Results object """ cluster_name = args.get("cluster_name") principal_arn = args.get("principal_arn") policy_arn = args.get("policy_arn") type_arg = args.get("type") namespaces = argToList(args.get("namespaces")) if type_arg and type_arg == "namespace" and not namespaces: raise Exception(f"When the {type_arg=}, you must enter a namespace.") access_scope = {"type": type_arg, "namespaces": namespaces} print_debug_logs( client, f"Associating access policy with parameters: {cluster_name=}, {principal_arn=}, {policy_arn=}, {access_scope=}", ) response = client.associate_access_policy( clusterName=cluster_name, principalArn=principal_arn, policyArn=policy_arn, accessScope=access_scope ) response_data = response.get("associatedAccessPolicy", {}) response_data["clusterName"] = response.get("clusterName") response_data["principalArn"] = response.get("principalArn") response_data["associatedAt"] = datetime_to_string(response_data.get("associatedAt")) response_data["modifiedAt"] = datetime_to_string(response_data.get("modifiedAt")) headers = ["clusterName", "principalArn", "policyArn", "associatedAt"] readable_output = tableToMarkdown( name="The access policy was associated to the access entry successfully.", t=response_data, removeNull=True, headers=headers, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EKS.AssociatedAccessPolicy", outputs=response_data, raw_response=response_data, outputs_key_field="clusterName", ) @staticmethod def update_access_entry_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Updates an existing Access Entry for an Amazon EKS cluster. Args: client (BotoClient): The boto3 client for EKS service args (Dict[str, Any]): Command arguments including cluster_name, principal_arn, kubernetes_groups, client_request_token, and user_name Returns: CommandResults: Results containing the updated access entry details """ cluster_name = args.get("cluster_name", "") principal_arn = args.get("principal_arn", "") kwargs: Dict[str, Any] = { "clusterName": cluster_name, "principalArn": principal_arn, "kubernetesGroups": argToList(args.get("kubernetes_groups")), "clientRequestToken": args.get("client_request_token"), "username": args.get("user_name"), } remove_nulls_from_dictionary(kwargs) print_debug_logs( client, f"Updating EKS access entry for cluster: {cluster_name}, principal: {principal_arn}, kwargs keys: {kwargs.keys()}", ) response = client.update_access_entry(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) access_entry = serialize_response_with_datetime_encoding(response.get("accessEntry", {})) headers = ["clusterName", "principalArn", "username", "type", "createdAt"] readable_output = tableToMarkdown( name="The AWS EKS Access Entry was updated successfully", t=access_entry, headers=headers, removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EKS.AccessEntry", outputs_key_field=["clusterName", "principalArn"], outputs=access_entry, raw_response=access_entry, ) @staticmethod def create_access_entry_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a new Access Entry for an Amazon EKS cluster. Args: client (BotoClient): The boto3 client for EKS service args (Dict[str, Any]): Command arguments including cluster_name, principal_arn, kubernetes_groups, client_request_token, type, and tags Returns: CommandResults: Results containing the created access entry details """ cluster_name = args.get("cluster_name", "") principal_arn = args.get("principal_arn", "") kwargs: Dict[str, Any] = { "clusterName": cluster_name, "principalArn": principal_arn, "kubernetesGroups": argToList(args.get("kubernetes_groups")), "clientRequestToken": args.get("client_request_token"), "username": args.get("user_name"), "type": args.get("type"), "tags": {tag["Key"]: tag["Value"] for tag in parse_tag_field(args.get("tags"))} if args.get("tags") else None, } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Creating EKS access entry for cluster: {cluster_name}, principal: {principal_arn}, {kwargs=}") response = client.create_access_entry(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) access_entry = serialize_response_with_datetime_encoding(response.get("accessEntry", {})) headers = ["clusterName", "principalArn", "username", "type", "tags", "createdAt"] readable_output = tableToMarkdown( name="The AWS EKS Access Entry was created successfully", t=access_entry, headers=headers, removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.EKS.AccessEntry", outputs_key_field=["clusterName", "principalArn"], outputs=access_entry, raw_response=access_entry, ) @staticmethod def list_clusters_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns a list of EKS clusters owned by the authenticated sender of the request. Args: client (BotoClient): The boto3 client for EKS service args (Dict[str, Any]): Command arguments including account_id, region, limit, next_token, include Returns: CommandResults: Results containing the list of EKS cluster names """ kwargs = build_pagination_kwargs(args, max_limit=100, limit_name="maxResults", next_token_name="nextToken") if include := argToList(args.get("include")): kwargs["include"] = include print_debug_logs(client, f"Listing EKS clusters with parameters: {kwargs}") response = client.list_clusters(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) clusters = response.get("clusters", []) next_token = response.get("nextToken") if not clusters: return CommandResults(readable_output="There aren't any clusters.") clusters_data = [{"ClusterName": cluster} for cluster in clusters] readable_output = tableToMarkdown( name="AWS EKS Clusters", t=clusters_data, headers=["ClusterName"], removeNull=True, headerTransform=pascalToSpace, ) outputs: Dict[str, Any] = {"AWS.EKS.Clusters": clusters, "AWS.EKS(true)": {"ClustersNextToken": next_token}} return CommandResults( readable_output=readable_output, outputs=outputs, raw_response=response, ) class RDS: service = AWSServices.RDS @staticmethod def modify_db_cluster_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modifies an Amazon RDS DB Cluster configuration. Args: client (BotoClient): The boto3 client for RDS service args (Dict[str, Any]): Command arguments including cluster configuration options Returns: CommandResults: Results of the operation with update information """ try: kwargs = { "DBClusterIdentifier": args.get("db_cluster_identifier"), } # Optional parameters optional_params = { "DeletionProtection": "deletion_protection", "EnableIAMDatabaseAuthentication": "enable_iam_database_authentication", } for param, arg_name in optional_params.items(): if arg_name in args: kwargs[param] = argToBoolean(args[arg_name]) demisto.debug(f"executing modify_db_cluster with {kwargs}") response = client.modify_db_cluster(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: db_cluster = response.get("DBCluster", {}) readable_output = f"Successfully modified DB cluster {args.get('db_cluster_identifier')}" if db_cluster: db_cluster = convert_datetimes_to_iso_safe(db_cluster) headers = [ "DBClusterIdentifier", "Status", "Engine", "EngineVersion", "Endpoint", "Port", ] readable_output += "\n\nUpdated DB Cluster details:\n" readable_output += tableToMarkdown( "", t=db_cluster, headers=headers, removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.RDS.DBCluster", outputs=db_cluster, outputs_key_field="DBClusterIdentifier", ) else: raise DemistoException( f"Failed to modify DB cluster. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error modifying DB cluster: {str(e)}") @staticmethod def modify_db_cluster_snapshot_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modifies attributes of an Amazon RDS DB Cluster snapshot. Args: client (BotoClient): The boto3 client for RDS service args (Dict[str, Any]): Command arguments for snapshot attribute modification Returns: CommandResults: Results of the snapshot attribute modification operation """ try: kwargs = { "DBClusterSnapshotIdentifier": args.get("db_cluster_snapshot_identifier"), "AttributeName": args.get("attribute_name"), } # Optional parameters if "values_to_add" in args: kwargs["ValuesToAdd"] = argToList(args.get("values_to_add")) if "values_to_remove" in args: kwargs["ValuesToRemove"] = argToList(args.get("values_to_remove")) remove_nulls_from_dictionary(kwargs) response = client.modify_db_cluster_snapshot_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: attributes = response.get("DBClusterSnapshotAttributesResult", {}) if attributes: readable_output = ( f"Successfully modified DB cluster snapshot attribute for {args.get('db_cluster_snapshot_identifier')}" ) readable_output += "\n\nUpdated DB Cluster Snapshot Attributes:\n" readable_output += tableToMarkdown("", attributes) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.RDS.DBClusterSnapshotAttributes", outputs=attributes, outputs_key_field="DBClusterSnapshotIdentifier", ) else: raise DemistoException( f"Failed to modify DB cluster snapshot attribute. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error modifying DB cluster snapshot attribute: {str(e)}") @staticmethod def modify_db_instance_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modifies an Amazon RDS DB Instance configuration. Args: client (BotoClient): The boto3 client for RDS service args (Dict[str, Any]): Command arguments including instance identifier and configuration options Returns: CommandResults: Results of the operation with update information """ try: kwargs = { "DBInstanceIdentifier": args.get("db_instance_identifier"), "MultiAZ": arg_to_bool_or_none(args.get("multi_az")), "ApplyImmediately": arg_to_bool_or_none(args.get("apply_immediately")), "AutoMinorVersionUpgrade": arg_to_bool_or_none(args.get("auto_minor_version_upgrade")), "DeletionProtection": arg_to_bool_or_none(args.get("deletion_protection")), "EnableIAMDatabaseAuthentication": arg_to_bool_or_none(args.get("enable_iam_database_authentication")), "PubliclyAccessible": arg_to_bool_or_none(args.get("publicly_accessible")), "CopyTagsToSnapshot": arg_to_bool_or_none(args.get("copy_tags_to_snapshot")), "BackupRetentionPeriod": int(args.get("backup_retention_period", "")) if args.get("backup_retention_period") else None, "VpcSecurityGroupIds": argToList(args.get("vpc_security_group_ids")), } remove_nulls_from_dictionary(kwargs) demisto.info(f"modify_db_instance {kwargs=}") response = client.modify_db_instance(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: db_instance = response.get("DBInstance", {}) readable_output = ( f"Successfully modified DB instance {args.get('db_instance_identifier')}" f"\n\nUpdated DB Instance details:\n\n" ) if db_instance: db_instance = convert_datetimes_to_iso_safe(db_instance) readable_output += tableToMarkdown("", t=db_instance, removeNull=True) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.RDS.DBInstance", outputs=db_instance, outputs_key_field="DBInstanceIdentifier", ) else: raise DemistoException( f"Failed to modify DB instance. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"Error {response['Error']['Message']}", ) except Exception as e: raise DemistoException(f"Error modifying DB instance: {str(e)}") @staticmethod def modify_db_snapshot_attribute_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Adds or removes permission for the specified AWS account IDs to restore the specified DB snapshot. Args: client (BotoClient): The boto3 client for RDS service args (Dict[str, Any]): Command arguments including snapshot identifier and attribute settings Returns: CommandResults: Results of the operation with success/failure message """ kwargs = { "DBSnapshotIdentifier": args.get("db_snapshot_identifier"), "AttributeName": args.get("attribute_name"), "ValuesToAdd": argToList(args.get("values_to_add")) if "values_to_add" in args else None, "ValuesToRemove": argToList(args.get("values_to_remove")) if "values_to_remove" in args else None, } remove_nulls_from_dictionary(kwargs) response = client.modify_db_snapshot_attribute(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: # Return the changed fields in the command results: return CommandResults( readable_output=( f"Successfully modified DB snapshot attribute for {args.get('db_snapshot_identifier')}:\n" f"{tableToMarkdown('Modified', kwargs)}" ) ) else: raise DemistoException(f"Couldn't modify DB snapshot attribute for {args.get('db_snapshot_identifier')}") @staticmethod def modify_event_subscription_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Modifies the configuration of an existing Amazon RDS event notification subscription. This command performs the 'ModifyEventSubscription' API operation, allowing updates to the target SNS topic, the list of event categories, the source type, and the enabled state of the subscription. Args: client (BotoClient): The initialized Boto3 client. args (Dict[str, Any]): Command arguments, typically containing: - 'subscription_name' (str): The unique name of the subscription to modify. (Required) - 'enabled' (str): Boolean string ('true' or 'false') to activate/deactivate the subscription. (Optional) - 'event_categories' (str | List[str]): A list of event categories to subscribe to. (Optional) - 'sns_topic_arn' (str): The ARN of the new SNS topic to publish events to. (Optional) - 'source_type' (str): The type of resource generating events. (Optional) Returns: CommandResults: A CommandResults object containing the modified EventSubscription details. """ kwargs = { "SubscriptionName": args.get("subscription_name"), "Enabled": arg_to_bool_or_none(args.get("enabled")), "EventCategories": argToList(args.get("event_categories", [])), "SnsTopicArn": args.get("sns_topic_arn"), "SourceType": args.get("source_type"), } remove_nulls_from_dictionary(kwargs) try: demisto.debug(f"calling modify_event_subscription with {kwargs=}") response = client.modify_event_subscription(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: headers = [ "CustomerAwsId", "CustSubscriptionId", "SnsTopicArn", "Status", "SubscriptionCreationTime", "SourceType", "EventCategoriesList", "Enabled", "EventSubscriptionArn", "SourceIdsList", ] return CommandResults( readable_output=tableToMarkdown( name=f"Event subscription {args.get('subscription_name')} successfully modified.", headers=headers, t=response.get("EventSubscription"), removeNull=True, ), outputs_prefix="AWS.RDS.EventSubscription", outputs=response.get("EventSubscription"), outputs_key_field="CustSubscriptionId", ) raise DemistoException(f"Failed to modify event subscription {args.get('subscription_name')}.") except Exception as e: raise DemistoException(f"Error: {str(e)}") @staticmethod def describe_db_instances_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes provisioned RDS instances. Args: client (BotoClient): The boto3 client for RDS service args (Dict[str, Any]): Command arguments including DB instance identifier, filters, and pagination Returns: CommandResults: Results of the operation with DB instance information """ kwargs = { "DBInstanceIdentifier": args.get("db_instance_identifier"), "Filters": parse_filter_field(args.get("filters")), } if not args.get("db_instance_identifier"): pagination_kwargs = build_pagination_kwargs( args, minimum_limit=20, max_limit=100, limit_name="MaxRecords", next_token_name="Marker" ) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) demisto.debug(f"calling describe_db_instances_command with {kwargs=}") response = client.describe_db_instances(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) db_instances = response.get("DBInstances", []) if not db_instances: return CommandResults(readable_output="No DB instances found.") outputs = { "AWS.RDS.DBInstances(val.DBInstanceIdentifier && val.DBInstanceIdentifier == obj.DBInstanceIdentifier)": db_instances, "AWS.RDS(true)": {"DBInstancesNextToken": response.get("Marker")}, } readable_output = tableToMarkdown( "AWS RDS DB Instances", db_instances, headers=[ "DBInstanceIdentifier", "DBInstanceClass", "Engine", "DBInstanceStatus", ], removeNull=True, headerTransform=pascalToSpace, ) return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) class Redshift: service = AWSServices.Redshift @staticmethod def modify_cluster_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the settings of a cluster. Args: client (BotoClient): The boto3 client for Redshift service args (Dict[str, Any]): Command arguments including cluster identifier and modification parameters Returns: CommandResults: Results of the operation with cluster modification details """ kwargs = { "ClusterIdentifier": args.get("cluster_identifier"), "ClusterType": args.get("cluster_type"), "NodeType": args.get("node_type"), "NumberOfNodes": arg_to_number(args.get("number_of_nodes")), "ClusterSecurityGroups": argToList(args.get("cluster_security_groups")), "VpcSecurityGroupIds": argToList(args.get("vpc_security_group_ids")), "ClusterParameterGroupName": args.get("cluster_parameter_group_name"), "AutomatedSnapshotRetentionPeriod": arg_to_number(args.get("automated_snapshot_retention_period")), "ManualSnapshotRetentionPeriod": arg_to_number(args.get("manual_snapshot_retention_period")), "PreferredMaintenanceWindow": args.get("preferred_maintenance_window"), "ClusterVersion": args.get("cluster_version"), "AllowVersionUpgrade": arg_to_bool_or_none(args.get("allow_version_upgrade")), "HsmClientCertificateIdentifier": args.get("hsm_client_certificate_identifier"), "HsmConfigurationIdentifier": args.get("hsm_configuration_identifier"), "NewClusterIdentifier": args.get("new_cluster_identifier"), "PubliclyAccessible": arg_to_bool_or_none(args.get("publicly_accessible")), "ElasticIp": args.get("elastic_ip"), "EnhancedVpcRouting": arg_to_bool_or_none(args.get("enhanced_vpc_routing")), "MaintenanceTrackName": args.get("maintenance_track_name"), "Encrypted": arg_to_bool_or_none(args.get("encrypted")), "KmsKeyId": args.get("kms_key_id"), "AvailabilityZoneRelocation": arg_to_bool_or_none(args.get("availability_zone_relocation")), "AvailabilityZone": args.get("availability_zone"), "Port": arg_to_number(args.get("port")), "IpAddressType": args.get("ip_address_type"), "MultiAZ": arg_to_bool_or_none(args.get("multi_az")), "ExtraComputeForAutomaticOptimization": arg_to_bool_or_none(args.get("extra_compute_for_automatic_optimization")), } remove_nulls_from_dictionary(kwargs) response = client.modify_cluster(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) cluster_data = response.get("Cluster", {}) if cluster_data.get("PendingModifiedValues", {}).get("MasterUserPassword"): del cluster_data["PendingModifiedValues"]["MasterUserPassword"] readable_output = tableToMarkdown( f"Successfully modified Redshift cluster: {cluster_data.get('ClusterIdentifier')}", cluster_data, headers=["ClusterIdentifier", "NodeType", "ClusterStatus", "PubliclyAccessible", "Encrypted", "NumberOfNodes"], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.Redshift.Clusters", outputs_key_field="ClusterIdentifier", outputs=cluster_data, readable_output=readable_output, raw_response=response, ) class CostExplorer: service = AWSServices.CostExplorer @staticmethod def billing_cost_usage_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves cost and usage data from AWS Cost Explorer API. This command provides detailed cost and usage information for AWS services over a specified time period. It supports multiple metrics including costs (blended, unblended, amortized) and usage quantities. Results can be filtered by AWS services and support pagination for large datasets. Args: client (BotoClient): AWS Cost Explorer boto3 client args (Dict[str, Any]): Command arguments containing: - metrics: List of metrics to retrieve (UsageQuantity, BlendedCost, etc.) - start_date: Start date for the report (YYYY-MM-DD format) - end_date: End date for the report (YYYY-MM-DD format) - granularity: Time granularity (Daily, Monthly, Hourly) - aws_services: Optional filter for specific AWS services - next_page_token: Token for pagination Returns: CommandResults: Contains usage data grouped by time periods and metrics, with separate tables for each metric type in readable output Raises: DemistoException: If AWS API call fails or invalid parameters provided """ today_utc = datetime.now(UTC) metrics = argToList(args.get("metrics", "UsageQuantity")) allowed_metrics = { "AmortizedCost", "BlendedCost", "NetAmortizedCost", "NetUnblendedCost", "NormalizedUsageAmount", "UnblendedCost", "UsageQuantity", } invalid_metrics = [m for m in metrics if m not in allowed_metrics] if invalid_metrics: raise DemistoException( f"Invalid metrics: {', '.join(invalid_metrics)}. Allowed metrics: {', '.join(sorted(allowed_metrics))}" ) start_date = arg_to_datetime(args.get("start_date")) or (today_utc - timedelta(days=7)) end_date = arg_to_datetime(args.get("end_date")) or today_utc granularity = args.get("granularity", "Daily").upper() aws_services = argToList(args.get("aws_services")) token = args.get("next_page_token") request = { "TimePeriod": { "Start": start_date.date().isoformat(), "End": end_date.date().isoformat(), }, "Granularity": granularity, "Metrics": metrics, } if aws_services: request["Filter"] = { "Dimensions": { "Key": "SERVICE", "MatchOptions": ["EQUALS"], "Values": aws_services, } } if token: request["NextPageToken"] = token demisto.debug(f"AWS get_cost_and_usage request: {request}") response = client.get_cost_and_usage(**request) results = response.get("ResultsByTime", []) next_token = response.get("NextPageToken", "") demisto.debug(f"AWS get_cost_and_usage response - ResultsByTime count: {len(results)},\n NextToken: {next_token}") results_by_metric: dict[str, list] = {metric: [] for metric in metrics} for result in results: total = result.get("Total") service = total.get("Keys", [None])[0] if total.get("Keys") else None for metric in metrics: results_by_metric[metric].append( { "Service": service, "StartDate": result.get("TimePeriod", {}).get("Start"), "EndDate": result.get("TimePeriod", {}).get("End"), "Amount": total.get(metric, {}).get("Amount", ""), "Unit": total.get(metric, {}).get("Unit", ""), } ) outputs = {"AWS.Billing.Usage": results, "AWS.Billing(true)": {"UsageNextToken": next_token}} readable_tables = [] for metric in metrics: metric_results = results_by_metric[metric] if metric_results: table = tableToMarkdown( f"AWS Billing Usage - {metric}", metric_results, headers=["Service", "StartDate", "EndDate", "Amount", "Unit"], removeNull=True, headerTransform=pascalToSpace, ) readable_tables.append(table) readable = "\n".join(readable_tables) if readable_tables else "No billing usage data found." if next_token: readable = f"Next Page Token: {next_token}\n\n" + readable return CommandResults( readable_output=readable, outputs=outputs, raw_response=response, ) @staticmethod def billing_forecast_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves cost forecast data from AWS Cost Explorer API. This command provides forecasted cost information for AWS services over a future time period. It uses historical data to predict future spending patterns and supports multiple cost metrics. Results can be filtered by AWS services and include prediction intervals for accuracy assessment. Args: client (BotoClient): AWS Cost Explorer boto3 client args (Dict[str, Any]): Command arguments containing: - metric: Single forecast metric (AMORTIZED_COST, BLENDED_COST, etc.) - start_date: Start date for the forecast (YYYY-MM-DD format, defaults to today) - end_date: End date for the forecast (YYYY-MM-DD format, defaults to +7 days) - granularity: Time granularity (Daily, Monthly, Hourly) - aws_services: Optional filter for specific AWS services - next_page_token: Token for pagination Returns: CommandResults: Contains forecast data with mean values and prediction intervals, organized by time periods for the specified metric Raises: DemistoException: If AWS API call fails or invalid parameters provided """ today_utc = datetime.now(UTC) metric = args.get("metric", "AMORTIZED_COST") start_date = arg_to_datetime(args.get("start_date")) or today_utc end_date = arg_to_datetime(args.get("end_date")) or (today_utc + timedelta(days=7)) granularity = args.get("granularity", "Daily").upper() aws_services = argToList(args.get("aws_services")) token = args.get("next_page_token") request = { "TimePeriod": { "Start": start_date.date().isoformat(), "End": end_date.date().isoformat(), }, "Granularity": granularity, "Metric": metric, } if aws_services: request["Filter"] = { "Dimensions": { "Key": "SERVICE", "MatchOptions": ["EQUALS"], "Values": aws_services, } } if token: request["NextPageToken"] = token demisto.debug(f"AWS Cost Forecast request: {request}") response = client.get_cost_forecast(**request) results = response.get("ForecastResultsByTime", []) next_token = response.get("NextPageToken", "") demisto.debug(f"AWS Cost Forecast response - ForecastResultsByTime count: {len(results)},\nNextToken: {next_token}") metric_results = [] for result in results: metric_results.append( { "StartDate": result.get("TimePeriod", {}).get("Start"), "EndDate": result.get("TimePeriod", {}).get("End"), "TotalAmount": f"{float(result.get('MeanValue', 0)):.2f}", "TotalUnit": response.get("Unit"), } ) outputs = { "AWS.Billing.Forecast": results, "AWS.Billing(true)": {"ForecastNextToken": next_token}, } readable = tableToMarkdown( f"AWS Billing Forecast - {metric}", metric_results, headers=["StartDate", "EndDate", "TotalAmount", "TotalUnit"], removeNull=True, headerTransform=pascalToSpace, ) if next_token: readable = f"Next Page Token: {next_token}\n\n" + readable return CommandResults( readable_output=readable, outputs=outputs, raw_response=response, ) class Budgets: service = AWSServices.BUDGETS @staticmethod def billing_budgets_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves budget information from AWS Budgets API. This command lists all configured budgets for a specified AWS account, providing detailed information about budget limits, actual spending, forecasted spending, and time periods. Supports various budget types including cost, usage, and savings plans budgets. Args: client (BotoClient): AWS Budgets boto3 client args (Dict[str, Any]): Command arguments containing: - account_id: AWS account ID to retrieve budgets for (required) - max_result: Maximum number of results to return (default: 50, max: 1000) - show_filter_expression: Whether to include filter expressions in output - next_page_token: Token for pagination Returns: CommandResults: Contains budget data including names, types, limits, actual spend, forecasted spend, and time periods with pagination support Raises: DemistoException: If AWS API call fails, account_id is invalid, or other errors occur """ max_results = int(args.get("max_result", 50)) token = args.get("next_page_token") account_id = args.get("account_id") show_filter_expression = argToBoolean(args.get("show_filter_expression")) request = {"AccountId": account_id, "MaxResults": max_results} if token: request["NextToken"] = token if show_filter_expression: request["ShowFilterExpression"] = show_filter_expression demisto.debug(f"AWS Budgets request: {request}") response = client.describe_budgets(**request) budgets = response.get("Budgets", []) next_token = response.get("NextToken") demisto.debug(f"AWS Budgets response - Budgets count: {len(budgets)},\n NextToken: {next_token}") results = [] for b in budgets: budget_limit = b.get("BudgetLimit", {}) actual_spend = b.get("CalculatedSpend", {}).get("ActualSpend", {}) start = b.get("TimePeriod", {}).get("Start").strftime("%Y-%m-%d") end = b.get("TimePeriod", {}).get("End").strftime("%Y-%m-%d") results.append( { "BudgetName": b.get("BudgetName"), "BudgetType": b.get("BudgetType"), "BudgetLimitAmount": budget_limit.get("Amount"), "BudgetLimitUnit": budget_limit.get("Unit"), "ActualSpendAmount": actual_spend.get("Amount"), "ActualSpendUnit": actual_spend.get("Unit"), "TimePeriod": f"{start} - {end}", "FilterExpression": b.get("FilterExpression") if show_filter_expression else None, } ) outputs = { "AWS.Billing.Budget(val.BudgetName && val.BudgetName == obj.BudgetName)": results, "AWS.Billing(true)": {"BudgetNextToken": next_token}, } readable = tableToMarkdown( "AWS Budgets", results, headers=[ "BudgetName", "TimePeriod", "BudgetType", "BudgetLimitAmount", "BudgetLimitUnit", "FilterExpression", "ActualSpendAmount", "ActualSpendUnit", ], removeNull=True, headerTransform=pascalToSpace, ) if next_token: readable = f"Next Page Token: {next_token}\n\n" + readable return CommandResults( readable_output=readable, outputs=outputs, raw_response=json.loads(json.dumps(response, cls=DatetimeEncoder)), ) @staticmethod def billing_budget_notification_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves notification configurations for a specific budget from AWS Budgets API. This command lists all notification settings associated with a particular budget, including notification types (actual vs forecasted), thresholds, comparison operators, and subscriber information (email addresses or SNS topics). Args: client (BotoClient): AWS Budgets boto3 client args (Dict[str, Any]): Command arguments containing: - account_id: AWS account ID that owns the budget (required) - budget_name: Name of the budget to retrieve notifications for (required) - max_result: Maximum number of results to return (default: 50, max: 100) - next_page_token: Token for pagination Returns: CommandResults: Contains notification configurations including notification types, thresholds, comparison operators, and subscriber details Raises: DemistoException: If AWS API call fails, budget doesn't exist, or invalid parameters provided """ budget_name = args.get("budget_name") max_results = int(args.get("max_result", 50)) token = args.get("next_page_token") request = {"AccountId": args["account_id"], "BudgetName": budget_name, "MaxResults": max_results} if token: request["NextToken"] = token demisto.debug(f"AWS Budget Notifications request: {request}") response = client.describe_notifications_for_budget(**request) notifications = response.get("Notifications", []) next_token = response.get("NextToken", "") demisto.debug(f"AWS Budget Notifications response - Notifications count: {len(notifications)},\n NextToken: {next_token}") outputs = { "AWS.Billing.Notification": notifications, "AWS.Billing(true)": {"NotificationNextToken": next_token}, } readable = tableToMarkdown(f"Notifications for Budget: {budget_name}", notifications) if next_token: readable = f"Next Page Token: {next_token}\n\n" + readable return CommandResults( readable_output=readable, outputs=outputs, raw_response=response, ) class CloudTrail: service = AWSServices.CloudTrail @staticmethod def start_logging_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Starts the recording of AWS API calls and log file delivery for a trail. """ name = args.get("name") try: response = client.start_logging(Name=name) return CommandResults(readable_output=f"Successfully started logging for CloudTrail: {name}", raw_response=response) except Exception as e: raise DemistoException(f"Error starting logging for CloudTrail {name}: {str(e)}") @staticmethod def update_trail_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Updates trail settings that control what events you are logging, and how to handle log files. Changes to a trail do not require stopping the CloudTrail service. Args: client (BotoClient): The boto3 client for CloudTrail service args (Dict[str, Any]): Command arguments including trail name and configuration options Returns: CommandResults: Results of the operation with update information """ try: kwargs = { "Name": args.get("name"), "S3BucketName": args.get("s3_bucket_name"), "S3KeyPrefix": args.get("s3_key_prefix"), "SnsTopicName": args.get("sns_topic_name"), "IncludeGlobalServiceEvents": arg_to_bool_or_none(args.get("include_global_service_events")), "IsMultiRegionTrail": arg_to_bool_or_none(args.get("is_multi_region_trail")), "EnableLogFileValidation": arg_to_bool_or_none(args.get("enable_log_file_validation")), "CloudWatchLogsLogGroupArn": args.get("cloud_watch_logs_log_group_arn"), "CloudWatchLogsRoleArn": args.get("cloud_watch_logs_role_arn"), "KMSKeyId": args.get("kms_key_id"), } remove_nulls_from_dictionary(kwargs) response = client.update_trail(**kwargs) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: trail_data = response.get("Trail", {}) readable_output = f"Successfully updated CloudTrail: {args.get('name')}" if trail_data: readable_output += "\n\nUpdated Trail Details:" readable_output += tableToMarkdown("", trail_data) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.CloudTrail.Trail", outputs=trail_data, outputs_key_field="TrailARN", raw_response=response, ) else: raise DemistoException( f"Failed to update CloudTrail. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) except Exception as e: raise DemistoException(f"Error updating CloudTrail {args.get('name')}: {str(e)}") @staticmethod def describe_trails_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves descriptions of the specified trail or all trails in the account. Args: client (BotoClient): The boto3 client for CloudTrail service args (Dict[str, Any]): Command arguments including trail names (optional) Returns: CommandResults: Detailed information about CloudTrail trails """ trail_names = argToList(args.get("trail_names", [])) include_shadow_trails = arg_to_bool_or_none(args.get("include_shadow_trails", True)) kwargs = {"trailNameList": trail_names, "includeShadowTrails": include_shadow_trails} remove_nulls_from_dictionary(kwargs) response = client.describe_trails(**kwargs) trail_data = response.get("trailList", []) headers = [ "Name", "S3BucketName", "IncludeGlobalServiceEvents", "IsMultiRegionTrail", "TrailARN", "LogFileValidationEnabled", "HomeRegion", ] readable_output = tableToMarkdown( name="Trail List", t=trail_data, removeNull=True, headers=headers, headerTransform=pascalToSpace, ) return CommandResults( outputs_prefix="AWS.CloudTrail.Trails", outputs_key_field="TrailARN", raw_response=response, outputs=trail_data, readable_output=readable_output, ) class ECS: service = AWSServices.ECS @staticmethod def update_cluster_settings_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Updates the containerInsights setting of an ECS cluster. Args: client (BotoClient): The boto3 client for ECS service args (Dict[str, Any]): Command arguments including cluster name and setting value Returns: CommandResults: Results of the operation with updated cluster settings """ setting_value = args.get("value") print_debug_logs(client, f"Updating ECS cluster settings with parameters: {setting_value=}") # noqa: E501 response = client.update_cluster_settings( cluster=args.get("cluster_name"), settings=[ {"name": "containerInsights", "value": setting_value}, ], ) if response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK: cluster_data = response.get("cluster", {}) readable_output = f"Successfully updated ECS cluster: {args.get('cluster_name')}" if cluster_data: readable_output += "\n\nUpdated Cluster Details:ֿֿֿֿֿ\n" readable_output += tableToMarkdown("", cluster_data) return CommandResults( readable_output=readable_output, outputs_prefix="AWS.ECS.Cluster", outputs=cluster_data, outputs_key_field="clusterArn", raw_response=response, ) else: raise DemistoException( f"Failed to update ECS cluster. " f"Status code: {response['ResponseMetadata']['HTTPStatusCode']}. " f"{json.dumps(response)}" ) class KMS: service = AWSServices.KMS @staticmethod def enable_key_rotation_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Enables automatic rotation for a symmetric customer-managed KMS key. Uses a custom rotation period (days) from args; valid range is 90–2560. Args: client (BotoClient): The boto3 client for KMS service. args (Dict[str, Any]): Command arguments including key id and rotation period in days. Returns: CommandResults: Results of the operation with updated key rotation settings. """ key_id = args.get("key_id", "") rot_period = arg_to_number(args.get("rotation_period_in_days")) kwargs = {"KeyId": key_id, "RotationPeriodInDays": rot_period} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"EnableKeyRotation params: {kwargs}") try: resp = client.enable_key_rotation(**kwargs) status = resp.get("ResponseMetadata", {}).get("HTTPStatusCode") if status in (HTTPStatus.OK, HTTPStatus.NO_CONTENT): hr = f"Enabled automatic rotation for KMS key '{key_id}' (rotation period: {rot_period} days)." return CommandResults(readable_output=hr, raw_response=resp) return AWSErrorHandler.handle_response_error(resp) except ClientError as e: return AWSErrorHandler.handle_client_error(e) except Exception as e: raise DemistoException(f"Error enabling key rotation for '{key_id}': {str(e)}") class ELB: service = AWSServices.ELB @staticmethod def modify_load_balancer_attributes_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies Classic ELB attributes: Cross-Zone Load Balancing, Access Logs, Connection Draining, Connection Settings, AdditionalAttributes. Sends only sub-blocks provided by the user. Args: client (BotoClient): The boto3 client for ELB service. args (Dict[str, Any]): Command arguments including load balancer name and setting values: - cross-zone load balancing (enabled). - access logs (enabled, s3 bucket name, s3 bucket prefix, emit interval). - connection draining (enabled, timeout). - connection settings (idle timeout). - desync mitigation mode (monitor, defensive, strictest). Returns: CommandResults: Results of the operation with updated load balancer attributes. """ lb_name = args.get("load_balancer_name", "") attrs: Dict[str, Any] = {} # Cross-zone ELB.add_block_if_any( block_name="CrossZoneLoadBalancing", block={"Enabled": arg_to_bool_or_none(args.get("cross_zone_load_balancing_enabled"))}, target=attrs, ) # Access logs ELB.add_block_if_any( block_name="AccessLog", block={ "Enabled": arg_to_bool_or_none(args.get("access_log_enabled")), "S3BucketName": args.get("access_log_s3_bucket_name"), "S3BucketPrefix": args.get("access_log_s3_bucket_prefix"), "EmitInterval": arg_to_number(args.get("access_log_interval")), }, target=attrs, ) # Connection draining ELB.add_block_if_any( block_name="ConnectionDraining", block={ "Enabled": arg_to_bool_or_none(args.get("connection_draining_enabled")), "Timeout": arg_to_number(args.get("connection_draining_timeout")), }, target=attrs, ) # Connection settings (idle timeout) ELB.add_block_if_any( block_name="ConnectionSettings", block={"IdleTimeout": arg_to_number(args.get("connection_settings_idle_timeout"))}, target=attrs, ) # Additional attributes (JSON list of {Key,Value}) # Only one additional attribute is supported on classic ELB, Therefore we directly set the key and value if desync_mitigation_mode := args.get("desync_mitigation_mode"): attrs["AdditionalAttributes"] = [{"Key": "elb.http.desyncmitigationmode", "Value": desync_mitigation_mode}] kwargs = {"LoadBalancerName": lb_name, "LoadBalancerAttributes": attrs} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"ModifyLoadBalancerAttributes params: {kwargs}") try: resp = client.modify_load_balancer_attributes(**kwargs) status = resp.get("ResponseMetadata", {}).get("HTTPStatusCode") if status == HTTPStatus.OK: lb_attrs = resp.get("LoadBalancerAttributes", {}) out = {"LoadBalancerName": lb_name, "LoadBalancerAttributes": lb_attrs} hr = ELB.format_elb_modify_attributes_hr(lb_name, resp) return CommandResults( readable_output=hr, outputs_prefix="AWS.ELB.LoadBalancer", outputs_key_field="LoadBalancerName", outputs=out, raw_response=resp, ) return AWSErrorHandler.handle_response_error(resp) except ClientError as e: return AWSErrorHandler.handle_client_error(e) except Exception as e: raise DemistoException(f"Error modifying load balancer '{lb_name}': {str(e)}") @staticmethod def add_block_if_any(block_name: str, block: dict, target: dict) -> None: """ Adds a block to the target dictionary if the value is not empty. Args: block_name (str): The name of the block to add. block (dict): The block to add. target (dict): The target dictionary to add the block to. Returns: None """ remove_nulls_from_dictionary(block) if block: target[block_name] = block @staticmethod def format_elb_modify_attributes_hr(lb_name: str, resp: dict) -> str: """ Minimal formatter: - prints "Updated attributes for " - then one table per attribute block under LoadBalancerAttributes Args: lb_name (str): The name of the Classic ELB. resp (dict): The response from the modify_load_balancer_attributes API call. Returns: str: The formatted output. """ lb_attrs = resp.get("LoadBalancerAttributes", {}) sections: list[str] = [f"### Updated attributes for Classic ELB {lb_name}"] for attr_name, attr_values in lb_attrs.items(): title = attr_name if isinstance(attr_values, dict): sections.append(tableToMarkdown(title, [attr_values], removeNull=True)) elif attr_values and isinstance(attr_values, list): for attr_value in attr_values: sections.append(tableToMarkdown(title, attr_value, removeNull=True)) else: sections.append(tableToMarkdown(title, [{"Value": attr_values}], removeNull=True)) return "\n\n".join(sections) class Lambda: service = AWSServices.LAMBDA @staticmethod def get_function_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves the configuration information for a Lambda function. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including function name and optional qualifier Returns: CommandResults: Results of the operation with function configuration details """ # Prepare parameters function_name = args.get("function_name") params = {"FunctionName": function_name} if qualifier := args.get("qualifier"): params["Qualifier"] = qualifier # Get function configuration response = client.get_function_configuration(**params) # Remove ResponseMetadata for cleaner output if "ResponseMetadata" in response: del response["ResponseMetadata"] # Create human readable output human_readable = tableToMarkdown( f"Lambda Function Configuration: {function_name}", response, headers=[ "FunctionName", "FunctionArn", "Runtime", "CodeSha256", "State", "Description", "RevisionId", "LastModified", ], headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs_prefix="AWS.Lambda.FunctionConfig", outputs_key_field="FunctionArn", outputs=response, readable_output=human_readable, raw_response=response, ) @staticmethod def get_function_url_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Retrieves the configuration for a Lambda function URL. Args: client (BotoClient): The AWS Lambda client used to retrieve the function URL configuration. args (Dict[str, Any]): A dictionary containing the function URL configuration parameters including function_name and optional qualifier. Returns: CommandResults: An object containing the function URL configuration details including FunctionUrl, FunctionArn, AuthType, CreationTime, LastModifiedTime, and InvokeMode. """ function_name = args.get("function_name") qualifier = args.get("qualifier") # Prepare parameters params = {"FunctionName": function_name} if qualifier: params["Qualifier"] = qualifier # Get function URL configuration response = client.get_function_url_config(**params) # Remove ResponseMetadata for cleaner output if "ResponseMetadata" in response: del response["ResponseMetadata"] # Create human readable output human_readable = tableToMarkdown( f"Lambda Function URL Configuration: {function_name}", response, headers=["FunctionUrl", "FunctionArn", "AuthType", "CreationTime", "LastModifiedTime", "InvokeMode"], headerTransform=pascalToSpace, ) return CommandResults( outputs_prefix="AWS.Lambda.FunctionURLConfig", outputs_key_field="FunctionArn", outputs=response, readable_output=human_readable, raw_response=response, ) @staticmethod def update_function_url_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults: """ Updates the configuration for a Lambda function URL. Args: client (BotoClient): The AWS Lambda client used to update the function URL configuration. args (Dict[str, Any]): A dictionary containing the function URL configuration parameters including function_name, qualifier, auth_type, CORS settings (allow_credentials, allow_headers, allow_methods, allow_origins, expose_headers, max_age), and invoke_mode. Returns: CommandResults: An object containing a success message and raw response for updating the function URL configuration. """ params = { "FunctionName": args.get("function_name"), "Qualifier": args.get("qualifier"), "AuthType": args.get("auth_type"), "InvokeMode": args.get("invoke_mode"), } cors = { "AllowCredentials": arg_to_bool_or_none(args.get("cors_allow_credentials")), "AllowHeaders": argToList(args.get("cors_allow_headers", [])), "AllowMethods": argToList(args.get("cors_allow_methods", [])), "AllowOrigins": argToList(args.get("cors_allow_origins", [])), "ExposeHeaders": argToList(args.get("cors_expose_headers", [])), "MaxAge": arg_to_number(args.get("cors_max_age")), } fixed_cors = remove_empty_elements(cors) if any(fixed_cors.values()): params.update({"Cors": fixed_cors}) fixed_params = remove_empty_elements(params) response = client.update_function_url_config(**fixed_params) # Create human readable output human_readable = tableToMarkdown( f"The Updated Lambda Function URL Configuration: {response.get('FunctionArn',args.get('function_name'))}", response, headers=["FunctionUrl", "FunctionArn", "AuthType", "CreationTime", "LastModifiedTime", "InvokeMode"], headerTransform=pascalToSpace, ) return CommandResults( outputs_prefix="AWS.Lambda.FunctionURLConfig", outputs_key_field="FunctionArn", outputs=response, readable_output=human_readable, raw_response=response, ) @staticmethod def _parse_policy_response(data: dict[str, Any]) -> tuple[dict, list | None]: """ Parses the response data representing a policy into a structured format. Args: data (dict): The response data containing the policy information. Returns: tuple[dict[str, Any], list[dict[str, str | None]]]: A tuple containing the parsed policy information. The first element of the tuple is a dictionary representing the policy metadata with the following keys: - "Id" (str): The ID of the policy. - "Version" (str): The version of the policy. - "RevisionId" (str): The revision ID of the policy. The second element of the tuple is a list of dictionaries representing the policy statements. Each dictionary in the list represents a statement with the following keys: - "Sid" (str): The ID of the statement. - "Effect" (str): The effect of the statement (e.g., "Allow" or "Deny"). - "Action" (str): The action associated with the statement. - "Resource" (str): The resource associated with the statement. - "Principal" (str | None): The principal associated with the statement, if applicable. """ policy: dict[str, Any] = data.get("Policy", {}) statements: list[dict[str, str | None]] = policy.get("Statement", []) if len(statements) == 1: return { "Sid": statements[0].get("Sid"), "Effect": statements[0].get("Effect"), "Action": statements[0].get("Action"), "Resource": statements[0].get("Resource"), "Principal": statements[0].get("Principal"), }, None else: policy_table = { "Id": policy.get("Id"), "Version": policy.get("Version"), "RevisionId": data.get("RevisionId"), } statements_table = [ { "Sid": statement.get("Sid"), "Effect": statement.get("Effect"), "Action": statement.get("Action"), "Resource": statement.get("Resource"), "Principal": statement.get("Principal"), } for statement in statements ] return policy_table, statements_table @staticmethod def get_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the policy for a Lambda function from AWS and parses it into a dictionary. Args: args (dict): A dictionary containing the function name and optional qualifier. aws_client: The AWS client(boto3 client) used to retrieve the policy. Returns: CommandResults: An object containing the parsed policy as outputs, a readable output in Markdown format, and relevant metadata. """ kwargs = {"FunctionName": args["function_name"]} if qualifier := args.get("qualifier"): kwargs["Qualifier"] = qualifier response = client.get_policy(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) fixed_response = {} fixed_response["AccountId"] = args.get("account_id", "") fixed_response["FunctionName"] = args["function_name"] fixed_response["Region"] = args["region"] response["Policy"] = json.loads(response["Policy"]) fixed_response.update(response["Policy"]) fixed_response.update({"RevisionId": response.get("RevisionId")}) parsed_policy, parsed_statement = Lambda._parse_policy_response(response) policy_table = tableToMarkdown(name="Policy Statements", t=parsed_policy) if parsed_statement: # if policy contains a multiple statements, then print the statements in another table statements_table = tableToMarkdown("Statements", t=parsed_statement) policy_table = policy_table + statements_table return CommandResults( outputs=fixed_response, readable_output=policy_table, outputs_prefix="AWS.Lambda.Policy", outputs_key_field=["Region", "FunctionName", "AccountId"], raw_response=response, ) @staticmethod def invoke_command(client: BotoClient, args: Dict[str, Any]): """ Invokes a Lambda function with the specified parameters and returns the response. Args: client (BotoClient): The AWS Lambda client used to invoke the function. args (Dict[str, Any]): A dictionary containing the function invocation parameters including functionName, invocationType, logType, clientContext, payload, and qualifier. Returns: CommandResults: An object containing the invocation response data including function name, region, request payload, log results, response payload, executed version, and any function errors, formatted as readable output. """ payload = args.get("payload") kwargs: dict[str, Any] = { "FunctionName": args.get("function_name"), "InvocationType": args.get("invocation_type"), "LogType": args.get("log_type"), "ClientContext": args.get("client_context"), "Payload": json.dumps(payload) if (not isinstance(payload, str)) or (not payload.startswith("{") and not payload.startswith("[")) else payload, "Qualifier": args.get("qualifier"), } fixed_kwargs = remove_empty_elements(kwargs) response = client.invoke(**fixed_kwargs) data = { "FunctionName": args.get("function_name"), "Region": args.get("region"), "RequestPayload": args.get("payload"), } remove_nulls_from_dictionary(data) if "LogResult" in response: data.update({"LogResult": base64.b64decode(response["LogResult"]).decode("utf-8")}) # type:ignore if "Payload" in response: data.update({"Payload": response["Payload"].read().decode("utf-8")}) # type:ignore response["Payload"] = data["Payload"] if "ExecutedVersion" in response: data.update({"ExecutedVersion": response["ExecutedVersion"]}) # type:ignore if "FunctionError" in response: data.update({"FunctionError": response["FunctionError"]}) human_readable = tableToMarkdown("AWS Lambda Invoked Functions", data) return CommandResults( outputs=data, readable_output=human_readable, outputs_prefix="AWS.Lambda.InvokedFunction", outputs_key_field=["FunctionName", "Region"], raw_response=response, ) @staticmethod def get_function_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves information about a Lambda function including configuration, code location, and metadata. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - function_name (str): The name of the Lambda function - qualifier (str, optional): Version or alias to retrieve - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing function configuration, code location, tags, and concurrency settings """ # Build API parameters kwargs = {"FunctionName": args.get("function_name")} if qualifier := args.get("qualifier"): kwargs["Qualifier"] = qualifier print_debug_logs(client, f"Getting Lambda function with parameters: {kwargs}") response = client.get_function(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding response = serialize_response_with_datetime_encoding(response) # Add region to response response["Region"] = args.get("region") # Extract configuration for readable output func_config = response.get("Configuration", {}) func_config["Location"] = response.get("Code", {}).get("Location") # type: ignore func_config["Region"] = args.get("region") response["FunctionArn"] = func_config["FunctionArn"] outputs = copy.deepcopy(response) outputs.pop("ResponseMetadata", None) human_readable = tableToMarkdown( "AWS Lambda Function", func_config, headerTransform=pascalToSpace, removeNull=True, headers=["FunctionName", "FunctionArn", "Runtime", "Region", "Location"], ) return CommandResults( outputs_prefix="AWS.Lambda.Functions", outputs_key_field="FunctionArn", outputs=outputs, readable_output=human_readable, raw_response=response, ) @staticmethod def list_functions_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists Lambda functions in the specified region. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - region (str): AWS region - account_id (str): AWS account ID - limit (int, optional): Maximum number of functions to return - next_token (str, optional): Token for pagination Returns: CommandResults: Results containing list of Lambda functions with their configurations """ # Build pagination parameters using build_pagination_kwargs pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=50, next_token_name="Marker", limit_name="MaxItems" ) print_debug_logs(client, f"Listing Lambda functions with pagination parameters: {pagination_kwargs}") response = client.list_functions(**pagination_kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding serialized_response = serialize_response_with_datetime_encoding(response) functions_list = serialized_response.get("Functions", []) next_marker = serialized_response.get("NextMarker") if not functions_list: return CommandResults(readable_output="No Lambda functions found.") # Add region to each function for func in functions_list: func["Region"] = args.get("region") human_readable = tableToMarkdown( "AWS Lambda Functions", functions_list, headerTransform=pascalToSpace, removeNull=True, headers=["FunctionName", "FunctionArn", "Runtime", "LastModified", "Region"], ) # Prepare outputs with pagination support outputs = { "AWS.Lambda.Functions(val.FunctionArn && val.FunctionArn == obj.FunctionArn)": functions_list, "AWS.Lambda(true)": {"FunctionsNextToken": next_marker}, } return CommandResults( outputs=outputs, readable_output=human_readable, raw_response=serialized_response, ) @staticmethod def list_aliases_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists aliases for a Lambda function. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - function_name (str): The name of the Lambda function - function_version (str, optional): Function version to filter aliases - limit (int, optional): Maximum number of aliases to return - next_token (str, optional): Token for pagination - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing list of aliases for the function """ kwargs = {"FunctionName": args.get("function_name")} if function_version := args.get("function_version"): kwargs["FunctionVersion"] = function_version # Build pagination parameters using build_pagination_kwargs pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=10000, next_token_name="Marker", limit_name="MaxItems" ) kwargs.update(pagination_kwargs) print_debug_logs(client, f"Listing Lambda aliases with parameters: {kwargs}") response = client.list_aliases(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding serialized_response = serialize_response_with_datetime_encoding(response) aliases_list = serialized_response.get("Aliases", []) next_marker = serialized_response.get("NextMarker") if not aliases_list: return CommandResults(readable_output=f"No aliases found for function {args.get('function_name')}.") # Prepare readable output human_readable = tableToMarkdown( "AWS Lambda Aliases", aliases_list, headerTransform=pascalToSpace, removeNull=True, headers=["AliasArn", "Name", "FunctionVersion"], ) # Prepare outputs with pagination support outputs = { "AWS.Lambda.Aliases(val.AliasArn && val.AliasArn == obj.AliasArn)": aliases_list, "AWS.Lambda(true)": {"AliasesNextToken": next_marker}, } return CommandResults( outputs=outputs, readable_output=human_readable, raw_response=serialized_response, ) @staticmethod def get_account_settings_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves account settings for AWS Lambda. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing account limits and usage """ print_debug_logs(client, "Getting Lambda account settings") response = client.get_account_settings() if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding serialized_response = serialize_response_with_datetime_encoding(response) account_limit = serialized_response.get("AccountLimit", {}) account_usage = serialized_response.get("AccountUsage", {}) # Prepare readable output readable_data = { "AccountLimit": { "TotalCodeSize": str(account_limit.get("TotalCodeSize")), "CodeSizeUnzipped": str(account_limit.get("CodeSizeUnzipped")), "CodeSizeZipped": str(account_limit.get("CodeSizeZipped")), "ConcurrentExecutions": str(account_limit.get("ConcurrentExecutions")), "UnreservedConcurrentExecutions": str(account_limit.get("UnreservedConcurrentExecutions")), }, "AccountUsage": { "TotalCodeSize": str(account_usage.get("TotalCodeSize")), "FunctionCount": str(account_usage.get("FunctionCount")), }, } human_readable = tableToMarkdown( "AWS Lambda Account Settings", readable_data, headerTransform=pascalToSpace, removeNull=True, ) # Add region and account_id to the root of the output for context output = { "Region": args.get("region"), "AccountId": args.get("account_id"), "AccountLimit": account_limit, "AccountUsage": account_usage, } return CommandResults( outputs_prefix="AWS.Lambda.AccountSettings", outputs_key_field="AccountId", outputs=output, readable_output=human_readable, raw_response=serialized_response, ) @staticmethod def list_versions_by_function_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the versions of a Lambda function and returns the results. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - function_name (str): The name of the Lambda function - next_token (str, optional): The token for pagination - limit (int, optional): The maximum number of items to return - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing list of function versions with their configurations """ kwargs = {"FunctionName": args.get("function_name")} # Build pagination parameters using build_pagination_kwargs pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=50, next_token_name="Marker", limit_name="MaxItems" ) kwargs.update(pagination_kwargs) print_debug_logs(client, f"Listing Lambda function versions with parameters: {kwargs}") response = client.list_versions_by_function(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding serialized_response = serialize_response_with_datetime_encoding(response) versions = serialized_response.get("Versions", []) next_marker = serialized_response.get("NextMarker") if not versions: return CommandResults(readable_output=f"No versions found for function {args.get('function_name')}.") headers = ["FunctionName", "Role", "Runtime", "LastModified", "State", "Description"] human_readable = tableToMarkdown( "AWS Lambda Function Versions", versions, headers=headers, headerTransform=pascalToSpace, removeNull=True, ) # Prepare output with region context output = { "FunctionVersions": versions, "FunctionArn": versions[0].get("FunctionArn"), } outputs = { "AWS.Lambda.Functions(val.FunctionArn && val.FunctionArn == obj.FunctionArn)": output, "AWS.Lambda.Functions(true)": {"FunctionVersionsNextToken": next_marker}, } return CommandResults( outputs=outputs, readable_output=human_readable, raw_response=serialized_response, ) @staticmethod def delete_function_url_config_command(client: BotoClient, args: Dict[str, Any]): """ Deletes the URL configuration for a Lambda function in AWS. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - function_name (str): The name of the Lambda function - qualifier (str, optional): The qualifier of the function - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results of the deletion operation with success message """ kwargs = {"FunctionName": args.get("function_name")} if qualifier := args.get("qualifier"): kwargs["Qualifier"] = qualifier print_debug_logs(client, f"Deleting Lambda function URL config with parameters: {kwargs}") response = client.delete_function_url_config(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return CommandResults( readable_output=f"Successfully deleted function URL configuration for {args.get('function_name')}" ) return AWSErrorHandler.handle_response_error(response) @staticmethod def create_function_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a Lambda function from AWS. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including function configuration - function_name (str): The name of the function - runtime (str): The runtime environment - role (str): The ARN of the function's execution role - handler (str): The function entry point - code (str, optional): Entry ID of uploaded ZIP file - s3_bucket (str, optional): S3 bucket containing the code - description (str, optional): Function description - memory_size (int, optional): Memory size in MB (default: 128) - function_timeout (int, optional): Timeout in seconds (default: 3) - publish (bool, optional): Whether to publish the first version - environment (str/dict, optional): Environment variables - tags (str/dict, optional): Tags for the function - layers (list, optional): List of layer ARNs - vpc_config (str/dict, optional): VPC configuration - tracing_config (str, optional): Tracing mode (default: Active) - package_type (str, optional): Deployment package type - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing the created function details """ kwargs = prepare_create_function_kwargs(args) print_debug_logs(client, f"Creating Lambda function: {args.get('function_name')} using {kwargs=}") response = client.create_function(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.CREATED: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding response = serialize_response_with_datetime_encoding(response) outputs = copy.deepcopy(response) outputs.pop("ResponseMetadata", None) # Prepare readable output output_headers = [ "FunctionName", "FunctionArn", "Description", "Version", ] readable_output = tableToMarkdown( name=f"Created Lambda Function: {args.get('function_name')}", t=outputs, headerTransform=pascalToSpace, removeNull=True, headers=output_headers, ) return CommandResults( outputs=outputs, raw_response=response, outputs_prefix="AWS.Lambda.Functions", outputs_key_field="FunctionArn", readable_output=readable_output, ) @staticmethod def list_layer_versions_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the versions of an Lambda layer. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - layer_name (str): The name or ARN of the layer - compatible_runtime (str, optional): A runtime identifier - next_token (str, optional): Pagination token - limit (int, optional): Maximum number of versions to return - compatible_architecture (str, optional): Compatible architecture - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing list of layer versions """ kwargs = { "LayerName": args.get("layer_name"), "CompatibleRuntime": args.get("compatible_runtime"), "CompatibleArchitecture": args.get("compatible_architecture"), } # Build pagination parameters using build_pagination_kwargs pagination_kwargs = build_pagination_kwargs( args, minimum_limit=1, max_limit=50, next_token_name="Marker", limit_name="MaxItems" ) kwargs.update(pagination_kwargs) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Listing Lambda layer versions with parameters: {kwargs}") response = client.list_layer_versions(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding serialized_response = serialize_response_with_datetime_encoding(response) layer_versions = serialized_response.get("LayerVersions", []) next_marker = serialized_response.get("NextMarker") if not layer_versions: return CommandResults(readable_output=f"No layer versions found for {args.get('layer_name')}.") # Prepare outputs outputs = { "AWS.Lambda.LayerVersions(val.LayerVersionArn && val.LayerVersionArn == obj.LayerVersionArn)": layer_versions, "AWS.Lambda.LayerVersions(true)": {"LayerVersionsNextToken": next_marker}, } headers = ["LayerVersionArn", "Description", "CreatedDate", "Version"] readable_output = tableToMarkdown( name="Layer Version List", t=layer_versions, headers=headers, headerTransform=pascalToSpace, removeNull=True ) return CommandResults( outputs=remove_empty_elements(outputs), outputs_prefix="AWS.Lambda.LayerVersions", raw_response=serialized_response, readable_output=readable_output, ) @staticmethod def delete_function_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes a Lambda function from AWS. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - function_name (str): The name of the Lambda function - qualifier (str, optional): The qualifier of the function - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results of the deletion operation with success message """ kwargs = {"FunctionName": args.get("function_name")} if qualifier := args.get("qualifier"): kwargs["Qualifier"] = qualifier print_debug_logs(client, f"Deleting Lambda function with parameters: {kwargs}") response = client.delete_function(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: return CommandResults(readable_output=f"Successfully deleted Lambda function: {args.get('function_name')}") return AWSErrorHandler.handle_response_error(response) @staticmethod def delete_layer_version_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes a version of a Lambda layer. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - layer_name (str): The name or ARN of the layer - version_number (int): The version number to delete - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results of the deletion operation with success message """ kwargs = {"LayerName": args.get("layer_name"), "VersionNumber": arg_to_number(args.get("version_number"))} print_debug_logs(client, f"Deleting Lambda layer version with parameters: {kwargs}") response = client.delete_layer_version(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") in [HTTPStatus.OK, HTTPStatus.NO_CONTENT]: msg = f"Successfully deleted version {kwargs.get('VersionNumber')} of layer {kwargs.get('LayerName')}" return CommandResults(readable_output=msg) return AWSErrorHandler.handle_response_error(response) @staticmethod def publish_layer_version_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a Lambda layer from a ZIP archive. Args: client (BotoClient): The boto3 client for Lambda service args (Dict[str, Any]): Command arguments including: - layer_name (str): The name of the layer - description (str, optional): Description of the layer version - zip_file (str, optional): Entry ID of uploaded ZIP file - s3_bucket (str, optional): S3 bucket containing the layer code - s3_key (str, optional): S3 key of the layer code - s3_object_version (str, optional): S3 object version - compatible_runtimes (list, optional): Compatible runtimes - compatible_architectures (list, optional): Compatible architectures - region (str): AWS region - account_id (str): AWS account ID Returns: CommandResults: Results containing the published layer version details """ # Prepare content configuration content = {} s3_bucket = args.get("s3_bucket") s3_key = args.get("s3_key") s3_object_version = args.get("s3_object_version") if zip_file := args.get("zip_file"): file_path = demisto.getFilePath(zip_file).get("path") content["ZipFile"] = read_zip_to_bytes(file_path) elif s3_bucket and s3_key and s3_object_version: content["S3Bucket"] = s3_bucket content["S3Key"] = s3_key content["S3ObjectVersion"] = s3_object_version else: raise DemistoException( "Either zip_file or a combination of s3_bucket, s3_key and s3_object_version must be provided." ) kwargs = { "LayerName": args.get("layer_name"), "Description": args.get("description", ""), "Content": content, "CompatibleRuntimes": argToList(args.get("compatible_runtimes")), "CompatibleArchitectures": argToList(args.get("compatible_architectures")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Publishing Lambda layer version: {kwargs=}") response = client.publish_layer_version(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") not in [HTTPStatus.OK, HTTPStatus.CREATED]: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) # Serialize response with datetime encoding outputs = serialize_response_with_datetime_encoding(response) outputs.pop("ResponseMetadata", None) # Extract outputs based on headers outputs["Region"] = args.get("region") output_headers = [ "LayerVersionArn", "LayerArn", "Description", "CreatedDate", "Version", ] readable_output = tableToMarkdown( name=f"Published Layer Version: {response.get('LayerArn')}", t=outputs, headers=output_headers, headerTransform=pascalToSpace, removeNull=True, ) return CommandResults( outputs=remove_empty_elements(outputs), raw_response=serialize_response_with_datetime_encoding(response), outputs_prefix="AWS.Lambda.LayerVersions", outputs_key_field="LayerVersionArn", readable_output=readable_output, ) @staticmethod def update_function_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Updates the configuration of a Lambda function. Args: client (BotoClient): The boto3 client for Lambda service. args (Dict[str, Any]): Command arguments including function name and configuration settings. Returns: CommandResults: Results of the operation with updated function configuration details. """ function_name = args.get("function_name") kwargs = remove_empty_elements(build_kwargs_lambda_function_config_update(args)) print_debug_logs(client, f"Calling update_function_configuration with {kwargs=}") response = client.update_function_configuration(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = copy.deepcopy(response) if outputs.get("ResponseMetadata", {}): del outputs["ResponseMetadata"] human_readable = tableToMarkdown( f"Lambda Function Configuration Updated: {function_name}", outputs, headerTransform=pascalToSpace, removeNull=True, headers=["FunctionName", "FunctionArn", "Description", "LastModified"], ) return CommandResults( outputs_prefix="AWS.Lambda.FunctionConfig", outputs_key_field="FunctionArn", outputs=outputs, readable_output=human_readable, raw_response=response, ) class ACM: service = AWSServices.ACM @staticmethod def update_certificate_options_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Updates Certificate Transparency (CT) logging preference for an ACM certificate. Args: client: The AWS ACM boto3 client used to perform the update request. args (dict): A dictionary containing the certificate ARN and the desired transparency logging preference ("ENABLED" or "DISABLED"). Returns: CommandResults: An object containing a human-readable summary of the change, the raw AWS API response, and related metadata. """ arn = args.get("certificate_arn") pref = args.get("transparency_logging_preference") kwargs = {"CertificateArn": arn, "Options": {"CertificateTransparencyLoggingPreference": pref}} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"UpdateCertificateOptions params: {kwargs}") try: resp = client.update_certificate_options(**kwargs) status = resp.get("ResponseMetadata", {}).get("HTTPStatusCode") if status in (HTTPStatus.OK, HTTPStatus.NO_CONTENT): hr = f"Updated Certificate Transparency (CT) logging to '{pref}' for certificate '{arn}'." return CommandResults(readable_output=hr, raw_response=resp) return AWSErrorHandler.handle_response_error(resp) except ClientError as e: return AWSErrorHandler.handle_client_error(e) except Exception as e: raise DemistoException(f"Error updating certificate options for '{arn}': {str(e)}") class SSM: service = AWSServices.SSM @staticmethod def inventory_entries_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns an inventory item, and it's list of entries. Args: client: The AWS ACM boto3 client used to perform the update request. args (dict): A dictionary containing the command arguments. Returns: CommandResults: An object containing an inventory item, and it's list of entries. """ instance_id = args.get("instance_id") type_name = args.get("type_name") filters = args.get("filters") kwargs = { "InstanceId": instance_id, "TypeName": type_name, "Filters": parse_name_value_type_format_filter(filters), } kwargs.update(build_pagination_kwargs(args, 1, 50)) kwargs = remove_empty_elements(kwargs) demisto.debug(f"{kwargs=}") response = client.list_inventory_entries(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) if entries := response.get("Entries"): readable_output = tableToMarkdown( f"The inventory entries of item {instance_id} with the type {type_name}", entries, headerTransform=pascalToSpace, removeNull=True, ) outputs = copy.deepcopy(response) if outputs.get("ResponseMetadata", {}): del outputs["ResponseMetadata"] outputs["EntriesNextPageToken"] = outputs.pop("NextToken") if response.get("NextToken") else None return CommandResults( outputs_prefix="AWS.SSM.Inventory", outputs_key_field="InstanceId", outputs=outputs, readable_output=readable_output, raw_response=response, ) else: return CommandResults(readable_output=f"No entries found for the item {instance_id}.") @staticmethod @polling_function( name="aws-ssm-command-run", interval=arg_to_number(demisto.args().get("interval_in_seconds")) or DEFAULT_INTERVAL_IN_SECONDS, timeout=arg_to_number(demisto.args().get("polling_timeout")) or DEFAULT_TIMEOUT_POLLING_COMMAND, requires_polling_arg=False, # means it will always be default to poll, poll=true, ) def command_run_command(args: Dict[str, Any], client: BotoClient) -> PollResult | None: """ Runs commands on one or more managed nodes. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): A dictionary containing the command arguments. Returns: CommandResults: An object containing an inventory item, and it's list of entries. """ if command_id := args.get("command_id"): demisto.debug(f"There is a {command_id=}. Not the first execution.") response_command_list = client.list_commands(CommandId=command_id) status = response_command_list.get("Commands", [])[0].get("Status") demisto.debug(f"The {status=} of {command_id=}") if status in TERMINAL_COMMAND_STATUSES: return PollResult( response=CommandResults( readable_output=f"The command {command_id} status is {status}, {TERMINAL_COMMAND_STATUSES[status]}", raw_response=serialize_response_with_datetime_encoding(response_command_list), ), continue_to_poll=False, ) # if command not in TERMINAL_COMMAND_STATUSES, continue polling return PollResult( continue_to_poll=True, args_for_next_run=args, response=None, ) demisto.debug("First execution of command-run") document_hash = args.get("document_hash") kwargs = { "InstanceIds": argToList(args.get("instance_ids")), "DocumentName": args.get("document_name"), "DocumentVersion": args.get("document_version"), "DocumentHash": document_hash, "Comment": args.get("comment"), "OutputS3BucketName": args.get("output_s3_bucket_name"), "OutputS3KeyPrefix": args.get("output_s3_key_prefix"), "MaxConcurrency": args.get("max_concurrency"), "MaxErrors": args.get("max_errors"), "DocumentHashType": "Sha256" if document_hash else None, } if targets := args.get("targets"): kwargs["Targets"] = parse_target_field(targets) if parameters := args.get("parameters"): kwargs["Parameters"] = parse_key_values_2_dict(parameters) if command_timeout := arg_to_number(args.get("command_timeout")): max_command_timeout = 2592000 # Maximum timeout for running commands in ssm (30 days). min_command_timeout = 30 # Minimum timeout for running commands in ssm. if max_command_timeout < command_timeout or command_timeout < min_command_timeout: raise DemistoException( f"Command timeout must be between {min_command_timeout} and {max_command_timeout} seconds." ) kwargs["TimeoutSeconds"] = command_timeout kwargs = remove_empty_elements(kwargs) demisto.debug(f"{kwargs=}") response_command_run = client.send_command(**kwargs) command_id = response_command_run.get("Command", {}).get("CommandId", "") demisto.debug(f"The {command_id=} of the current execution.") args["command_id"] = command_id command_response = serialize_response_with_datetime_encoding(response_command_run.get("Command", {})) return PollResult( response=None, continue_to_poll=True, args_for_next_run=args, partial_result=CommandResults( readable_output=f"Command {command_id} was sent successfully.", outputs=command_response, outputs_prefix="AWS.SSM.Command", outputs_key_field="CommandId", ), ) @staticmethod def add_tags_to_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Adds or overwrites one or more tags for the specified SSM resource. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): A dictionary containing the resource type, resource ID, and tags to add. Returns: CommandResults: A success message indicating the tags were added. """ resource_id = args.get("resource_id") kwargs = { "ResourceType": args.get("resource_type"), "ResourceId": resource_id, "Tags": parse_tag_field(args.get("tags")), } print_debug_logs(client, f"add_tags_to_resource {kwargs=}") response = client.add_tags_to_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Tags were successfully added to the SSM resource '{resource_id}'.", ) @staticmethod def remove_tags_from_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Removes one or more tags from the specified SSM resource. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): A dictionary containing the resource type, resource ID, and tag keys to remove. Returns: CommandResults: A success message indicating the tags were removed. """ kwargs = { "ResourceType": args.get("resource_type"), "ResourceId": args.get("resource_id"), "TagKeys": argToList(args.get("tag_keys")), } print_debug_logs(client, f"remove_tags_from_resource {kwargs=}") response = client.remove_tags_from_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"Tags were successfully removed from the SSM resource '{args.get('resource_id')}'.", ) @staticmethod def list_tags_for_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns a list of the tags assigned to the specified SSM resource. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): Command arguments including resource_type and resource_id. Returns: CommandResults: An object containing the tag list for the specified resource. """ resource_id = args.get("resource_id") kwargs = { "ResourceType": args.get("resource_type"), "ResourceId": resource_id, } print_debug_logs(client, f"list_tags_for_resource {kwargs=}") response = client.list_tags_for_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) tag_list = response.get("TagList", []) if not tag_list: return CommandResults(readable_output=f"No tags found for SSM resource '{resource_id}'.") outputs = {"ResourceId": resource_id, "TagList": tag_list} return CommandResults( outputs_prefix="AWS.SSM.Tags", outputs_key_field="ResourceId", outputs=outputs, readable_output=tableToMarkdown( f"Tags for SSM resource '{resource_id}'", tag_list, headers=["Key", "Value"], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def inventory_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Queries SSM inventory information for managed nodes. Args: client (BotoClient): The boto3 client for SSM service. args (Dict[str, Any]): Command arguments including optional filters (key/values/type format), result_attributes, aggregator_expression, aggregator_groups (JSON), inventory_aggregator, limit, and next_token. Returns: CommandResults: Results containing inventory entities with their data. """ aggregator_groups_raw = args.get("aggregator_groups") aggregator_expression = args.get("aggregator_expression") inventory_aggregator = args.get("inventory_aggregator") result_attributes = argToList(args.get("result_attributes")) if aggregator_groups_raw and inventory_aggregator: raise ValueError( "The arguments aggregator_groups and inventory_aggregator cannot be used together. " "Use aggregator_groups to define groups within the aggregator, " "or inventory_aggregator to define a nested sub-aggregator expression." ) if aggregator_groups_raw and not aggregator_expression: raise ValueError( "The argument aggregator_expression is required when using aggregator_groups. " "Groups must be nested inside a parent aggregator that has an Expression." ) # GetInventory Filters use Key/Type/Values — parsed directly from the triple-format filter string inventory_filters = parse_name_value_type_format_filter(args.get("filters")) # Build aggregator — Groups must live in a nested sub-aggregator under Expression. # AWS enforces: Groups cannot coexist with Expression in the same aggregator object. if aggregator_groups_raw: try: aggregator_groups = json.loads(aggregator_groups_raw) except json.JSONDecodeError as e: raise ValueError(f"aggregator_groups must be a valid JSON string: {e}") from e aggregator = { "Expression": aggregator_expression, "Aggregators": [{"Groups": aggregator_groups}], } else: aggregator = remove_empty_elements( { "Expression": aggregator_expression, "Aggregators": [{"Expression": inventory_aggregator}] if inventory_aggregator else None, } ) kwargs: Dict[str, Any] = remove_empty_elements( { "Filters": inventory_filters, "ResultAttributes": [{"TypeName": t} for t in result_attributes], "Aggregators": [aggregator] if aggregator else None, } ) kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM inventory with {kwargs=}") response = client.get_inventory(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) entities = response.get("Entities", []) if not entities: return CommandResults(readable_output="No inventory entities found.") outputs = { "AWS.SSM.Inventory(val.Id && val.Id == obj.Id)": entities, "AWS.SSM(true)": {"InventoryNextToken": response.get("NextToken")}, } table_rows = [] for entity in entities: row: Dict[str, Any] = {"Id": entity.get("Id")} first_entry: Dict[str, Any] = next(iter(entity.get("Data", {}).values()), {}) row["TypeName"] = first_entry.get("TypeName") row["SchemaVersion"] = first_entry.get("SchemaVersion") row["CaptureTime"] = first_entry.get("CaptureTime") row["Content"] = first_entry.get("Content") table_rows.append(row) return CommandResults( outputs=remove_empty_elements(outputs), readable_output=tableToMarkdown( "AWS SSM Inventory", table_rows, headers=["Id", "TypeName", "SchemaVersion", "CaptureTime", "Content"], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def associations_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns all State Manager associations in the current AWS account and Region. Args: client (BotoClient): The boto3 client for SSM service. args (Dict[str, Any]): Command arguments including optional filters (semicolon-separated list in the format key=,value=), limit, and next_token. Returns: CommandResults: Results containing the list of SSM associations with their status, schedule, and target information. """ raw_filters = parse_tag_field(args.get("filters")) association_filter_list = [{"key": f["Key"], "value": f["Value"]} for f in raw_filters] kwargs: Dict[str, Any] = remove_empty_elements( { "AssociationFilterList": association_filter_list, } ) kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM associations with parameters: {kwargs}") response = client.list_associations(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) associations = response.get("Associations", []) if not associations: return CommandResults(readable_output="No SSM associations found.") outputs = { "AWS.SSM.Associations(val.AssociationId && val.AssociationId == obj.AssociationId)": associations, "AWS.SSM(true)": {"AssociationsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS SSM Associations", associations, headers=[ "Name", "AssociationId", "AssociationName", "AssociationVersion", "LastExecutionDate", "ScheduleExpression", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def association_get_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the association for the specified target or managed node. Args: client (BotoClient): The boto3 client for SSM service. args (Dict[str, Any]): Command arguments. Must provide either association_id, or both instance_id and document_name. Returns: CommandResults: Results containing the association description. """ association_id = args.get("association_id") instance_id = args.get("instance_id") document_name = args.get("document_name") if not (association_id or (instance_id and document_name)): raise DemistoException("Must provide either association_id, or both instance_id and document_name.") kwargs = remove_empty_elements( { "AssociationId": association_id, "AssociationVersion": args.get("association_version"), "InstanceId": instance_id, "Name": document_name, } ) print_debug_logs(client, f"Describing SSM association with parameters: {kwargs}") response = client.describe_association(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) association = response.get("AssociationDescription", {}) if not association: return CommandResults(readable_output="No association found.") return CommandResults( outputs_prefix="AWS.SSM.Associations", outputs_key_field="AssociationId", outputs=association, readable_output=tableToMarkdown( "AWS SSM Association", association, headers=[ "Name", "AssociationId", "AssociationName", "AssociationVersion", "DocumentVersion", "Date", "LastExecutionDate", "ScheduleExpression", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def association_versions_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves all versions of an association for a specific association ID. Args: client (BotoClient): The boto3 client for SSM service. args (Dict[str, Any]): Command arguments including association_id (required), and optional limit and next_token. Returns: CommandResults: Results containing the list of association versions. """ kwargs: Dict[str, Any] = { "AssociationId": args.get("association_id"), } kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM association versions with parameters: {kwargs}") response = client.list_association_versions(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) association_versions = response.get("AssociationVersions", []) if not association_versions: return CommandResults(readable_output=f"No versions found for association '{args.get('association_id')}'.") association_id = args.get("association_id") outputs = { "AWS.SSM.Associations(val.AssociationId && val.AssociationId == obj.AssociationId)": { "AssociationId": association_id, "Versions": association_versions, }, "AWS.SSM.Associations(true)": {"AssociationVersionNextToken": response.get("NextToken")}, } return CommandResults( outputs=remove_empty_elements(outputs), readable_output=tableToMarkdown( "AWS SSM Association Versions", association_versions, headers=[ "AssociationId", "AssociationVersion", "Name", "AssociationName", "CreatedDate", "DocumentVersion", "ScheduleExpression", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def documents_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns all SSM documents in the current AWS account and Region. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): Command arguments including optional filters, limit, and next_token. Returns: CommandResults: Results containing the list of SSM documents with pagination token. """ raw_filters = parse_filter_field(args.get("filters")) kwargs = remove_empty_elements( { "Filters": [{"Key": f["Name"], "Values": f["Values"]} for f in raw_filters], } ) kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM documents with parameters: {kwargs}") response = client.list_documents(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) documents = response.get("DocumentIdentifiers", []) if not documents: return CommandResults(readable_output="No SSM documents found.") outputs = { "AWS.SSM.Documents(val.Name && val.Name == obj.Name)": documents, "AWS.SSM(true)": {"DocumentsNextToken": response.get("NextToken")}, } return CommandResults( outputs=remove_empty_elements(outputs), readable_output=tableToMarkdown( "AWS SSM Documents", documents, headers=[ "Name", "Owner", "DocumentVersion", "DocumentType", "PlatformTypes", "CreatedDate", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def document_describe_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Describes the specified SSM document. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): Command arguments including document_name, and optional document_version and version_name. Returns: CommandResults: Results containing the SSM document description. """ kwargs = remove_empty_elements( { "Name": args.get("document_name"), "DocumentVersion": args.get("document_version"), "VersionName": args.get("version_name"), } ) print_debug_logs(client, f"Describing SSM document with {kwargs=}") response = client.describe_document(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) document = response.get("Document", {}) if not document: return CommandResults(readable_output=f"No document found with name '{args.get('document_name')}'.") return CommandResults( outputs_prefix="AWS.SSM.Documents", outputs_key_field="Name", outputs=document, readable_output=tableToMarkdown( "AWS SSM Document", document, headers=[ "Name", "Owner", "DocumentVersion", "DocumentType", "PlatformTypes", "CreatedDate", "Status", "Description", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod def automation_execution_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the executions of SSM automation workflows. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): Command arguments including optional filters, limit, and next_token. Returns: CommandResults: Results containing the list of automation executions with pagination token. """ # remap Name→Key raw_filters = parse_filter_field(args.get("filters")) automation_filters = [{"Key": f["Name"], "Values": f["Values"]} for f in raw_filters] kwargs = remove_empty_elements({"Filters": automation_filters}) kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM automation executions with {kwargs=}") response = client.describe_automation_executions(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) executions = response.get("AutomationExecutionMetadataList", []) if not executions: return CommandResults(readable_output="No SSM automation executions found.") outputs = { "AWS.SSM.AutomationExecutions(" "val.AutomationExecutionId && val.AutomationExecutionId == obj.AutomationExecutionId)": executions, "AWS.SSM(true)": {"AutomationExecutionsNextToken": response.get("NextToken")}, } return CommandResults( outputs=remove_empty_elements(outputs), readable_output=tableToMarkdown( "AWS SSM Automation Executions", executions, headers=[ "AutomationExecutionId", "DocumentName", "DocumentVersion", "ExecutionStartTime", "ExecutionEndTime", "AutomationExecutionStatus", "Mode", "ExecutedBy", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod @polling_function( name="aws-ssm-automation-execution-run", interval=arg_to_number(demisto.args().get("interval_in_seconds")) or DEFAULT_INTERVAL_IN_SECONDS, timeout=arg_to_number(demisto.args().get("polling_timeout")) or DEFAULT_TIMEOUT_POLLING_COMMAND, requires_polling_arg=False, ) def automation_execution_run_command(args: Dict[str, Any], client: BotoClient) -> PollResult: """ Initiates or polls the status of an SSM automation execution. Args: args (dict): Command arguments including document_name, and optional parameters, mode, document_version, max_concurrency, max_errors, target_parameter_name, target_key, target_values, tags, client_token, and execution_id (hidden, used for polling). client: The AWS SSM boto3 client used to perform the request. Returns: PollResult: An object containing the results of the command and whether to continue polling. """ execution_id = args.get("execution_id") if not execution_id: # First execution — start the automation targets = parse_target_field(args.get("targets")) # Build AlarmConfiguration from flat args — only when alarm_names are provided alarm_names = argToList(args.get("alarm_names")) alarm_configuration = ( { "Alarms": [{"Name": name} for name in alarm_names], "IgnorePollAlarmFailure": arg_to_bool_or_none(args.get("alarm_ignore_poll_failure")), } if alarm_names else None ) # Build TargetLocations from tag-style flat arg raw_target_locations = parse_tag_field(args.get("target_locations")) target_locations = [{loc["Key"]: loc["Value"] for loc in raw_target_locations}] # Build TargetMaps from key-values format target_maps_str: str = args.get("target_maps") or "" raw_target_maps = parse_key_values_2_dict(target_maps_str) if target_maps_str else {} target_maps = [{k: v} for k, v in raw_target_maps.items()] or None parameters_str: str = args.get("parameters") or "" parameters = parse_key_values_2_dict(parameters_str) if parameters_str else None kwargs = remove_empty_elements( { "DocumentName": args.get("document_name"), "DocumentVersion": args.get("document_version"), "Mode": args.get("mode", "Auto"), "ClientToken": args.get("client_token"), "MaxConcurrency": args.get("max_concurrency"), "MaxErrors": args.get("max_errors"), "TargetParameterName": args.get("target_parameter_name"), "Parameters": parameters, "Tags": parse_tag_field(args.get("tags")) if args.get("tags") else None, "Targets": targets, "TargetLocations": target_locations, "TargetLocationsURL": args.get("target_locations_url"), "TargetMaps": target_maps, "AlarmConfiguration": alarm_configuration, } ) # Redact Parameters from debug log as they may contain sensitive data safe_kwargs = {k: v for k, v in kwargs.items() if k != "Parameters"} print_debug_logs(client, f"Starting SSM automation execution with {safe_kwargs=}") response = client.start_automation_execution(**kwargs) execution_id = response.get("AutomationExecutionId", "") args["execution_id"] = execution_id demisto.debug(f"[ssm] aws-ssm-automation-execution-run: started execution_id={execution_id}") return PollResult( partial_result=CommandResults( readable_output=f"Automation execution {execution_id} started successfully.", ), response=None, continue_to_poll=True, args_for_next_run=args, ) # Polling — check status demisto.debug(f"[ssm] aws-ssm-automation-execution-run: polling execution_id={execution_id}") automation_response = client.get_automation_execution(AutomationExecutionId=execution_id) automation = serialize_response_with_datetime_encoding(automation_response.get("AutomationExecution", {})) status = automation.get("AutomationExecutionStatus", "") demisto.debug(f"[ssm] aws-ssm-automation-execution-run: execution_id={execution_id} status={status}") if status in TERMINAL_COMMAND_STATUSES: if failure_message := automation.get("FailureMessage"): readable_output = f"Automation execution {execution_id} failed: {failure_message}" else: readable_output = f"Automation execution {execution_id} status: {status}. {TERMINAL_COMMAND_STATUSES[status]}" return PollResult( response=CommandResults( outputs_prefix="AWS.SSM.AutomationExecutions", outputs_key_field="AutomationExecutionId", outputs=automation, readable_output=readable_output, raw_response=automation, ), continue_to_poll=False, ) return PollResult(response=None, continue_to_poll=True, args_for_next_run=args) @staticmethod @polling_function( name="aws-ssm-automation-execution-cancel", interval=arg_to_number(demisto.args().get("interval_in_seconds")) or DEFAULT_INTERVAL_IN_SECONDS, timeout=arg_to_number(demisto.args().get("polling_timeout")) or DEFAULT_TIMEOUT_POLLING_COMMAND, requires_polling_arg=False, ) def automation_execution_cancel_command(args: Dict[str, Any], client: BotoClient) -> PollResult: """ Cancels an SSM automation execution and polls until the cancellation is confirmed. Args: args (dict): Command arguments including automation_execution_id, optional type (Cancel or Complete), and first_run (hidden, used for polling state). client: The AWS SSM boto3 client used to perform the request. Returns: PollResult: An object containing the results of the command and whether to continue polling. """ automation_execution_id = args.get("automation_execution_id", "") if not argToBoolean(args.get("first_run", True)): # Polling — check cancellation status automation_response = client.get_automation_execution(AutomationExecutionId=automation_execution_id) automation = serialize_response_with_datetime_encoding(automation_response.get("AutomationExecution", {})) status = automation.get("AutomationExecutionStatus", "") demisto.debug(f"[ssm] aws-ssm-automation-execution-cancel: {automation_execution_id=} {status=}") if status in TERMINAL_COMMAND_STATUSES: return PollResult( response=CommandResults( outputs_prefix="AWS.SSM.AutomationExecutions", outputs_key_field="AutomationExecutionId", outputs=automation, readable_output=f"Automation execution {automation_execution_id} status: {status}. " f"{TERMINAL_COMMAND_STATUSES[status]}", raw_response=automation, ), continue_to_poll=False, ) return PollResult(response=None, continue_to_poll=True, args_for_next_run=args) # Initial execution — send cancel kwargs: Dict[str, Any] = remove_empty_elements( { "AutomationExecutionId": automation_execution_id, "Type": args.get("type"), } ) print_debug_logs(client, f"Stopping SSM automation execution with {kwargs=}") client.stop_automation_execution(**kwargs) args["first_run"] = False return PollResult( response=None, partial_result=CommandResults( readable_output=f"Cancellation request sent for automation execution '{automation_execution_id}'.", ), continue_to_poll=True, args_for_next_run=args, ) @staticmethod def command_list_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the commands requested by users of the AWS account. Args: client: The AWS SSM boto3 client used to perform the request. args (dict): Command arguments including optional command_id, instance_id, filters, limit, and next_token. Returns: CommandResults: Results containing the list of SSM commands with pagination token. """ raw_filters = parse_tag_field(args.get("filters")) command_filters = [{"key": f["Key"], "value": f["Value"]} for f in raw_filters] kwargs: Dict[str, Any] = remove_empty_elements( { "CommandId": args.get("command_id"), "InstanceId": args.get("instance_id"), "Filters": command_filters, } ) kwargs.update(build_pagination_kwargs(args, minimum_limit=1, max_limit=50)) print_debug_logs(client, f"Listing SSM commands with {kwargs=}") response = client.list_commands(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) commands = response.get("Commands", []) if not commands: # AWS SSM ListCommands has a known pagination quirk: when called without a NextToken, # it may return an empty Commands list alongside a new NextToken. # We transparently retry once with the returned token so the caller always receives actual results when they exist. if retry_token := response.get("NextToken"): demisto.debug( "[SSM] command_list_command: received empty Commands with a NextToken — retrying with the new token." ) kwargs["NextToken"] = retry_token response = client.list_commands(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response = serialize_response_with_datetime_encoding(response) print_debug_logs(client, f"Retry {response=}") commands = response.get("Commands", []) if not commands: return CommandResults(readable_output="No SSM commands found.") outputs = { "AWS.SSM.Command(val.CommandId && val.CommandId == obj.CommandId)": commands, "AWS.SSM(true)": {"CommandNextToken": response.get("NextToken")}, } return CommandResults( outputs=remove_empty_elements(outputs), readable_output=tableToMarkdown( "AWS SSM Commands", commands, headers=[ "CommandId", "DocumentName", "Status", "RequestedDateTime", "Comment", "TargetCount", "CompletedCount", "ErrorCount", "DeliveryTimedOutCount", ], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) @staticmethod @polling_function( name="aws-ssm-command-cancel", interval=arg_to_number(demisto.args().get("interval_in_seconds")) or DEFAULT_INTERVAL_IN_SECONDS, timeout=arg_to_number(demisto.args().get("polling_timeout")) or DEFAULT_TIMEOUT_POLLING_COMMAND, requires_polling_arg=False, ) def command_cancel_command(args: Dict[str, Any], client: BotoClient) -> PollResult: """ Cancels the specified SSM command and polls until the cancellation is confirmed. Args: args (dict): Command arguments including command_id, optional instance_ids, and first_run (hidden, used for polling state). client: The AWS SSM boto3 client used to perform the request. Returns: PollResult: An object containing the results of the command and whether to continue polling. """ command_id = args.get("command_id", "") if not argToBoolean(args.get("first_run", True)): # Polling — check command status response_list = client.list_commands(CommandId=command_id) status = response_list.get("Commands", [{}])[0].get("Status", "") demisto.debug(f"[ssm] aws-ssm-command-cancel: command_id={command_id} status={status}") if status in TERMINAL_COMMAND_STATUSES: return PollResult( response=CommandResults( readable_output=f"Command {command_id} status: {status}. {TERMINAL_COMMAND_STATUSES[status]}", ), continue_to_poll=False, ) return PollResult(response=None, continue_to_poll=True, args_for_next_run=args) # Initial execution — send cancel kwargs: Dict[str, Any] = remove_empty_elements( { "CommandId": command_id, "InstanceIds": argToList(args.get("instance_ids")) or None, } ) print_debug_logs(client, f"Cancelling SSM command with {kwargs=}") client.cancel_command(**kwargs) args["first_run"] = False return PollResult( response=None, partial_result=CommandResults( readable_output=f"Cancellation request sent for command '{command_id}'.", ), continue_to_poll=True, args_for_next_run=args, ) class CloudWatchLogs: service = AWSServices.CloudWatchLogs @staticmethod def log_group_create_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a log group with the specified name. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - kms_key_id: The ARN of the CMK to use when encrypting log data (optional). - log_group_class: The class of the log group - STANDARD or INFREQUENT_ACCESS (optional). - tags: Key-value pairs to tag the log group, in JSON format (optional). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ tags = args.get("tags") kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "kmsKeyId": args.get("kms_key_id"), "logGroupClass": args.get("log_group_class"), "tags": {tag["Key"]: tag["Value"] for tag in parse_tag_field(tags)}, "deletionProtectionEnabled": arg_to_bool_or_none(args.get("deletion_protection_enabled")), } kwargs = remove_empty_elements(kwargs) demisto.debug(f"{kwargs=}") response = client.create_log_group(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully created log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def log_stream_create_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates a log stream for the specified log group. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - log_stream_name: The name of the log stream (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "logStreamName": args.get("log_stream_name"), } demisto.debug(f"{kwargs=}") response = client.create_log_stream(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully created log stream: {args.get('log_stream_name')} " f"in log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def log_group_delete_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified log group and permanently deletes all the archived log events associated with the log group. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ log_group_name = args.get("log_group_name") demisto.debug(f"{log_group_name=}") response = client.delete_log_group(logGroupName=log_group_name) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully deleted log group: {log_group_name}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def log_stream_delete_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified log stream and permanently deletes all the archived log events associated with the log stream. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - log_stream_name: The name of the log stream (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "logStreamName": args.get("log_stream_name"), } demisto.debug(f"{kwargs=}") response = client.delete_log_stream(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully deleted log stream: {args.get('log_stream_name')} " f"from log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def log_events_filter_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists log events from the specified log group. You can list all the log events or filter the results using a filter pattern, a time range, and the name of the log stream. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - log_group_identifier: The ARN or name of the log group to search (optional). - log_stream_names: Comma-separated list of log stream names (optional). - log_stream_name_prefix: Filters by log stream name prefix (optional). - start_time: Start of the time range in milliseconds since epoch (optional). - end_time: End of the time range in milliseconds since epoch (optional). - filter_pattern: The filter pattern to use (optional). - limit: Maximum number of events to return (optional). - next_token: Pagination token from a previous request (optional). - unmask: Display log events with unmasked data (optional). Returns: CommandResults: Results containing the filtered log events. Raises: DemistoException: If the AWS API call fails. """ log_group_name = args.get("log_group_name") log_group_identifier = args.get("log_group_identifier") if not (bool(log_group_name) ^ bool(log_group_identifier)): # "^" is bitwise XOR - exactly one of the two must be true raise DemistoException("You must provide exactly one of the arguments log_group_name or log_group_identifier.") log_stream_names = args.get("log_stream_names") kwargs: dict[str, Any] = { "logGroupName": log_group_name, "logGroupIdentifier": log_group_identifier, "logStreamNames": argToList(log_stream_names), "logStreamNamePrefix": args.get("log_stream_name_prefix"), "startTime": arg_to_number(args.get("start_time")), "endTime": arg_to_number(args.get("end_time")), "filterPattern": args.get("filter_pattern"), "unmask": arg_to_bool_or_none(args.get("unmask")), } kwargs.update( build_pagination_kwargs( args, next_token_name="nextToken", limit_name="limit", ) ) remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.filter_log_events(**kwargs) events = response.get("events", []) if not events: return CommandResults( readable_output="No events were found.", ) readable = tableToMarkdown( "AWS CloudWatch Logs Events", events, headers=["logStreamName", "timestamp", "message", "ingestionTime", "eventId"], headerTransform=pascalToSpace, removeNull=True, ) next_token = response.get("nextToken") outputs = { "AWS.CloudWatchLogs.Events(val.eventId && val.eventId == obj.eventId)": events, "AWS.CloudWatchLogs(true)": {"EventsNextToken": next_token}, } return CommandResults( outputs=outputs, readable_output=readable, raw_response=response, ) @staticmethod def log_groups_describe_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the specified log groups. You can list all your log groups or filter the results by prefix. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name_prefix: The prefix to match (optional). - log_group_name_pattern: Case-sensitive substring to match against log group names (optional). - log_group_identifiers: Comma-separated list of log group ARNs or names to describe (optional). - account_identifiers: Comma-separated list of account IDs for cross-account querying (optional). - include_linked_accounts: Whether to include log groups in linked source accounts (optional). - log_group_class: Filter by log group class - STANDARD or INFREQUENT_ACCESS (optional). - limit: Maximum number of items returned (optional, default up to 50). - next_token: Pagination token from a previous request (optional). Returns: CommandResults: Results containing the log groups. Raises: DemistoException: If the AWS API call fails. """ log_group_identifiers = args.get("log_group_identifiers") kwargs: dict[str, Any] = { "logGroupNamePrefix": args.get("log_group_name_prefix"), "logGroupNamePattern": args.get("log_group_name_pattern"), "logGroupIdentifiers": argToList(log_group_identifiers), "accountIdentifiers": argToList(args.get("account_identifiers")), "includeLinkedAccounts": arg_to_bool_or_none(args.get("include_linked_accounts")), "logGroupClass": args.get("log_group_class"), } kwargs.update(build_pagination_kwargs(args, max_limit=50, next_token_name="nextToken", limit_name="limit")) remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.describe_log_groups(**kwargs) data = response.get("logGroups", []) if not data: return CommandResults( readable_output="No log groups were found.", ) readable = tableToMarkdown( "AWS CloudWatch Log Groups", data, headers=["logGroupName", "creationTime", "arn", "retentionInDays", "metricFilterCount", "storedBytes", "kmsKeyId"], headerTransform=pascalToSpace, removeNull=True, ) next_token = response.get("nextToken") outputs = { "AWS.CloudWatchLogs.LogGroups(val.logGroupName && val.logGroupName == obj.logGroupName)": data, "AWS.CloudWatchLogs(true)": {"LogGroupsNextToken": next_token}, } return CommandResults( outputs=outputs, readable_output=readable, raw_response=response, ) @staticmethod def log_streams_describe_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the log streams for the specified log group. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - log_group_identifier: The ARN or name of the log group (optional). - log_stream_name_prefix: The prefix to match (optional). - order_by: Order results by LogStreamName or LastEventTime (optional). - descending: If true, results are returned in descending order (optional). - limit: Maximum number of items returned (optional). - next_token: Pagination token from a previous request (optional). Returns: CommandResults: Results containing the log streams. Raises: DemistoException: If the AWS API call fails. """ log_group_name = args.get("log_group_name") log_group_identifier = args.get("log_group_identifier") if not (bool(log_group_name) ^ bool(log_group_identifier)): # "^" is bitwise XOR - exactly one of the two must be true raise DemistoException("You must provide exactly one of the arguments log_group_name or log_group_identifier.") kwargs: dict[str, Any] = { "logGroupName": log_group_name, "logGroupIdentifier": log_group_identifier, "logStreamNamePrefix": args.get("log_stream_name_prefix"), "orderBy": args.get("order_by"), "descending": arg_to_bool_or_none(args.get("descending")), } kwargs.update(build_pagination_kwargs(args, next_token_name="nextToken", limit_name="limit", max_limit=50)) remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.describe_log_streams(**kwargs) data = response.get("logStreams", []) if not data: return CommandResults( readable_output="No log streams were found.", ) readable = tableToMarkdown( "AWS CloudWatch Log Streams", data, headers=[ "logStreamName", "creationTime", "arn", "firstEventTimestamp", "lastEventTimestamp", "storedBytes", "lastIngestionTime", "uploadSequenceToken", ], headerTransform=pascalToSpace, removeNull=True, ) next_token = response.get("nextToken") data_log = {"logGroupName": log_group_name, "LogStreams": data} outputs = { "AWS.CloudWatchLogs.LogGroups(val.logGroupName && val.logGroupName == obj.logGroupName)": data_log, "AWS.CloudWatchLogs.LogGroups(true)": {"LogStreamsNextToken": next_token}, } return CommandResults( outputs=outputs, readable_output=readable, raw_response=response, ) @staticmethod def retention_policy_put_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Sets the retention of the specified log group. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - retention_in_days: The number of days to retain the log events (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "retentionInDays": arg_to_number(args.get("retention_in_days")), } demisto.debug(f"{kwargs=}") response = client.put_retention_policy(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully set retention policy of {args.get('retention_in_days')} days " f"for log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def retention_policy_delete_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified retention policy. Log events do not expire if they belong to log groups without a retention policy. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ log_group_name = args.get("log_group_name") demisto.debug(f"{log_group_name=}") response = client.delete_retention_policy(logGroupName=log_group_name) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully deleted retention policy for log group: {log_group_name}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def log_events_put_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Uploads a log event to the specified log stream. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - log_stream_name: The name of the log stream (required). - timestamp: The time the event occurred in milliseconds since epoch (required). - message: The raw event message (required). - key_attributes: Entity key attributes in the format key=,value= separated by semicolons (optional). - attributes: Entity attributes in the format key=,value= separated by semicolons (optional). Returns: CommandResults: Results of the operation. Raises: DemistoException: If the AWS API call fails. """ key_attributes = parse_tag_field(args.get("key_attributes")) attributes = parse_tag_field(args.get("attributes")) entity: dict[str, Any] = { "keyAttributes": {tag["Key"]: tag["Value"] for tag in key_attributes}, "attributes": {tag["Key"]: tag["Value"] for tag in attributes}, } remove_nulls_from_dictionary(entity) kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "logStreamName": args.get("log_stream_name"), "logEvents": [ { "timestamp": arg_to_number(args.get("timestamp")), "message": args.get("message"), } ], "entity": entity if any(entity.values()) else None, } kwargs = remove_empty_elements(kwargs) demisto.debug(f"{kwargs=}") response = client.put_log_events(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: data = { "rejectedLogEventsInfo": response.get("rejectedLogEventsInfo"), "rejectedEntityInfo": response.get("rejectedEntityInfo"), } readable = "Successfully created a log event!" return CommandResults( outputs_prefix="AWS.CloudWatchLogs.PutLogEvents", outputs=data, readable_output=readable, raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def metric_filter_put_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates or updates a metric filter and associates it with the specified log group. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - filter_name: A name for the metric filter (required). - filter_pattern: A filter pattern for extracting metric data (required). - metric_name: The name of the CloudWatch metric (required). - metric_namespace: The namespace of the CloudWatch metric (required). - metric_value: The value to publish to the CloudWatch metric (required). - default_value: The value to emit when a filter pattern does not match (optional). - dimensions: The fields to use as dimensions for the metric, as JSON (optional). - unit: The unit to assign to the metric (optional). - field_selection_criteria: A filter expression that specifies which log events should be processed based on system fields such as source account and source region (optional, max 2000 chars). - emit_system_field_dimensions: Comma-separated list of system fields to emit as additional dimensions. Valid values are @aws.account and @aws.region (optional). - apply_on_transformed_logs: Whether to apply on transformed logs (optional). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ default_value = args.get("default_value") dimensions = args.get("dimensions") emit_system_field_dimensions = args.get("emit_system_field_dimensions") metric_transformation: dict[str, Any] = { "metricName": args.get("metric_name"), "metricNamespace": args.get("metric_namespace"), "metricValue": args.get("metric_value"), "defaultValue": float(default_value) if default_value else None, "dimensions": {tag["Key"]: tag["Value"] for tag in parse_tag_field(dimensions)}, "unit": args.get("unit"), } kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "filterName": args.get("filter_name"), "filterPattern": args.get("filter_pattern"), "metricTransformations": [metric_transformation], "applyOnTransformedLogs": arg_to_bool_or_none(args.get("apply_on_transformed_logs")), "fieldSelectionCriteria": args.get("field_selection_criteria"), "emitSystemFieldDimensions": argToList(emit_system_field_dimensions), } kwargs = remove_empty_elements(kwargs) demisto.debug(f"{kwargs=}") response = client.put_metric_filter(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully created/updated metric filter: {args.get('filter_name')} " f"for log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def metric_filter_delete_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified metric filter. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (required). - filter_name: The name of the metric filter (required). Returns: CommandResults: Results of the operation with a success message. Raises: DemistoException: If the AWS API call fails. """ kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "filterName": args.get("filter_name"), } demisto.debug(f"{kwargs=}") response = client.delete_metric_filter(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") == HTTPStatus.OK: return CommandResults( readable_output=f"Successfully deleted metric filter: {args.get('filter_name')} " f"from log group: {args.get('log_group_name')}", raw_response=response, ) return AWSErrorHandler.handle_response_error(response) @staticmethod def metric_filters_describe_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Lists the specified metric filters. Args: client (BotoClient): The AWS CloudWatch Logs boto3 client. args (Dict[str, Any]): Command arguments containing: - log_group_name: The name of the log group (optional). - filter_name_prefix: The prefix to match (optional). - metric_name: Filters results by metric name (optional). - metric_namespace: Filters results by metric namespace (optional). - limit: Maximum number of items returned (optional, default 50). - next_token: Pagination token from a previous request (optional). Returns: CommandResults: Results containing the metric filters. Raises: DemistoException: If the AWS API call fails. """ kwargs: dict[str, Any] = { "logGroupName": args.get("log_group_name"), "filterNamePrefix": args.get("filter_name_prefix"), "metricName": args.get("metric_name"), "metricNamespace": args.get("metric_namespace"), } kwargs.update(build_pagination_kwargs(args, next_token_name="nextToken", limit_name="limit", max_limit=50)) remove_nulls_from_dictionary(kwargs) demisto.debug(f"{kwargs=}") response = client.describe_metric_filters(**kwargs) metric_filters = response.get("metricFilters", []) if not metric_filters: return CommandResults(readable_output="No metric filters were found.") metric_filters = serialize_response_with_datetime_encoding(metric_filters) next_token = response.get("nextToken") return CommandResults( outputs={ "AWS.CloudWatchLogs.MetricFilters(val.filterName && val.filterName == obj.filterName)": metric_filters, "AWS.CloudWatchLogs(true)": {"MetricFiltersNextToken": next_token}, }, readable_output=tableToMarkdown( "AWS CloudWatch Metric Filters", metric_filters, headers=["filterName", "filterPattern", "creationTime", "logGroupName"], headerTransform=pascalToSpace, removeNull=True, ), raw_response=response, ) class NetworkFirewall: service = AWSServices.NetworkFirewall @staticmethod def describe_firewall_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns the data objects for the specified firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn Returns: CommandResults: Formatted results with firewall information """ validate_network_firewall_identifier(args, "firewall") kwargs = {"FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn")} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing firewall with parameters: {kwargs.keys()}") response = client.describe_firewall(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response_outputs = copy.deepcopy(response) firewall = response_outputs.get("Firewall", {}) firewall["FirewallStatus"] = response_outputs.pop("FirewallStatus", {}) firewall["UpdateToken"] = response_outputs.pop("UpdateToken", None) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=firewall, readable_output=tableToMarkdown( "AWS Network Firewall", firewall, headers=["FirewallName", "FirewallArn", "FirewallPolicyArn", "VpcId", "Description", "FirewallId"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def list_firewalls_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the metadata for the firewalls that you have defined. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing: - limit: The maximum number of objects that you want Network Firewall to return for this request. - next_token: When you request a list of objects with a MaxResults setting, if the number of objects that are still available for retrieval exceeds the maximum you requested, Network Firewall returns a NextToken value in the response. - vpc_ids: The unique identifiers of the VPCs that you want to retrieve the firewalls for. Returns: CommandResults: Formatted results with firewalls information """ kwargs = {"VpcIds": argToList(args.get("vpc_ids"))} kwargs.update(build_pagination_kwargs(args, next_token_name="NextToken", limit_name="MaxResults", max_limit=100)) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Listing firewalls with parameters: {kwargs.keys()}") response = client.list_firewalls(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) firewalls = response.get("Firewalls", []) outputs = { "AWS.NetworkFirewall.Firewalls(val.FirewallArn == obj.FirewallArn)": firewalls, "AWS.NetworkFirewall(true)": {"FirewallsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS Network Firewalls", firewalls, headers=["FirewallName", "FirewallArn"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def create_firewall_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates an AWS Network Firewall firewall for your VPC. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): The Command arguments. Returns: CommandResults: Formatted results with firewall information """ vpc_id = args.get("vpc_id") subnet_mappings_raw = args.get("subnet_mappings") transit_gateway_id = args.get("transit_gateway_id") availability_zone_mappings = args.get("availability_zone_mappings") if vpc_id and transit_gateway_id: raise ValueError( "You must provide exactly one of the following pairs 'vpc_id' and 'subnet_mappings' or 'transit_gateway_id' " "and 'availability_zone_mappings'." ) if not vpc_id and not transit_gateway_id: raise ValueError( "You must provide exactly one of the following pairs 'vpc_id' and 'subnet_mappings' or 'transit_gateway_id' " "and 'availability_zone_mappings'." ) if vpc_id and not subnet_mappings_raw: raise ValueError("The argument 'subnet_mappings' is required when 'vpc_id' is provided.") if transit_gateway_id and not availability_zone_mappings: raise ValueError("The argument 'availability_zone_mappings' is required when 'transit_gateway_id' is provided.") subnet_mappings = None if subnet_mappings_raw: try: subnet_mappings = json.loads(subnet_mappings_raw) except json.JSONDecodeError as e: raise ValueError(f"subnet_mappings must be a valid JSON string: {e}") from e kwargs = remove_empty_elements( { "FirewallName": args.get("firewall_name"), "FirewallPolicyArn": args.get("firewall_policy_arn"), "VpcId": vpc_id, "DeleteProtection": arg_to_bool_or_none(args.get("delete_protection")), "SubnetChangeProtection": arg_to_bool_or_none(args.get("subnet_change_protection")), "FirewallPolicyChangeProtection": arg_to_bool_or_none(args.get("firewall_policy_change_protection")), "Description": args.get("description"), "SubnetMappings": subnet_mappings, "Tags": parse_tag_field(args.get("tags", "")), "EncryptionConfiguration": { "KeyId": args.get("encryption_config_id"), "Type": args.get("encryption_config_type"), }, "EnabledAnalysisTypes": argToList(args.get("enabled_analysis_types")), "TransitGatewayId": transit_gateway_id, "AvailabilityZoneMappings": [ {"AvailabilityZone": az} for az in argToList(args.get("availability_zone_mappings")) ], "AvailabilityZoneChangeProtection": arg_to_bool_or_none(args.get("availability_zone_change_protection")), } ) print_debug_logs(client, f"Creating firewall with parameters: {kwargs.keys()}") response = client.create_firewall(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response_outputs = copy.deepcopy(response) firewall = response_outputs.get("Firewall", {}) firewall["FirewallStatus"] = response_outputs.pop("FirewallStatus", {}) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=firewall, readable_output=tableToMarkdown( "AWS Network Firewall", firewall, headers=["FirewallName", "FirewallArn", "FirewallPolicyArn", "VpcId", "Description", "FirewallId"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def delete_firewall_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified firewall and its firewall policy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn Returns: CommandResults: Formatted results with firewall information """ validate_network_firewall_identifier(args, "firewall") kwargs = { "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Deleting firewall with parameters: {kwargs.keys()}") response = client.delete_firewall(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The command was executed successfully. The current firewall status is " f"{response.get('FirewallStatus', {}).get('Status')}.", raw_response=response, ) @staticmethod def update_firewall_delete_protection_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the flag, DeleteProtection, which indicates whether it is possible to delete the firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, and delete_protection Returns: CommandResults: Formatted results with firewall information """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "DeleteProtection": arg_to_bool_or_none(args.get("delete_protection")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Updating firewall delete protection with parameters: {kwargs.keys()}") response = client.update_firewall_delete_protection(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output="The delete protection flag of the firewall was updated successfully.", raw_response=response, ) @staticmethod def update_subnet_change_protection_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the flag, SubnetChangeProtection, which indicates whether it is possible to change the subnets that the firewall is associated with. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, subnet_change_protection, and an optional update_token Returns: CommandResults: Formatted results with a success message """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "SubnetChangeProtection": arg_to_bool_or_none(args.get("subnet_change_protection")), } remove_nulls_from_dictionary(kwargs) print_debug_logs( client, f"Updating firewall subnet change protection with parameters: " f"{kwargs.keys()} and {kwargs.get('SubnetChangeProtection')=}", ) response = client.update_subnet_change_protection(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output="The subnet change protection flag of the firewall was updated successfully.", raw_response=response, ) @staticmethod def update_firewall_description_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the description for the specified firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, and description Returns: CommandResults: Formatted results with firewall information """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "Description": args.get("description"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Updating firewall description with parameters: {kwargs.keys()}") response = client.update_firewall_description(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output="The firewall description was updated successfully.", raw_response=response, ) @staticmethod def list_firewall_policies_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the metadata for the firewall policies that you have defined. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing: - limit: The maximum number of objects that you want Network Firewall to return for this request. - next_token: When you request a list of objects with a MaxResults setting, if the number of objects that are still available for retrieval exceeds the maximum you requested, Network Firewall returns a NextToken value in the response. Returns: CommandResults: Formatted results with firewall policies information """ kwargs = build_pagination_kwargs(args, next_token_name="NextToken", limit_name="MaxResults", max_limit=100) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Listing firewall policies with parameters: {kwargs}") response = client.list_firewall_policies(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) firewall_policies = response.get("FirewallPolicies", []) updated_firewall_policies = [ {"FirewallPolicyName": policy.get("Name"), "FirewallPolicyArn": policy.get("Arn")} for policy in firewall_policies ] outputs = { "AWS.NetworkFirewall.FirewallPolicies(val.FirewallPolicyArn == obj.FirewallPolicyArn)": updated_firewall_policies, "AWS.NetworkFirewall(true)": {"FirewallPoliciesNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS Network Firewall Policies", updated_firewall_policies, headers=["FirewallPolicyName", "FirewallPolicyArn"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def describe_firewall_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns the data objects for the specified firewall policy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_policy_name or firewall_policy_arn Returns: CommandResults: Formatted results with firewall policy information """ validate_network_firewall_identifier(args, "firewall_policy") kwargs = {"FirewallPolicyName": args.get("firewall_policy_name"), "FirewallPolicyArn": args.get("firewall_policy_arn")} remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing firewall policy with parameters: {kwargs}") response = client.describe_firewall_policy(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) raw_response = copy.deepcopy(response) firewall_policy_response = response.get("FirewallPolicyResponse", {}) firewall_policy_response["UpdateToken"] = response.get("UpdateToken") firewall_policy_response.update(response.get("FirewallPolicy", {})) return CommandResults( outputs_prefix="AWS.NetworkFirewall.FirewallPolicies", outputs_key_field="FirewallPolicyArn", outputs=firewall_policy_response, readable_output=tableToMarkdown( "AWS Network Firewall Policy", firewall_policy_response, headers=["FirewallPolicyName", "FirewallPolicyArn", "Description", "FirewallPolicyStatus"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=raw_response, ) @staticmethod def create_firewall_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates the firewall policy for the firewall according to the specifications. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing the firewall policy name, the stateless and stateful rule group references and default actions, the stateful engine options, the TLS inspection configuration ARN, the policy rule variables, a description, tags, and the encryption configuration. Returns: CommandResults: Formatted results with firewall policy information """ firewall_policy = create_network_firewall_policy_obj(args) encryption_configuration = remove_empty_elements( { "KeyId": args.get("encryption_configuration_key_id"), "Type": args.get("encryption_configuration_key_type"), } ) kwargs = remove_empty_elements( { "FirewallPolicyName": args.get("firewall_policy_name"), "FirewallPolicy": firewall_policy, "Description": args.get("description"), "Tags": parse_tag_field(args.get("tags", "")), "EncryptionConfiguration": encryption_configuration, } ) print_debug_logs(client, f"Creating firewall policy with parameters: {kwargs}") response = client.create_firewall_policy(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) raw_response = copy.deepcopy(response) firewall_policy_response = response.get("FirewallPolicyResponse", {}) firewall_policy_response["UpdateToken"] = response.get("UpdateToken") return CommandResults( outputs_prefix="AWS.NetworkFirewall.FirewallPolicies", outputs_key_field="FirewallPolicyArn", outputs=firewall_policy_response, readable_output=tableToMarkdown( "AWS Network Firewall Policy", firewall_policy_response, headers=["FirewallPolicyName", "FirewallPolicyArn", "Description", "FirewallPolicyStatus"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=raw_response, ) @staticmethod def associate_firewall_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Associates a FirewallPolicy to a Firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, firewall_policy_arn, and optionally update_token Returns: CommandResults: Formatted results with firewall policy association information """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "FirewallPolicyArn": args.get("firewall_policy_arn"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Associating firewall policy with parameters: {kwargs}") response = client.associate_firewall_policy(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = copy.deepcopy(response) outputs.pop("ResponseMetadata", None) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=outputs, readable_output="The firewall policy was associated with the firewall successfully.", raw_response=response, ) @staticmethod def delete_firewall_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified FirewallPolicy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_policy_name or firewall_policy_arn Returns: CommandResults: Formatted results with firewall policy information """ validate_network_firewall_identifier(args, "firewall_policy") kwargs = { "FirewallPolicyName": args.get("firewall_policy_name"), "FirewallPolicyArn": args.get("firewall_policy_arn"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Deleting firewall policy with parameters: {kwargs}") response = client.delete_firewall_policy(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The command was executed successfully. The current firewall policy status is " f"{response.get('FirewallPolicyResponse', {}).get('FirewallPolicyStatus')}.", raw_response=response, ) @staticmethod def update_firewall_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Updates the properties of the specified firewall policy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing the update token, the firewall policy name or ARN, the stateless and stateful rule group references and default actions, the stateful engine options, the TLS inspection configuration ARN, the policy rule variables, a description, the encryption configuration, and a dry run flag. Returns: CommandResults: Formatted results with firewall policy information """ validate_network_firewall_identifier(args, "firewall_policy") firewall_policy = create_network_firewall_policy_obj(args) kwargs = remove_empty_elements( { "UpdateToken": args.get("update_token"), "FirewallPolicyArn": args.get("firewall_policy_arn"), "FirewallPolicyName": args.get("firewall_policy_name"), "FirewallPolicy": firewall_policy, "Description": args.get("description"), "EncryptionConfiguration": { "KeyId": args.get("encryption_configuration_key_id"), "Type": args.get("encryption_configuration_key_type"), }, } ) print_debug_logs(client, f"Updating firewall policy with parameters: {kwargs}") response = client.update_firewall_policy(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output="The firewall policy was updated successfully.", raw_response=response, ) @staticmethod def update_firewall_policy_change_protection_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Modifies the flag, ChangeProtection, which indicates whether it is possible to change the firewall policy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_policy_name or firewall_policy_arn, and firewall_policy_change_protection Returns: CommandResults: Formatted results with firewall policy information """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "FirewallPolicyChangeProtection": arg_to_bool_or_none(args.get("firewall_policy_change_protection")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Updating firewall policy change protection with parameters: {kwargs}") response = client.update_firewall_policy_change_protection(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output="The change protection flag of the firewall was updated successfully.", raw_response=response, ) @staticmethod def create_rule_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates the specified stateless or stateful rule group, which includes the rules for network traffic inspection, a capacity setting, and tags. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing the rule group name, type, capacity, a JSON rule group object or a Suricata-compatible rules string, a description, tags, and the encryption configuration. Returns: CommandResults: Formatted results with rule group information """ kwargs = create_rule_group_common_kwargs(args) kwargs["Capacity"] = arg_to_number(args.get("capacity")) kwargs["Tags"] = parse_tag_field(args.get("tags", "")) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Creating rule group with parameters: {kwargs.keys()}") response = client.create_rule_group(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) raw_response = copy.deepcopy(response) rule_group_response = response.get("RuleGroupResponse", {}) rule_group_response["UpdateToken"] = response.get("UpdateToken") return CommandResults( outputs_prefix="AWS.NetworkFirewall.RuleGroups", outputs_key_field="RuleGroupArn", outputs=rule_group_response, readable_output=tableToMarkdown( "AWS Network Firewall Rule Group", rule_group_response, headers=["RuleGroupName", "RuleGroupArn", "Type", "Capacity", "Description", "RuleGroupStatus"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=raw_response, ) @staticmethod def delete_rule_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes the specified rule group. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing rule_group_name or rule_group_arn, and type Returns: CommandResults: Formatted results with the rule group deletion status """ validate_network_firewall_identifier(args, "rule_group") kwargs = { "RuleGroupName": args.get("rule_group_name"), "RuleGroupArn": args.get("rule_group_arn"), "Type": args.get("type"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Deleting rule group with parameters: {kwargs.keys()}") response = client.delete_rule_group(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The command was executed successfully. The current rule group status is " f"{response.get('RuleGroupResponse', {}).get('RuleGroupStatus')}.", raw_response=response, ) @staticmethod def describe_rule_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns the data objects for the specified rule group. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing rule_group_name or rule_group_arn, type, and optionally analyze_rule_group Returns: CommandResults: Formatted results with rule group information """ validate_network_firewall_identifier(args, "rule_group") kwargs = { "RuleGroupName": args.get("rule_group_name"), "RuleGroupArn": args.get("rule_group_arn"), "Type": args.get("type"), "AnalyzeRuleGroup": arg_to_bool_or_none(args.get("analyze_rule_group")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing rule group with parameters: {kwargs.keys()}") response = client.describe_rule_group(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) raw_response = copy.deepcopy(response) rule_group_response = response.get("RuleGroupResponse", {}) rule_group_response["UpdateToken"] = response.get("UpdateToken") rule_group_response.update(response.get("RuleGroup", {})) return CommandResults( outputs_prefix="AWS.NetworkFirewall.RuleGroups", outputs_key_field="RuleGroupArn", outputs=rule_group_response, readable_output=tableToMarkdown( "AWS Network Firewall Rule Group", rule_group_response, headers=["RuleGroupName", "RuleGroupArn", "Type", "Capacity", "Description", "RuleGroupStatus"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=raw_response, ) @staticmethod def list_rule_groups_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the metadata for the rule groups that you have defined. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing: - limit: The maximum number of objects that you want Network Firewall to return for this request. - next_token: When you request a list of objects with a MaxResults setting, if the number of objects that are still available for retrieval exceeds the maximum you requested, Network Firewall returns a NextToken value in the response. - scope: The scope of the request. Valid settings are MANAGED and ACCOUNT. - managed_type: Indicates the general category of the Amazon Web Services managed rule group. - type: Indicates whether the rule group is stateless or stateful. Returns: CommandResults: Formatted results with rule groups information """ kwargs = { "Scope": args.get("scope"), "ManagedType": args.get("managed_type"), "Type": args.get("type"), } kwargs.update(build_pagination_kwargs(args, next_token_name="NextToken", limit_name="MaxResults", max_limit=100)) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Listing rule groups with parameters: {list(kwargs.keys())}") response = client.list_rule_groups(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) rule_groups = response.get("RuleGroups", []) updated_rule_groups = [ { "RuleGroupName": rule_group.get("Name"), "RuleGroupArn": rule_group.get("Arn"), "VendorName": rule_group.get("VendorName"), } for rule_group in rule_groups ] outputs = { "AWS.NetworkFirewall.RuleGroups(val.RuleGroupArn == obj.RuleGroupArn)": updated_rule_groups, "AWS.NetworkFirewall(true)": {"RuleGroupsNextToken": response.get("NextToken")}, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS Network Firewall Rule Groups", updated_rule_groups, headers=["RuleGroupName", "RuleGroupArn", "VendorName"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def update_rule_group_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Updates the rule settings for the specified rule group. You use a rule group by reference in one or more firewall policies. When you modify a rule group, you modify all firewall policies that use the rule group. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing the update token, the rule group name or ARN, type, a JSON rule group object or a Suricata-compatible rules string, a description, the encryption configuration, the source metadata, and an analyze rule group flag. Returns: CommandResults: Formatted results with rule group information """ validate_network_firewall_identifier(args, "rule_group") kwargs = create_rule_group_common_kwargs(args) kwargs["UpdateToken"] = args.get("update_token") kwargs["RuleGroupArn"] = args.get("rule_group_arn") remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Updating rule group with parameters: {kwargs.keys()}") response = client.update_rule_group(**kwargs) response = serialize_response_with_datetime_encoding(response) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) raw_response = copy.deepcopy(response) rule_group_response = response.get("RuleGroupResponse", {}) rule_group_response["UpdateToken"] = response.get("UpdateToken") return CommandResults( outputs_prefix="AWS.NetworkFirewall.RuleGroups", outputs_key_field="RuleGroupArn", outputs=rule_group_response, readable_output=tableToMarkdown( "AWS Network Firewall Rule Group", rule_group_response, headers=["RuleGroupName", "RuleGroupArn", "Type", "Capacity", "Description", "RuleGroupStatus"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=raw_response, ) @staticmethod def associate_subnets_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Associates the specified subnets in the Amazon VPC to the firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, subnet_mappings, and an optional update_token Returns: CommandResults: Formatted results with the firewall subnet mappings """ validate_network_firewall_identifier(args, "firewall") subnet_mappings = parse_subnet_mappings_field(args.get("subnet_mappings")) kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "SubnetMappings": subnet_mappings, } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Associating subnets with parameters: {kwargs.keys()} and {subnet_mappings=}") response = client.associate_subnets(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response_outputs = copy.deepcopy(response) response_outputs.pop("ResponseMetadata", None) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=response_outputs, readable_output=tableToMarkdown( "AWS Network Firewall Associate Subnets", response_outputs.get("SubnetMappings", []), removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def disassociate_subnets_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Removes the specified subnet associations from the firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, subnet_ids, and an optional update_token Returns: CommandResults: Formatted results with the remaining subnet mappings """ validate_network_firewall_identifier(args, "firewall") kwargs = { "UpdateToken": args.get("update_token"), "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "SubnetIds": argToList(args.get("subnet_ids")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Disassociating subnets with parameters: {kwargs.keys()}") response = client.disassociate_subnets(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) response_outputs = copy.deepcopy(response) response_outputs.pop("ResponseMetadata", None) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=response_outputs, readable_output=tableToMarkdown("AWS Network Firewall Subnets Disassociated Successfully", []), raw_response=response, ) @staticmethod def delete_resource_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Deletes a resource policy that you created in a put_resource_policy request. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing resource_arn Returns: CommandResults: Formatted results with the operation status """ print_debug_logs(client, f"Deleting resource policy for resource: {args.get('resource_arn')}") response = client.delete_resource_policy(ResourceArn=args.get("resource_arn")) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The resource policy for {args.get('resource_arn')} was deleted successfully.", raw_response=response, ) @staticmethod def put_resource_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Creates or updates an AWS Identity and Access Management policy for your rule group or firewall policy. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing resource_arn and policy Returns: CommandResults: Formatted results with the operation status """ kwargs = { "ResourceArn": args.get("resource_arn"), "Policy": args.get("policy"), } print_debug_logs(client, f"Putting resource policy with parameters: {kwargs.keys()}") response = client.put_resource_policy(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The resource policy for {args.get('resource_arn')} was created/updated successfully.", raw_response=response, ) @staticmethod def describe_resource_policy_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves a resource policy that you created in a put_resource_policy request. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing resource_arn Returns: CommandResults: Formatted results with the resource policy """ print_debug_logs(client, f"Describing resource policy for resource: {args.get('resource_arn')}") response = client.describe_resource_policy(ResourceArn=args.get("resource_arn")) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = {"ResourceArn": args.get("resource_arn"), "Policy": response.get("Policy")} return CommandResults( outputs_prefix="AWS.NetworkFirewall.ResourcePolicies", outputs_key_field="ResourceArn", outputs=outputs, readable_output=tableToMarkdown( "AWS Network Firewall Resource Policy", outputs, headers=["ResourceArn", "Policy"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def list_tags_for_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Retrieves the tags associated with the specified resource. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing: - resource_arn: The Amazon Resource Name (ARN) of the resource. - limit: The maximum number of objects that you want Network Firewall to return for this request. - next_token: The pagination token from a previous request. Returns: CommandResults: Formatted results with the resource tags """ kwargs = {"ResourceArn": args.get("resource_arn")} kwargs.update(build_pagination_kwargs(args, next_token_name="NextToken", limit_name="MaxResults", max_limit=100)) remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Listing tags for resource with parameters: {kwargs.keys()}") response = client.list_tags_for_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) tags = response.get("Tags", []) if not tags: return CommandResults(readable_output="No tags were found.") outputs = { "AWS.NetworkFirewall.Tags(val.ResourceArn == obj.ResourceArn)": { "ResourceArn": args.get("resource_arn"), "Tags": tags, "TagsNextToken": response.get("NextToken"), }, } return CommandResults( outputs=outputs, readable_output=tableToMarkdown( "AWS Network Firewall Tags", tags, headers=["Key", "Value"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def tag_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Adds the specified tags to the specified resource. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing resource_arn and tags Returns: CommandResults: Formatted results with the operation status """ kwargs = { "ResourceArn": args.get("resource_arn"), "Tags": parse_tag_field(args.get("tags")), } print_debug_logs(client, f"Tagging resource with parameters: {kwargs.keys()}") response = client.tag_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The resource {args.get('resource_arn')} was tagged successfully.", raw_response=response, ) @staticmethod def untag_resource_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Removes the tags with the specified keys from the specified resource. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing resource_arn and tag_keys Returns: CommandResults: Formatted results with the operation status """ kwargs = { "ResourceArn": args.get("resource_arn"), "TagKeys": argToList(args.get("tag_keys")), } print_debug_logs(client, f"Untagging resource with parameters: {kwargs.keys()}") response = client.untag_resource(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) return CommandResults( readable_output=f"The tags were removed from the resource {args.get('resource_arn')} successfully.", raw_response=response, ) @staticmethod def describe_logging_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Returns the logging configuration for the specified firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn Returns: CommandResults: Formatted results with the logging configuration """ validate_network_firewall_identifier(args, "firewall") kwargs = { "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Describing logging configuration with parameters: {kwargs.keys()}") response = client.describe_logging_configuration(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = { "FirewallArn": response.get("FirewallArn"), "LoggingConfiguration": response.get("LoggingConfiguration", {}), "EnableMonitoringDashboard": response.get("EnableMonitoringDashboard"), } remove_nulls_from_dictionary(outputs) log_destination_configs = outputs.get("LoggingConfiguration", {}).get("LogDestinationConfigs", []) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=outputs, readable_output=tableToMarkdown( f"AWS Network Firewall Logging Configuration for the Firewall {outputs.get('FirewallArn')}", log_destination_configs, headers=["LogType", "LogDestinationType", "LogDestination"], removeNull=True, headerTransform=pascalToSpace, ), raw_response=response, ) @staticmethod def update_logging_configuration_command(client: BotoClient, args: Dict[str, Any]) -> CommandResults | None: """ Sets the logging configuration for the specified firewall. Args: client (BotoClient): The boto3 client for NetworkFirewall service args (Dict[str, Any]): Command arguments containing firewall_name or firewall_arn, log_type, log_destination_type, log_destination_key, log_destination_value, and enable_monitoring_dashboard Returns: CommandResults: Formatted results with the logging configuration """ validate_network_firewall_identifier(args, "firewall") log_type = args.get("log_type") log_destination_type = args.get("log_destination_type") log_destination_key = args.get("log_destination_key") log_destination_value = args.get("log_destination_value") logging_configuration = None if any((log_type, log_destination_type, log_destination_key, log_destination_value)): if not all((log_type, log_destination_type, log_destination_key, log_destination_value)): raise ValueError( "To set a logging destination you must provide all of the following arguments: " "log_type, log_destination_type, log_destination_key, log_destination_value." ) logging_configuration = { "LogDestinationConfigs": [ { "LogType": log_type, "LogDestinationType": log_destination_type, "LogDestination": {log_destination_key: log_destination_value}, } ] } kwargs = { "FirewallName": args.get("firewall_name"), "FirewallArn": args.get("firewall_arn"), "LoggingConfiguration": logging_configuration, "EnableMonitoringDashboard": arg_to_bool_or_none(args.get("enable_monitoring_dashboard")), } remove_nulls_from_dictionary(kwargs) print_debug_logs(client, f"Updating logging configuration with parameters: {kwargs.keys()}") response = client.update_logging_configuration(**kwargs) if response.get("ResponseMetadata", {}).get("HTTPStatusCode") != HTTPStatus.OK: return AWSErrorHandler.handle_response_error(response, args.get("account_id")) outputs = { "FirewallArn": response.get("FirewallArn"), "FirewallName": response.get("FirewallName"), "LoggingConfiguration": response.get("LoggingConfiguration", {}), "EnableMonitoringDashboard": response.get("EnableMonitoringDashboard"), } remove_nulls_from_dictionary(outputs) return CommandResults( outputs_prefix="AWS.NetworkFirewall.Firewalls", outputs_key_field="FirewallArn", outputs=outputs, readable_output="The logging configuration was updated successfully.", raw_response=response, ) def get_file_path(file_id): filepath_result = demisto.getFilePath(file_id) return filepath_result COMMANDS_MAPPING: dict[str, Callable] = { "aws-billing-cost-usage-list": CostExplorer.billing_cost_usage_list_command, "aws-billing-forecast-list": CostExplorer.billing_forecast_list_command, "aws-billing-budgets-list": Budgets.billing_budgets_list_command, "aws-billing-budget-notification-list": Budgets.billing_budget_notification_list_command, "aws-s3-public-access-block-update": S3.put_public_access_block_command, "aws-s3-public-access-block-quick-action": S3.put_public_access_block_command, "aws-s3-bucket-delete": S3.delete_bucket_command, "aws-s3-bucket-objects-list": S3.list_bucket_objects_command, "aws-s3-bucket-objects-list-v2": S3.list_bucket_objects_v2_command, "aws-s3-bucket-versioning-put": S3.put_bucket_versioning_command, "aws-s3-bucket-versioning-enable-quick-action": S3.put_bucket_versioning_command, "aws-s3-bucket-logging-put": S3.put_bucket_logging_command, "aws-s3-bucket-enable-bucket-access-logging-quick-action": S3.put_bucket_logging_command, "aws-s3-bucket-acl-put": S3.put_bucket_acl_command, "aws-s3-bucket-acl-set-to-private-quick-action": S3.put_bucket_acl_command, "aws-s3-bucket-policy-put": S3.put_bucket_policy_command, "aws-s3-bucket-policy-put-quick-action": S3.put_bucket_policy_command, "aws-s3-bucket-website-delete": S3.delete_bucket_website_command, "aws-s3-bucket-website-disable-hosting-quick-action": S3.delete_bucket_website_command, "aws-s3-bucket-ownership-controls-put": S3.put_bucket_ownership_controls_command, "aws-s3-bucket-ownership-controls-put-quick-action": S3.put_bucket_ownership_controls_command, "aws-s3-file-upload": S3.file_upload_command, "aws-s3-file-download": S3.file_download_command, "aws-s3-bucket-website-get": S3.get_bucket_website_command, "aws-s3-bucket-acl-get": S3.get_bucket_acl_command, "aws-s3-bucket-create": S3.bucket_create_command, "aws-s3-buckets-list": S3.buckets_list_command, "aws-iam-account-password-policy-get": IAM.get_account_password_policy_command, "aws-iam-account-password-policy-update": IAM.update_account_password_policy_command, "aws-iam-role-policy-put": IAM.put_role_policy_command, "aws-iam-login-profile-delete": IAM.delete_login_profile_command, "aws-iam-user-policy-put": IAM.put_user_policy_command, "aws-iam-role-from-instance-profile-remove": IAM.remove_role_from_instance_profile_command, "aws-iam-access-key-update": IAM.update_access_key_command, "aws-ec2-instance-metadata-options-modify": EC2.modify_instance_metadata_options_command, "aws-ec2-enable-imdsv2-quick-action": EC2.modify_instance_metadata_options_command, "aws-ec2-instance-attribute-modify": EC2.modify_instance_attribute_command, "aws-ec2-instance-attribute-modify-quick-action": EC2.modify_instance_attribute_command, "aws-ec2-snapshot-attribute-modify": EC2.modify_snapshot_attribute_command, "aws-ec2-image-attribute-modify": EC2.modify_image_attribute_command, "aws-ec2-image-attribute-set-ami-to-private-quick-action": EC2.modify_image_attribute_command, "aws-ec2-security-group-ingress-revoke": EC2.revoke_security_group_ingress_command, "aws-ec2-security-group-ingress-authorize": EC2.authorize_security_group_ingress_command, "aws-ec2-security-group-egress-revoke": EC2.revoke_security_group_egress_command, "aws-ec2-snapshot-create": EC2.create_snapshot_command, "aws-ec2-snapshot-permission-modify": EC2.modify_snapshot_permission_command, "aws-ec2-subnet-attribute-modify": EC2.modify_subnet_attribute_command, "aws-ec2-vpcs-describe": EC2.describe_vpcs_command, "aws-ec2-subnets-describe": EC2.describe_subnets_command, "aws-ec2-ipam-resource-discoveries-describe": EC2.describe_ipam_resource_discoveries_command, "aws-ec2-ipam-resource-discovery-associations-describe": EC2.describe_ipam_resource_discovery_associations_command, "aws-ec2-set-snapshot-to-private-quick-action": EC2.modify_snapshot_permission_command, "aws-ec2-latest-ami-get": EC2.get_latest_ami_command, "aws-ec2-network-acl-create": EC2.create_network_acl_command, "aws-ec2-ipam-discovered-public-addresses-get": EC2.get_ipam_discovered_public_addresses_command, "aws-ec2-security-group-create": EC2.create_security_group_command, "aws-ec2-security-group-delete": EC2.delete_security_group_command, "aws-ec2-security-groups-describe": EC2.describe_security_groups_command, "aws-ec2-security-group-egress-authorize": EC2.authorize_security_group_egress_command, "aws-eks-clusters-list": EKS.list_clusters_command, "aws-eks-access-entry-create": EKS.create_access_entry_command, "aws-eks-access-entry-update": EKS.update_access_entry_command, "aws-ec2-images-describe": EC2.describe_images_command, "aws-ec2-image-create": EC2.create_image_command, "aws-ec2-image-deregister": EC2.deregister_image_command, "aws-ec2-image-copy": EC2.copy_image_command, "aws-ec2-image-available-waiter": EC2.image_available_waiter_command, "aws-ec2-instances-monitor": EC2.monitor_instances_command, "aws-ec2-instances-unmonitor": EC2.unmonitor_instances_command, "aws-ec2-instances-reboot": EC2.reboot_instances_command, "aws-ec2-instance-running-waiter": EC2.instance_running_waiter_command, "aws-ec2-instance-status-ok-waiter": EC2.instance_status_ok_waiter_command, "aws-ec2-instance-stopped-waiter": EC2.instance_stopped_waiter_command, "aws-ec2-instance-terminated-waiter": EC2.instance_terminated_waiter_command, "aws-ec2-iam-instance-profile-associations-describe": EC2.describe_iam_instance_profile_associations_command, "aws-ec2-password-data-get": EC2.get_password_data_command, "aws-ec2-reserved-instances-describe": EC2.describe_reserved_instances_command, "aws-ec2-snapshots-describe": EC2.describe_snapshots_command, "aws-ec2-snapshot-delete": EC2.delete_snapshot_command, "aws-ec2-snapshot-copy": EC2.copy_snapshot_command, "aws-ec2-snapshot-completed-waiter": EC2.snapshot_completed_waiter_command, "aws-eks-cluster-config-update": EKS.update_cluster_config_command, "aws-eks-enable-control-plane-logging-quick-action": EKS.update_cluster_config_command, "aws-eks-disable-public-access-quick-action": EKS.update_cluster_config_command, "aws-eks-cluster-describe": EKS.describe_cluster_command, "aws-eks-access-policy-associate": EKS.associate_access_policy_command, "aws-rds-db-cluster-modify": RDS.modify_db_cluster_command, "aws-rds-db-cluster-enable-iam-auth-quick-action": RDS.modify_db_cluster_command, "aws-rds-db-cluster-enable-deletion-protection-quick-action": RDS.modify_db_cluster_command, "aws-rds-db-cluster-snapshot-attribute-modify": RDS.modify_db_cluster_snapshot_attribute_command, "aws-rds-db-cluster-snapshot-set-to-private-quick-action": RDS.modify_db_cluster_snapshot_attribute_command, "aws-rds-db-instance-modify": RDS.modify_db_instance_command, "aws-rds-db-instance-modify-publicly-accessible-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-modify-copy-tags-on-rds-snapshot-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-modify-enable-automatic-backup-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-enable-iam-auth-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-enable-deletion-protection-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-enable-auto-upgrade-quick-action": RDS.modify_db_instance_command, "aws-rds-db-instance-enable-multi-az-quick-action": RDS.modify_db_instance_command, "aws-rds-db-snapshot-attribute-modify": RDS.modify_db_snapshot_attribute_command, "aws-rds-event-subscription-modify": RDS.modify_event_subscription_command, "aws-rds-event-subscription-modify-quick-action": RDS.modify_event_subscription_command, "aws-rds-db-snapshot-attribute-set-snapshot-to-private-quick-action": RDS.modify_db_snapshot_attribute_command, "aws-rds-db-instances-describe": RDS.describe_db_instances_command, "aws-cloudtrail-logging-start": CloudTrail.start_logging_command, "aws-cloudtrail-logging-start-enable-logging-quick-action": CloudTrail.start_logging_command, "aws-cloudtrail-trail-update": CloudTrail.update_trail_command, "aws-cloudtrail-trail-enable-log-validation-quick-action": CloudTrail.update_trail_command, "aws-ec2-instances-describe": EC2.describe_instances_command, "aws-ec2-instances-start": EC2.start_instances_command, "aws-ec2-instances-stop": EC2.stop_instances_command, "aws-ec2-instances-terminate": EC2.terminate_instances_command, "aws-ec2-instances-run": EC2.run_instances_command, "aws-ec2-tags-create": EC2.create_tags_command, "aws-ec2-regions-describe": EC2.regions_describe_command, "aws-ec2-network-interface-attribute-modify": EC2.network_interface_attribute_modify_command, "aws-s3-bucket-policy-delete": S3.delete_bucket_policy_command, "aws-s3-public-access-block-get": S3.get_public_access_block_command, "aws-s3-bucket-encryption-get": S3.get_bucket_encryption_command, "aws-s3-bucket-policy-get": S3.get_bucket_policy_command, "aws-cloudtrail-trails-describe": CloudTrail.describe_trails_command, "aws-acm-certificate-options-update": ACM.update_certificate_options_command, "aws-ecs-cluster-settings-update": ECS.update_cluster_settings_command, "aws-lambda-function-configuration-get": Lambda.get_function_configuration_command, "aws-lambda-function-url-config-get": Lambda.get_function_url_configuration_command, "aws-lambda-policy-get": Lambda.get_policy_command, "aws-lambda-invoke": Lambda.invoke_command, "aws-lambda-function-url-config-update": Lambda.update_function_url_configuration_command, "aws-lambda-function-configuration-update": Lambda.update_function_configuration_command, "aws-lambda-function-get": Lambda.get_function_command, "aws-lambda-functions-list": Lambda.list_functions_command, "aws-lambda-aliases-list": Lambda.list_aliases_command, "aws-lambda-account-settings-get": Lambda.get_account_settings_command, "aws-lambda-function-versions-list": Lambda.list_versions_by_function_command, "aws-lambda-function-url-config-delete": Lambda.delete_function_url_config_command, "aws-lambda-function-create": Lambda.create_function_command, "aws-lambda-layer-version-list": Lambda.list_layer_versions_command, "aws-lambda-function-delete": Lambda.delete_function_command, "aws-lambda-layer-version-delete": Lambda.delete_layer_version_command, "aws-lambda-layer-version-publish": Lambda.publish_layer_version_command, "aws-kms-key-rotation-enable": KMS.enable_key_rotation_command, "aws-elb-load-balancer-attributes-modify": ELB.modify_load_balancer_attributes_command, "aws-ec2-addresses-describe": EC2.describe_addresses_command, "aws-ec2-address-allocate": EC2.allocate_address_command, "aws-ec2-address-associate": EC2.associate_address_command, "aws-ec2-address-disassociate": EC2.disassociate_address_command, "aws-ec2-address-release": EC2.release_address_command, "aws-ec2-volumes-describe": EC2.describe_volumes_command, "aws-ec2-volume-modify": EC2.modify_volume_command, "aws-ec2-volume-create": EC2.create_volume_command, "aws-ec2-volume-attach": EC2.attach_volume_command, "aws-ec2-volume-detach": EC2.detach_volume_command, "aws-ec2-volume-delete": EC2.delete_volume_command, "aws-ec2-launch-templates-describe": EC2.describe_launch_templates_command, "aws-ec2-launch-template-create": EC2.create_launch_template_command, "aws-ec2-launch-template-delete": EC2.delete_launch_template_command, "aws-ec2-fleet-create": EC2.create_fleet_command, "aws-ec2-fleet-delete": EC2.delete_fleet_command, "aws-ec2-fleets-describe": EC2.describe_fleets_command, "aws-ec2-fleet-instances-describe": EC2.describe_fleet_instances_command, "aws-ec2-fleet-modify": EC2.modify_fleet_command, "aws-ssm-association-versions-list": SSM.association_versions_list_command, "aws-ssm-association-get": SSM.association_get_command, "aws-ssm-associations-list": SSM.associations_list_command, "aws-ssm-inventory-list": SSM.inventory_list_command, "aws-ssm-inventory-entries-list": SSM.inventory_entries_list_command, "aws-ssm-command-run": SSM.command_run_command, "aws-redshift-cluster-modify": Redshift.modify_cluster_command, "aws-ssm-tag-add": SSM.add_tags_to_resource_command, "aws-ssm-tag-remove": SSM.remove_tags_from_resource_command, "aws-ssm-documents-list": SSM.documents_list_command, "aws-ssm-document-describe": SSM.document_describe_command, "aws-ssm-automation-executions-list": SSM.automation_execution_list_command, "aws-ssm-automation-execution-run": SSM.automation_execution_run_command, "aws-ssm-automation-execution-cancel": SSM.automation_execution_cancel_command, "aws-ssm-commands-list": SSM.command_list_command, "aws-ssm-command-cancel": SSM.command_cancel_command, "aws-ssm-tags-list": SSM.list_tags_for_resource_command, "aws-ec2-vpc-delete": EC2.delete_vpc_command, "aws-ec2-vpc-endpoint-create": EC2.create_vpc_endpoint_command, "aws-ec2-internet-gateway-describe": EC2.describe_internet_gateways_command, "aws-ec2-internet-gateway-detach": EC2.detach_internet_gateway_command, "aws-ec2-internet-gateway-delete": EC2.delete_internet_gateway_command, "aws-ec2-subnet-delete": EC2.delete_subnet_command, "aws-ec2-network-acl-entry-create": EC2.create_network_acl_entry_command, "aws-ec2-key-pairs-describe": EC2.describe_key_pairs_command, "aws-ec2-hosts-allocate": EC2.allocate_hosts_command, "aws-ec2-hosts-release": EC2.release_hosts_command, "aws-ec2-traffic-mirror-session-create": EC2.create_traffic_mirror_session_command, "aws-logs-log-group-create": CloudWatchLogs.log_group_create_command, "aws-logs-log-stream-create": CloudWatchLogs.log_stream_create_command, "aws-logs-log-group-delete": CloudWatchLogs.log_group_delete_command, "aws-logs-log-stream-delete": CloudWatchLogs.log_stream_delete_command, "aws-logs-log-events-filter": CloudWatchLogs.log_events_filter_command, "aws-logs-log-groups-describe": CloudWatchLogs.log_groups_describe_command, "aws-logs-log-streams-describe": CloudWatchLogs.log_streams_describe_command, "aws-logs-retention-policy-put": CloudWatchLogs.retention_policy_put_command, "aws-logs-retention-policy-delete": CloudWatchLogs.retention_policy_delete_command, "aws-logs-log-event-put": CloudWatchLogs.log_events_put_command, "aws-logs-metric-filter-put": CloudWatchLogs.metric_filter_put_command, "aws-logs-metric-filter-delete": CloudWatchLogs.metric_filter_delete_command, "aws-logs-metric-filters-describe": CloudWatchLogs.metric_filters_describe_command, "aws-network-firewall-firewall-describe": NetworkFirewall.describe_firewall_command, "aws-network-firewall-firewalls-list": NetworkFirewall.list_firewalls_command, "aws-network-firewall-firewall-create": NetworkFirewall.create_firewall_command, "aws-network-firewall-firewall-delete": NetworkFirewall.delete_firewall_command, "aws-network-firewall-firewall-delete-protection-update": NetworkFirewall.update_firewall_delete_protection_command, "aws-network-firewall-firewall-description-update": NetworkFirewall.update_firewall_description_command, "aws-network-firewall-firewall-policy-describe": NetworkFirewall.describe_firewall_policy_command, "aws-network-firewall-firewall-policies-list": NetworkFirewall.list_firewall_policies_command, "aws-network-firewall-firewall-policy-create": NetworkFirewall.create_firewall_policy_command, "aws-network-firewall-firewall-policy-associate": NetworkFirewall.associate_firewall_policy_command, "aws-network-firewall-firewall-policy-delete": NetworkFirewall.delete_firewall_policy_command, "aws-network-firewall-firewall-policy-update": NetworkFirewall.update_firewall_policy_command, "aws-network-firewall-firewall-policy-change-protection-update": NetworkFirewall.update_firewall_policy_change_protection_command, # noqa: E501 "aws-network-firewall-subnet-change-protection-update": NetworkFirewall.update_subnet_change_protection_command, "aws-network-firewall-subnets-associate": NetworkFirewall.associate_subnets_command, "aws-network-firewall-subnets-disassociate": NetworkFirewall.disassociate_subnets_command, "aws-network-firewall-rule-group-create": NetworkFirewall.create_rule_group_command, "aws-network-firewall-rule-group-delete": NetworkFirewall.delete_rule_group_command, "aws-network-firewall-rule-group-describe": NetworkFirewall.describe_rule_group_command, "aws-network-firewall-rule-groups-list": NetworkFirewall.list_rule_groups_command, "aws-network-firewall-rule-group-update": NetworkFirewall.update_rule_group_command, "aws-network-firewall-resource-policy-delete": NetworkFirewall.delete_resource_policy_command, "aws-network-firewall-resource-policy-put": NetworkFirewall.put_resource_policy_command, "aws-network-firewall-resource-policy-describe": NetworkFirewall.describe_resource_policy_command, "aws-network-firewall-tags-for-resource-list": NetworkFirewall.list_tags_for_resource_command, "aws-network-firewall-resource-tag": NetworkFirewall.tag_resource_command, "aws-network-firewall-resource-untag": NetworkFirewall.untag_resource_command, "aws-network-firewall-logging-configuration-describe": NetworkFirewall.describe_logging_configuration_command, "aws-network-firewall-logging-configuration-update": NetworkFirewall.update_logging_configuration_command, } REQUIRED_ACTIONS: list[str] = [ "ssm:ListAssociationVersions", "ssm:DescribeAssociation", "ssm:ListAssociations", "ssm:GetInventory", "ssm:AddTagsToResource", "ssm:RemoveTagsFromResource", "ssm:ListTagsForResource", "ssm:ListDocuments", "ssm:DescribeDocument", "ssm:DescribeAutomationExecutions", "ssm:StartAutomationExecution", "ssm:GetAutomationExecution", "ssm:StopAutomationExecution", "ssm:ListCommands", "ssm:CancelCommand", "kms:CreateGrant", "kms:Decrypt", "kms:DescribeKey", "kms:GenerateDataKey", "kms:EnableKeyRotation", "secretsmanager:CreateSecret", "secretsmanager:RotateSecret", "secretsmanager:TagResource", "rds:AddTagsToResource", "rds:CreateTenantDatabase", "rds:ModifyDBCluster", "rds:DescribeDBInstances", "redshift:ModifyCluster", "rds:ModifyDBClusterSnapshotAttribute", "rds:ModifyDBInstance", "rds:ModifyDBSnapshotAttribute", "s3:CreateBucket", "s3:ListAllMyBuckets", "s3:ListBucket", "s3:PutBucketAcl", "s3:PutBucketLogging", "s3:PutBucketVersioning", "s3:PutBucketPolicy", "s3:PutBucketPublicAccessBlock", "s3:PutObject", "s3:GetObject", "ec2:RevokeSecurityGroupEgress", "ec2:ModifyImageAttribute", "ec2:ModifyInstanceAttribute", "ec2:ModifySnapshotAttribute", "ec2:RevokeSecurityGroupIngress", "ec2:CreateSnapshot", "ec2:DescribeVpcs", "ec2:DescribeSubnets", "ec2:DescribeIpamResourceDiscoveries", "ec2:DescribeIpamResourceDiscoveryAssociations", "ec2:DescribeImages", "ec2:CreateImage", "ec2:DeregisterImage", "ec2:CopyImage", "ec2:DescribeSnapshots", "ec2:DeleteSnapshot", "ec2:CopySnapshot", "ec2:DescribeRegions", "eks:ListClusters", "eks:DescribeCluster", "eks:AssociateAccessPolicy", "eks:CreateAccessEntry", "eks:TagResource", "eks:UpdateAccessEntry", "ec2:CreateSecurityGroup", "ec2:CreateNetworkAcl", "ec2:GetIpamDiscoveredPublicAddresses", "ec2:CreateTags", "ec2:DeleteSecurityGroup", "ec2:DescribeAddresses", "ec2:AllocateAddress", "ec2:AssociateAddress", "ec2:DisassociateAddress", "ec2:ReleaseAddress", "ec2:DescribeInstances", "ec2:DescribeInstanceStatus", "ec2:DescribeSecurityGroups", "ec2:AuthorizeSecurityGroupEgress", "ec2:AuthorizeSecurityGroupIngress", "ec2:ModifyInstanceMetadataOptions", "ec2:MonitorInstances", "ec2:UnmonitorInstances", "ec2:RebootInstances", "ec2:DescribeIamInstanceProfileAssociations", "ec2:GetPasswordData", "ec2:DescribeReservedInstances", "ec2:DescribeInstances", "ec2:StartInstances", "ec2:StopInstances", "ec2:TerminateInstances", "ec2:RunInstances", "ec2:ModifyNetworkInterfaceAttribute", "ec2:CreateFleet", "ec2:DeleteFleets", "ec2:DescribeFleets", "ec2:DescribeFleetInstances", "ec2:ModifyFleet", "eks:UpdateClusterConfig", "iam:PassRole", "iam:DeleteLoginProfile", "iam:PutUserPolicy", "iam:RemoveRoleFromInstanceProfile", "iam:UpdateAccessKey", "iam:GetAccountPasswordPolicy", "iam:UpdateAccountPasswordPolicy", "iam:GetAccountAuthorizationDetails", "ecs:UpdateClusterSettings", "s3:GetBucketPolicy", "s3:GetBucketWebsite", "s3:GetBucketAcl", "s3:GetBucketPublicAccessBlock", "s3:GetEncryptionConfiguration", "s3:DeleteBucketPolicy", "s3:ListBuckets", "s3:DeleteBucket", "acm:UpdateCertificateOptions", "cloudtrail:DescribeTrails", "lambda:GetFunctionConfiguration", "lambda:GetFunctionUrlConfig", "lambda:GetPolicy", "lambda:InvokeFunction", "lambda:UpdateFunctionUrlConfig", "lambda:UpdateFunctionConfiguration", "lambda:GetFunction", "lambda:ListFunctions", "lambda:ListAliases", "lambda:GetAccountSettings", "lambda:ListVersionsByFunction", "lambda:DeleteFunctionUrlConfig", "lambda:CreateFunction", "lambda:ListLayerVersions", "lambda:DeleteFunction", "lambda:DeleteLayerVersion", "lambda:PublishLayerVersion", "elasticloadbalancing:ModifyLoadBalancerAttributes", "ce:GetCostAndUsage", "ce:GetCostForecast", "budgets:DescribeBudgets", "budgets:DescribeNotificationsForBudget", "ec2:DescribeVolumes", "ec2:ModifyVolume", "ec2:CreateVolume", "ec2:AttachVolume", "ec2:DetachVolume", "ec2:DeleteVolume", "ec2:DescribeLaunchTemplates", "ec2:CreateLaunchTemplate", "ec2:DeleteLaunchTemplate", "ssm:SendCommand", "ssm:ListCommands", "ec2:DeleteVpc", "ec2:CreateVpcEndpoint", "ec2:DescribeInternetGateways", "ec2:DetachInternetGateway", "ec2:DeleteInternetGateway", "ec2:DeleteSubnet", "ec2:CreateNetworkAclEntry", "ec2:DescribeKeyPairs", "ec2:AllocateHosts", "ec2:ReleaseHosts", "ec2:CreateTrafficMirrorSession", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:DeleteLogGroup", "logs:DeleteLogStream", "logs:FilterLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutRetentionPolicy", "logs:DeleteRetentionPolicy", "logs:PutLogEvents", "logs:PutMetricFilter", "logs:DeleteMetricFilter", "logs:DescribeMetricFilters", "logs:TagResource", "network-firewall:DescribeFirewall", "network-firewall:ListFirewalls", "network-firewall:DescribeFirewallPolicy", "network-firewall:CreateFirewall", "network-firewall:DeleteFirewall", "network-firewall:UpdateFirewallDeleteProtection", "network-firewall:UpdateFirewallDescription", "network-firewall:UpdateSubnetChangeProtection", "network-firewall:AssociateSubnets", "network-firewall:DisassociateSubnets", "network-firewall:TagResource", "network-firewall:ListFirewallPolicies", "network-firewall:CreateFirewallPolicy", "network-firewall:UpdateFirewallPolicy", "network-firewall:DeleteFirewallPolicy", "network-firewall:AssociateFirewallPolicy", "network-firewall:UpdateFirewallPolicyChangeProtection", "network-firewall:CreateRuleGroup", "network-firewall:DeleteRuleGroup", "network-firewall:DescribeRuleGroup", "network-firewall:ListRuleGroups", "network-firewall:UpdateRuleGroup", "network-firewall:DeleteResourcePolicy", "network-firewall:PutResourcePolicy", "network-firewall:DescribeResourcePolicy", "network-firewall:ListTagsForResource", "network-firewall:TagResource", "network-firewall:UntagResource", "network-firewall:DescribeLoggingConfiguration", "network-firewall:UpdateLoggingConfiguration", ] COMMAND_SERVICE_MAP = { "aws-billing-cost-usage-list": "ce", "aws-billing-forecast-list": "ce", "aws-billing-budgets-list": "budgets", "aws-billing-budget-notification-list": "budgets", } def print_debug_logs(client: BotoClient, message: str): """ Print debug logs with service prefix and command context. Args: client (BotoClient): The AWS client object message (str): The debug message to log """ service_name = client.meta.service_model.service_name demisto.debug(f"[{service_name}] {demisto.command()}: {message}") def test_module(params: dict) -> str: """ Validate marketplace credentials by calling ``sts:GetCallerIdentity``. ``GetCallerIdentity`` performs a live API call that returns details about the IAM user or role whose credentials are used to call the operation. It requires zero IAM permissions and validates the full auth chain including assumed-role sessions. boto3 client construction alone is a local-only operation — no network call is made and invalid credentials pass silently without this call. """ sts_client, _ = get_service_client( params=params, service_name=AWSServices.STS, config=Config(connect_timeout=5, read_timeout=5, retries={"max_attempts": 1}), ) sts_client.get_caller_identity() demisto.info("[AWS] test-module: sts.GetCallerIdentity succeeded") return "ok" def health_check(credentials: dict, account_id: str, connector_id: str) -> list[HealthCheckError] | HealthCheckError | None: """ Perform AWS service connectivity check with detailed error handling. Args: credentials (dict): AWS credentials account_id (str): AWS account ID connector_id (str): Connector identifier Returns: Single HealthCheckError if connectivity issues are found, None otherwise """ # List to collect all connectivity errors failed_services: list[str] = [] try: # Connectivity check for services for service in AWSServices: try: session = None # Attempt to create a client for each service client, session = get_service_client( session=session, service_name=service, config=Config(connect_timeout=3, read_timeout=3, retries={"max_attempts": 1}), ) demisto.info(f"[AWS Automation Health Check] Successfully created client for {service.value}") except Exception as service_error: demisto.error(f"[AWS Automation Health Check] Failed to create client for {service.value}: {str(service_error)}") failed_services.append(service.value) # If any services failed, create a single aggregated error if failed_services: error_msg = f"Failed to connect to AWS services: {', '.join(failed_services)}" connectivity_error = HealthCheckError( account_id=account_id, connector_id=connector_id, message=error_msg, error_type=ErrorType.CONNECTIVITY_ERROR, ) demisto.info(f"[AWS Automation Health Check] Connectivity error: {error_msg}") return connectivity_error demisto.info("[AWS Automation Health Check] All services connected successfully") return None except Exception as err: demisto.error(f"[AWS Automation Health Check] Unexpected error during health check: {err}") # Create a general internal error internal_error = HealthCheckError( account_id=account_id, connector_id=connector_id, message=f"Unexpected error during health check: {str(err)}", error_type=ErrorType.INTERNAL_ERROR, ) return internal_error def register_proxydome_header(boto_client: BotoClient) -> None: """ Register ProxyDome authentication header for all AWS API requests. This function adds the ProxyDome caller ID header to every boto3 request by registering an event handler that injects the header before sending requests. Args: boto_client (BotoClient): The boto3 client to configure with ProxyDome headers """ event_system = boto_client.meta.events proxydome_token: str = get_proxydome_token() def _add_proxydome_header(request, **kwargs): request.headers["x-caller-id"] = proxydome_token # Register the header injection function to be called before each request event_system.register_last("before-send.*.*", _add_proxydome_header) def _assume_role_credentials(params: dict, access_key_id: str, secret_access_key: str, region: str) -> dict: """ Call AWS STS ``AssumeRole`` using the marketplace-supplied access keys and return temporary credentials suitable for constructing a boto3 ``Session``. Returns: dict: ``{"AccessKeyId", "SecretAccessKey", "SessionToken"}`` from the STS response. """ role_arn: str = params.get("role_arn", "") role_session_name: str = params.get("role_session_name") or DEFAULT_SESSION_NAME session_duration = arg_to_number(params.get("session_duration")) sts_region: str = params.get("sts_region") or region sts_endpoint_url: str | None = params.get("sts_endpoint_url") or None sts_client = boto3.client( "sts", region_name=sts_region, aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key, endpoint_url=sts_endpoint_url, config=Config(connect_timeout=10, read_timeout=10, retries={"max_attempts": 1}), verify=not argToBoolean(params.get("insecure", False)), ) assume_kwargs: dict = {"RoleArn": role_arn, "RoleSessionName": role_session_name} if session_duration is not None: assume_kwargs["DurationSeconds"] = session_duration demisto.debug(f"[AWS] Calling sts.AssumeRole with {assume_kwargs=}") return sts_client.assume_role(**assume_kwargs)["Credentials"] def get_service_client( credentials: dict = {}, params: dict = {}, args: dict = {}, command: str = "", session: Session | None = None, service_name: str = "", config: Config | None = None, ) -> tuple[BotoClient, Session | None]: """ Create and configure a boto3 client for the specified AWS service. Supports two authentication paths: - **Cortex Cloud platform**: ``credentials`` is populated by ``get_cloud_credentials()`` with short-lived CTS tokens. Requests are routed through ProxyDome. - **XSOAR / XSIAM marketplace**: ``credentials`` is empty. Access keys are read from the paired ``credentials`` integration parameter. When ``role_arn`` is set, STS ``AssumeRole`` is called to obtain temporary credentials. ProxyDome is not used. Args: credentials (dict): Platform-supplied CTS credentials. Empty on the marketplace path. params (dict): Integration configuration parameters. args (dict): Command arguments (used for ``region`` override). command (str): AWS command name; used to resolve the service name when ``service_name`` is not provided. session (Session | None): Optional pre-built boto3 session to reuse. service_name (str): Explicit AWS service name; inferred from ``command`` when omitted. config (Config | None): Additional botocore configuration to merge on top of the base. Returns: tuple[BotoClient, Session | None]: Configured boto3 client and the session used. """ region: str = args.get("region") or params.get("region", "") or DEFAULT_REGION is_platform_path: bool = bool(credentials) or bool(get_connector_id()) if is_platform_path: aws_session: Session = session or Session( aws_access_key_id=credentials.get("key"), aws_secret_access_key=credentials.get("access_token"), aws_session_token=credentials.get("session_token"), region_name=region, ) else: creds_param = params.get("credentials") or {} access_key_id: str = creds_param.get("identifier", "") secret_access_key: str = creds_param.get("password", "") if not (access_key_id and secret_access_key): raise DemistoException( "AWS credentials are not configured. Provide an Access Key and Secret Key " "on the integration instance, or run the integration through a Cortex Cloud connector." ) if params.get("role_arn"): tmp_creds = _assume_role_credentials( params=params, access_key_id=access_key_id, secret_access_key=secret_access_key, region=region, ) aws_session = session or Session( aws_access_key_id=tmp_creds["AccessKeyId"], aws_secret_access_key=tmp_creds["SecretAccessKey"], aws_session_token=tmp_creds["SessionToken"], region_name=region, ) else: aws_session = session or Session( aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key, region_name=region, ) # Resolve service name from command if not explicitly provided. if command in COMMAND_SERVICE_MAP: service_name = COMMAND_SERVICE_MAP[command] elif "network-firewall" in command: service_name = "network-firewall" service_name = service_name or command.split("-")[1] service = AWSServices(service_name) # Build base config from user-supplied timeout/retries. read_timeout, connect_timeout = get_timeout(params.get("timeout") if params else None) _retries_raw = (params or {}).get("retries") retries = min(int(_retries_raw if _retries_raw is not None and _retries_raw != "" else DEFAULT_MAX_RETRIES), 10) base_config = Config( connect_timeout=connect_timeout, read_timeout=read_timeout, retries={"max_attempts": retries}, ) if is_platform_path: base_config = base_config.merge( Config( proxies={"https": DEFAULT_PROXYDOME}, proxies_config={"proxy_ca_bundle": DEFAULT_PROXYDOME_CERTFICATE_PATH}, ) ) client_config = base_config.merge(config) if config else base_config # On the platform path, verify using the ProxyDome CA bundle (requests go through ProxyDome # which presents a self-signed cert). On the marketplace path, respect the user's insecure param: # SSL verification is enabled by default (insecure=False) and disabled only when the user # checks the "Trust any certificate (not secure)" box. if is_platform_path: verify_ssl: str | bool = DEFAULT_PROXYDOME_CERTFICATE_PATH else: verify_ssl = not argToBoolean(params.get("insecure", False)) endpoint_url: str | None = (params.get("endpoint_url") or None) if params else None client = aws_session.client(service, verify=verify_ssl, config=client_config, endpoint_url=endpoint_url) if is_platform_path: register_proxydome_header(client) return client, aws_session def _dispatch_command(command: str, args: dict, service_client: BotoClient) -> CommandResults | list[CommandResults] | None: """ Invoke the correct handler: polling commands take ``(args, client)``, others ``(client, args)``. Args: command (str): The AWS command name to dispatch (e.g. ``"aws-ec2-instances-describe"``). Must be a key in ``COMMANDS_MAPPING``. args (dict): Command arguments. A non-``None`` ``polling_timeout`` key marks the command as a polling command and selects the reversed ``(args, client)`` argument order. service_client (BotoClient): The boto3 service client passed to the handler. Returns: CommandResults | list[CommandResults] | None: The result returned by the invoked command handler. """ if args.get("polling_timeout") is not None: demisto.debug(f"The {command=} is a polling command, call it with args as the first argument.") return COMMANDS_MAPPING[command](args, service_client) return COMMANDS_MAPPING[command](service_client, args) def execute_aws_command(command: str, args: dict, params: dict) -> CommandResults | list[CommandResults] | None: """ Execute an AWS command, routing to the appropriate service handler. When ``access_role_name`` and ``accounts_to_access`` are both configured and more than one account is listed, the command is fanned out in parallel across every listed account using ``ThreadPoolExecutor``. Per-account errors are isolated and returned as error entries without aborting the batch. Otherwise the command runs once against the single account from ``args["account_id"]`` (platform) or the configured credentials (marketplace). Args: command (str): The AWS command name to execute (e.g. ``"aws-ec2-instances-describe"``). args (dict): Command arguments including ``account_id``, ``region``, and service-specific parameters. params (dict): Integration configuration parameters from ``demisto.params()``. Returns: CommandResults | list[CommandResults] | None: A single result for the single-account path, or a flat list (one entry per account per result) for the multi-account fan-out. """ # Set STS regional endpoint mode once on the main thread before any parallel execution. if not get_connector_id() and (regional := params.get("sts_regional_endpoint")): demisto.debug(f"Setting AWS_STS_REGIONAL_ENDPOINTS={regional}") os.environ["AWS_STS_REGIONAL_ENDPOINTS"] = regional.lower() role_name = (params.get("access_role_name") or "").removeprefix("role/") accounts = argToList(params.get("accounts_to_access")) if role_name and accounts: max_workers = arg_to_number(params.get("max_workers")) or DEFAULT_MAX_WORKERS demisto.debug(f"[AWS] Multi-account fan-out: {command=}, {len(accounts)=}, {max_workers=}") def _run_for_account(account_id: str) -> list[CommandResults]: per_account_params = params | {"role_arn": f"arn:aws:iam::{account_id}:role/{role_name}"} per_account_args = args | {"account_id": account_id} try: creds = get_cloud_credentials(CloudTypes.AWS.value, account_id) if get_connector_id() else {} svc_client, _ = get_service_client(creds, per_account_params, per_account_args, command) result = _dispatch_command(command, per_account_args, svc_client) if result is None: return [CommandResults(readable_output=f"#### Result for account `{account_id}`:\nNo result returned.")] results_list = result if isinstance(result, list) else [result] for r in results_list: r.readable_output = f"#### Result for account `{account_id}`:\n{r.readable_output or ''}" if isinstance(r.outputs, list): for obj in r.outputs: if isinstance(obj, dict): obj.setdefault("AccountId", account_id) elif isinstance(r.outputs, dict): r.outputs.setdefault("AccountId", account_id) return results_list except Exception as e: demisto.error(f"[AWS] Error for account {account_id}: {e}") return [ CommandResults( readable_output=f"#### Error for account `{account_id}`\n{e}", entry_type=EntryType.ERROR, content_format=EntryFormat.MARKDOWN, ) ] with ThreadPoolExecutor(max_workers=max_workers) as executor: results: list[CommandResults] = [] for res in executor.map(_run_for_account, accounts): results.extend(res) return results # Single-account path. account_id: str = args.get("account_id", "") credentials = get_cloud_credentials(CloudTypes.AWS.value, account_id) if get_connector_id() else {} service_client, _ = get_service_client(credentials, params, args, command) return _dispatch_command(command, args, service_client) def main(): # pragma: no cover params = demisto.params() command = demisto.command() args = demisto.args() demisto.debug(f"Command: {command}") demisto.debug(f"Args: {args}") handle_proxy() try: if command == "test-module": results = ( run_health_check_for_accounts(connector_id, CloudTypes.AWS.value, health_check) if (connector_id := get_connector_id()) else test_module(params) ) demisto.info(f"[AWS] Health Check Results: {results}") return_results(results) elif command in COMMANDS_MAPPING: return_results(execute_aws_command(command, args, params)) else: raise NotImplementedError(f"Command {command} is not implemented") except ClientError as client_err: account_id = args.get("account_id", "") AWSErrorHandler.handle_client_error(client_err, account_id) except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()