F5Silverline
F5 Silverline Threat Intelligence is a cloud-based service incorporating external IP reputation and reducing threat-based communications. By identifying IP addresses and security categories associated with malicious activity, this managed service integrates dynamic lists of threatening IP addresses with the Silverline cloud-based platform, adding context-based security to policy decisions.
Network Security · F5 Silverline
Details
| ID | F5Silverline |
|---|---|
| Provider | F5 |
| Category | Network Security |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Cloud Runtime Security |
README
F5 Silverline Threat Intelligence is a cloud-based service incorporating external IP reputation and reducing threat-based communications. By identifying IP addresses and security categories associated with malicious activity, this managed service integrates dynamic lists of threatening IP addresses with the Silverline cloud-based platform, adding context-based security to policy decisions.
Configure F5 Silverline on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for F5 Silverline.
-
Click Add instance to create and configure a new integration instance.
Parameter Description Required Your F5 Silverline server URL True API Key The API Key to use for connection True Trust any certificate (not secure) False Use system proxy settings False - Click Test to validate the URLs, token, and connection.
Commands
You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
f5-silverline-ip-objects-list
Gets a dynamic list of threatening IP addresses by the given list type.
Base Command
f5-silverline-ip-objects-list
Input
| Argument Name | Description | Required |
|---|---|---|
| list_type | The dynamic lists type of threatening IP addresses. The type can be one of allowlist or denylist. Possible values are: allowlist, denylist. Note: Allowlists are named DDOS IP Allowlists in the F5 Silverline portal. | Required |
| object_id | A comma-separated list of IP object IDs. If this argument is given, only those IP objects will be displayed. Otherwise, all IP objects that match the given list_type will be displayed. IF you don’t the object ID, run this command without the object_id argument to get all the IDs. | Optional |
| page_number | Page number to return. The first page number is 1. | Optional |
| page_size | Number of results in a page. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| F5Silverline.IPObjectList.id | String | The ID of a particular threatening IP address object. |
| F5Silverline.IPObjectList.type | String | The type of a particular threatening IP address object. |
| F5Silverline.IPObjectList.attributes.ip | String | The IP address of a particular threatening IP address object. |
| F5Silverline.IPObjectList.attributes.mask | String | The mask of a particular threatening IP address object. |
| F5Silverline.IPObjectList.attributes.duration | String | The duration (in seconds) of a particular threatening IP address object where list type is ‘denylist’. |
| F5Silverline.IPObjectList.attributes.expires_at | String | The expiration date (timestamp) of a particular threatening IP address object. |
| F5Silverline.IPObjectList.attributes.list_target | String | The list target of a particular threatening IP address object. |
| F5Silverline.IPObjectList.links.self | String | The F5 Silverline URL link of a particular threatening IP address object. |
| F5Silverline.IPObjectList.meta.note | String | The note of a particular threatening IP address object. |
| F5Silverline.IPObjectList.meta.tags | Unknown | The tags of a particular threatening IP address object. |
| F5Silverline.IPObjectList.meta.created_at | String | The creation date (timestamp) of a particular threatening IP address object. |
| F5Silverline.IPObjectList.meta.updated_at | String | The last update date (timestamp) of a particular threatening IP address object. |
| F5Silverline.Paging.last_page_number | Number | The last page number that exists. |
| F5Silverline.Paging.current_page_size | Number | The number of IP objects to be returned on each page. |
| F5Silverline.Paging.current_page_number | Number | The number of the requested page. |
Command Example
!f5-silverline-ip-objects-list list_type=denylist
Context Example
{
"F5Silverline": {
"IPObjectList": [
{
"attributes": {
"duration": 0,
"expires_at": null,
"ip": "1.2.3.5",
"list_target": "proxy",
"mask": "32"
},
"id": "822f2572-ddc4-4eb1-84ab-f27e4095c8c4",
"links": {
"self": "https://portal.f5silverline.com/api/v1/ip_lists/denylist/ip_objects/822f2572-ddc4-4eb1-84ab-f27e4095c8c4?list_target=proxy"
},
"meta": {
"created_at": "2021-05-20T10:25:32.694Z",
"note": null,
"tags": [],
"updated_at": "2021-05-20T10:25:32.694Z"
},
"type": "ip_objects"
},
{
"attributes": {
"duration": 0,
"expires_at": null,
"ip": "14.16.1.0",
"list_target": "proxy",
"mask": "32"
},
"id": "66202ba3-04f1-4f7c-b1a8-9a49776a96a4",
"links": {
"self": "https://portal.f5silverline.com/api/v1/ip_lists/denylist/ip_objects/66202ba3-04f1-4f7c-b1a8-9a49776a96a4?list_target=proxy"
},
"meta": {
"created_at": "2021-06-09T07:59:31.772Z",
"note": "",
"tags": [],
"updated_at": "2021-06-09T07:59:31.772Z"
},
"type": "ip_objects"
}
],
"Paging": []
}
}
Human Readable Output
F5 Silverline denylist IP Objects
ID CIDR Range Created At Updated At 822f2572-ddc4-4eb1-84ab-f27e4095c8c4 1.2.3.5/32 2021-05-20T10:25:32.694Z 2021-05-20T10:25:32.694Z 66202ba3-04f1-4f7c-b1a8-9a49776a96a4 14.16.1.0/32 2021-06-09T07:59:31.772Z 2021-06-09T07:59:31.772Z
f5-silverline-ip-object-add
Adds a new particular threatening IP address object by its IP address.
Base Command
f5-silverline-ip-object-add
Input
| Argument Name | Description | Required |
|---|---|---|
| list_type | The dynamic lists type of threatening IP addresses. The type can be “allowlist” or “denylist”. Note: Allowlists are named DDOS IP Allowlists in the F5 Silverline portal. Possible values are: allowlist, denylist. | Required |
| list_target | This argument can be supplied to target either the proxy or routed denylist. If list_target is not specified it will assume both proxy and routed are requested (i.e., proxy-routed). Possible values are: “proxy”, “routed”, or “proxy-routed”. This argument limits the denylist type but is ignored for allowlist. Possible values are: proxy, routed, proxy-routed. | Optional |
| cidr_range | The CIDR range of a potentially threatening IP address. object (i.e IP address/ mask) in CSV format. For example, “1.2.3.4/32,2.3.4.5”. In case only IP address is given, the default mask is 32. | Required |
| duration | The duration (in seconds) of a new particular threatening IP address object where the list type is ‘denylist’. Setting the duration to 0 (default) means the new IP address object will never expire. This feature has been removed for allowlist. | Optional |
| note | The note of a new particular threatening IP address object. Default is empty. | Optional |
| tags | A comma-separated list of tags of a new particular threatening IP address object. | Optional |
Context Output
There is no context output for this command.
Command Example
!f5-silverline-ip-object-add cidr_range=2.5.3.4 list_type=denylist
Human Readable Output
IP object with CIDR range address: 2.5.3.4/32 added successfully into the denylist list.
f5-silverline-ip-object-delete
Deletes an existing particular threatening IP address object by its object ID or by its IP address.
Base Command
f5-silverline-ip-object-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| list_type | The dynamic lists type of threatening IP addresses. The type can be one of allowlist or denylist. Possible values are: allowlist, denylist. Note: Allowlists are named DDOS IP Allowlists in the F5 Silverline portal. | Required |
| object_id | The object ID of a particular threatening IP address object that should be deleted. | Optional |
| object_ip | The IP address of an existing threatening IP address object that should be deleted. | Optional |
Context Output
There is no context output for this command.
Command Example
!f5-silverline-ip-object-delete list_type=denylist object_id=66202ba3-04f1-4f7c-b1a8-9a49776a96a4
Human Readable Output
IP object with ID: 66202ba3-04f1-4f7c-b1a8-9a49776a96a4 deleted successfully from the denylist list.
Fetch F5 Silverline alerts
| F5 Silverline supported alert type | Incident Type |
|---|---|
| WAF logs | F5 Silverline WAF Events |
| L7 DDoS logs | F5 Silverline L7 DDoS Events |
| Threat Intelligence logs | F5 Silverline Threat Intelligence Events |
| iRule logs | F5 Silverline iRule Events |
As F5 Silverline API does not support fetch incidents for now, we retrieve alerts via a log collector.
In order to fetch alerts, follow the instructions below:
- In Cortex XSOAR, install the F5 Silverline integration.
- In the F5 Silverline portal, go to Config > Log Export .
- Configure the F5 Silverline “Log Export”. Follow the instructions here: https://support.f5silverline.com/hc/en-us/articles/214152048. The “Host” destination must support TLS+TCP communication.
- In Cortex XSOAR, go to Settings > Integrations.
- Search for Syslog. (This integration is installed by default).
- Configure the Syslog instance with your log receiver details:
- Click “Fetches incidents”.
- Set the Classifier to “F5 Silverline Classifier”.
- Set the Mapper to “F5 Silverline Mapper”.
- IP address - specify the IP address of your log receiver host.
- Port - specify the port of your log receiver host.
- Protocol - choose TCP or UDP.
- Format - specify to ‘Auto’.
Once the log receiver is configured it will forward the logs in TCP or UDP to Cortex XSOAR - Syslog integration and you will see that incidents were successfully pulled.
- After incidents are created, you can go to the incidents page and sort them by their type.
- You can go to an incident info tab (by clicking on an incident) and see all of the incident fields (under the Case Details header).
Configuration parameters
url— Your F5 Silverline server URL (required)token— (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (3)
-
f5-silverline-ip-object-addAdds a new particular threatening IP address object by its IP address.
-
f5-silverline-ip-object-deleteDelete an existing particular threatening IP address object by its object ID or by its IP address. If both id and ip are given, delete operation will be done by the given object_id.
-
f5-silverline-ip-objects-listGets a dynamic list of threatening IP addresses by the given list type.
import re import traceback from typing import Any import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa urllib3.disable_warnings() # pylint: disable=no-member BASE_URL = "/api/v1/ip_lists" TABLE_HEADERS_GET_OBJECTS = ["ID", "CIDR Range", "Created At", "Updated At"] PAGE_NUMBER_PATTERN = r"(?<=page\[number]=).*?(?=&)" class Client(BaseClient): def __init__(self, base_url: str, verify: bool, headers: dict, proxy: bool): """ Client to use in the F5_Silverline integration. Overrides BaseClient. Args: base_url (str): URL to access when doing an http request. verify (bool): Whether to check for SSL certificate validity. headers (dict): Headers to set when when doing an http request. proxy (bool): Whether the client should use proxies. """ super().__init__(base_url=base_url, verify=verify, proxy=proxy) self._headers = headers def request_ip_objects(self, body: dict, method: str, url_suffix: str, params: dict, resp_type="json") -> dict: """ Makes an HTTP request to F5 Silverline API by the given arguments. Args: body (dict): The dictionary to send in a 'POST' request. method (str): HTTP request method (GET/POST/DELETE). url_suffix (str): The API endpoint. params (dict): URL parameters to specify the query. resp_type (str): Determines which data format to return from the HTTP request. The default is 'json'. """ demisto.debug( f"current request is: method={method}, body={body}, url suffix={url_suffix}," f"params={params}, resp_type={resp_type}" ) return self._http_request( method=method, json_data=body, url_suffix=url_suffix, params=params, headers=self._headers, resp_type=resp_type ) def test_module(client: Client) -> str: """ Tests API connectivity and authentication. Does a GET request for this purpose. """ try: client.request_ip_objects(body={}, method="GET", url_suffix="denylist/ip_objects", params={}) message = "ok" except DemistoException as e: if "Unauthorized" in str(e): message = "Authorization Error: make sure API Key is correctly set" else: raise e return message def paging_args_to_params(page_size, page_number): """ Returns the parameters to the HTTP request when using paging. """ try: page_size = int(page_size) page_number = int(page_number) except ValueError: raise ValueError("page_number and page_size should be numbers") params = {"page[size]": page_size, "page[number]": page_number} return params def get_ip_and_mask_from_cidr(cidr_range): """ The input can be one of the following: 1. an IPv4 address (i.e the input does not include the separator '/'). In this case the default mask is 32 . 2. a CIDR range which seems like this: IPv4/mask. for example : 1.2.3.4/32 . """ if "/" not in cidr_range: ip_address = cidr_range mask = "32" else: cidr_range = cidr_range.split("/") ip_address = cidr_range[0] mask = cidr_range[1] return ip_address, mask def add_ip_objects_command(client: Client, args: dict[str, Any]): """ Adds a new IP object to the requested list type (denylist or allowlist). IP object includes an IP address (mandatory). Other fields are optional and have default values. Note: Human readable appears only if the HTTP request did not fail. API docs: https://portal.f5silverline.com/docs/api/v1/ip_objects.md (POST section) """ list_type = args["list_type"] cidr_range = args["cidr_range"] list_target = args.get("list_target", "proxy-routed") duration = int(args.get("duration", 0)) note = args.get("note", "") tags = argToList(args.get("tags", [])) url_suffix = f"{list_type}/ip_objects" errors_list = [] success_list = [] for ip_address, mask in map(get_ip_and_mask_from_cidr, argToList(cidr_range)): body = define_body_for_add_ip_command(list_target, mask, ip_address, duration, note, tags) try: client.request_ip_objects(body=body, method="POST", url_suffix=url_suffix, params={}, resp_type="content") except Exception as error: demisto.error(traceback.format_exc()) errors_list.append(f"could not add {ip_address}/{mask} to {list_type}. error: {error}") success_list.append(f"| {ip_address}/{mask} |") if success_list: success_lines = "\n".join(success_list) human_readable = f"IP objects were added successfully into the {list_type}\n| IP |\n| - |\n{success_lines}" return_results(CommandResults(readable_output=human_readable)) if errors_list: return_error("\n".join(errors_list)) def define_body_for_add_ip_command(list_target, mask, ip_address, duration, note, tags): """ API docs: https://portal.f5silverline.com/docs/api/v1/ip_objects.md (POST section) prepares the body of a POST request in order to add an IP """ return { "list_target": list_target, "data": { "id": "", "type": "ip_objects", "attributes": {"mask": mask, "ip": ip_address, "duration": duration}, "meta": {"note": note, "tags": tags}, }, } def is_object_id_exist(client, object_id_list, list_type): try: _, outputs = get_ip_objects_by_ids(client, object_id_list, list_type, {}) return True except Exception: demisto.debug(f"The following ids {object_id_list} were not found in {list_type} list.") raise DemistoException("An object with the given identifier was not found. ") def delete_ip_objects_command(client: Client, args: dict[str, Any]): """ Deletes an exist IP object from the requested list type (denylist or allowlist) by its object id or its ip. Note: Human readable appears only if the HTTP request did not fail and the object id exists. In case the ID does not exist, an error will be raised. API docs: https://portal.f5silverline.com/docs/api/v1/ip_objects.md (DELETE section) """ list_type = args["list_type"] object_id = args.get("object_id") object_ip = args.get("object_ip") object_id_list: list[Any] = [] list_target = args.get("list_target", "proxy") demisto.debug(f"debug-log: {list_type=}, {object_id=}, {object_ip=}, {list_target=}") if not object_id and not object_ip: raise DemistoException("At least one of the following arguments should be given: object_ip, object_id.") if not object_id: # we have got an ip, so we want to get all the matched ids for the given ip object_id_list = get_object_id_by_ip(client, list_type, object_ip) else: # we have got a specific id to be deleted object_id_list.append(object_id) if object_id_list: human_readable = "" for object_id in object_id_list: url_suffix = f"{list_type}/ip_objects/{object_id}" # like the matching ip-object-add command, the list_target argument is added as a default value to the request if # no other value explicitly selected, and is only relevant for denylist (ignored for allowlist) client.request_ip_objects( body={}, method="DELETE", url_suffix=url_suffix, params={"list_target": list_target}, resp_type="content" ) human_readable += f"IP object with ID: {object_id} deleted successfully from the {list_type} list. \n" return CommandResults(readable_output=human_readable) return None def get_object_id_by_ip(client, list_type, object_ip): """Get object ID by its IP attribute. This function paginates over a list of objects and extracts all of their ID attributes as preparation for deletion. Note: this function, used by the 'delete_ip_objects_command' command is utilizing pagination. This is because the GET request is limited to return up to 1,000 objects per requests, which may result in 404 error if the user has overall more objects than that. while the 'get_ip_objects_list_command' command already utilizes pagination, this is a different use-case since there is no use user input regarding pagination in 'f5-silverline-ip-object-delete command (there is no need). Args: client (Client): Client object list_type (list): Type of list to query object_ip (_type_): Object IP to search for deletion. Raises: DemistoException: Raised in the case an object with the given IP address was not found. Returns: list: A list of found ID objects. """ page = 1 url_suffix = f"{list_type}/ip_objects" response = client.request_ip_objects(body={}, method="GET", url_suffix=url_suffix, params={}) all_objects: list = response.get("data") try: while next_page := response.get("links", {}).get("links", {}).get("next", {}): page += 1 demisto.debug(f"debug-log: next page found in response: {next_page=}, performing pagination for {page=}") response = client.request_ip_objects(body={}, method="GET", url_suffix=url_suffix, params={"page": page}) if response: demisto.debug(f"debug-log: response from {page=} has {len(response.get('data'))} objects") all_objects.extend(response.get("data")) except Exception: demisto.debug("debug-log: exception raised while trying to paginate in get_object_id_by_ip: str (e)") demisto.debug(f"debug-log: found total of {len(all_objects)} objects.") all_match_ids = [] for obj in all_objects: attributes = obj.get("attributes") ip = attributes.get("ip", "") if ip == object_ip: all_match_ids.append(obj.get("id")) if not all_match_ids: raise DemistoException("An object with the given IP address was not found.") demisto.debug(f"debug-log: {all_match_ids=}") return all_match_ids def handle_paging(page_number, page_size): """ * Returns whether the user wants to get the results by paging (page size and page number). * Returns the parameters dict to the HTTP request when using paging, empty dict will be returned if paging was not required. """ is_paging_required = False params = {} if page_number and page_size: params = paging_args_to_params(page_size, page_number) is_paging_required = True return is_paging_required, params def add_paging_to_outputs(paging_dict, page_number): """ As the API returns a dict of links, for example: "links": { "self": "https://f5silverline.com/api/v1/ip_lists/allowlist/ip_objects?page[number]=1&page[size]=1", "first": "https://f5silverline.com/api/v1/ip_lists/allowlist/ip_objects?page[number]=1&page[size]=1", "last": "https://f5silverline.com/api/v1/ip_lists/allowlist/ip_objects?page[number]=20&page[size]=1", "next": "https://f5silverline.com/api/v1/ip_lists/allowlist/ip_objects?page[number]=2&page[size]=1" } we would like to get page numbers by regex filtering. """ link_to_current_obj = paging_dict.get("self") if not link_to_current_obj: demisto.debug(f"The paging response seems to be broken {paging_dict}") raise DemistoException("An error occurred when trying to parse paging response") current_page_number = re.search(PAGE_NUMBER_PATTERN, link_to_current_obj) # guardrails-disable-line current_page_number = current_page_number.group(0) if current_page_number else page_number link_to_last_obj = paging_dict.get("last") last_page_number = current_page_number if link_to_last_obj: last_page_number = re.search(PAGE_NUMBER_PATTERN, link_to_last_obj) # guardrails-disable-line if last_page_number: last_page_number = last_page_number.group(0) return current_page_number, last_page_number def paging_outputs_dict(current_page_number, last_page_number, page_size): return {"current_page_number": current_page_number, "current_page_size": page_size, "last_page_number": last_page_number} def paging_data_to_human_readable(current_page_number, last_page_number, page_size): output = f"Current page number: {current_page_number}\n " if not last_page_number: # if the current page number is also the last page number last_page_number = current_page_number output += f"Last page number: {last_page_number}\n" if page_size: output += f"Current page size: {page_size}" return output def get_ip_objects_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Gets a list of IP objects by the requested list type (denylist or allowlist). If the object_id argument is given, only those IP objects will be displayed. Otherwise, all IP objects that match the given list_type will be displayed. API docs: https://portal.f5silverline.com/docs/api/v1/ip_objects.md (GET section) """ list_type = args["list_type"] object_ids = argToList(args.get("object_id")) page_number = args.get("page_number") page_size = args.get("page_size") url_suffix = f"{list_type}/ip_objects" paging_data = [] paging_data_human_readable = "" is_paging_required, params = handle_paging(page_number, page_size) if not object_ids: # in case the user wants to get all the IP objects and not specific ones response = client.request_ip_objects(body={}, method="GET", url_suffix=url_suffix, params=params) outputs = response.get("data") human_results = parse_get_ip_object_list_results(response) if is_paging_required and outputs: to_page = response.get("links") if list_type == "allowlist": current_page_number, last_page_number = add_paging_to_outputs(to_page, page_number) else: to_page = to_page.get("links") # type: ignore current_page_number, last_page_number = add_paging_to_outputs(to_page, page_number) paging_data = paging_outputs_dict(current_page_number, last_page_number, page_size) paging_data_human_readable = paging_data_to_human_readable(current_page_number, last_page_number, page_size) else: human_results, outputs = get_ip_objects_by_ids(client, object_ids, list_type, params) # type: ignore human_readable = tableToMarkdown( f"F5 Silverline {list_type} IP Objects", human_results, TABLE_HEADERS_GET_OBJECTS, removeNull=True ) human_readable += paging_data_human_readable if not human_results and is_paging_required: human_readable = ( "No results were found. Please try to run the command without page_number and page_size to " "get all existing IP objects." ) return CommandResults( readable_output=human_readable, outputs_prefix="F5Silverline", outputs_key_field="id", outputs={"IPObjectList": outputs, "Paging": paging_data}, ) def get_ip_objects_by_ids(client: Client, object_ids: list, list_type: str, params: dict): """ In case the user requests one or more specific IP objects (by their object_id). For each id we make a separate HTTP request (the API does not support list of ids). """ human_results = [] outputs = [] for object_id in object_ids: url_suffix = f"{list_type}/ip_objects/{object_id}" res = client.request_ip_objects(body={}, method="GET", url_suffix=url_suffix, params=params) outputs.append(res.get("data")) human_results.append(parse_get_ip_object_list_results(res)[0]) return human_results, outputs def parse_get_ip_object_list_results(results: dict): """ Parsing the API response after requesting the IP object list. Parsing maps the important fields that will appear as the human readable output. An example for a response: https://portal.f5silverline.com/docs/api/v1/ip_objects.md. Under the title "Success Response -> Body" """ parsed_results = [] results_data = results.get("data") # type: ignore demisto.debug(f"response is {results_data}") if isinstance(results_data, dict): # in case the response consist only single ip object, the result is a dict and not a list, but we want to handle # those cases in the same way results_data = [results_data] for ip_object in results_data: # type: ignore if ip_object: ip_address = ip_object.get("attributes").get("ip") mask = ip_object.get("attributes").get("mask") cidr_range = f"{ip_address}/{mask}" parsed_results.append( { "ID": ip_object.get("id"), "CIDR Range": cidr_range, "Created At": ip_object.get("meta").get("created_at"), "Updated At": ip_object.get("meta").get("updated_at"), } ) return parsed_results def main() -> None: params = demisto.params() access_token = params.get("token").get("password") base_url = urljoin(params.get("url"), BASE_URL) verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) demisto.debug(f"Command being called is {demisto.command()}") try: headers: dict = {"X-Authorization-Token": access_token, "Content-Type": "application/json"} client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy) args = demisto.args() if demisto.command() == "test-module": return_results(test_module(client)) elif demisto.command() == "f5-silverline-ip-objects-list": return_results(get_ip_objects_list_command(client, args)) elif demisto.command() == "f5-silverline-ip-object-add": add_ip_objects_command(client, args) elif demisto.command() == "f5-silverline-ip-object-delete": return_results(delete_ip_objects_command(client, args)) else: raise NotImplementedError(f"{demisto.command()} is not an existing F5 Silverline command") except Exception as e: demisto.error(traceback.format_exc()) return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}") if __name__ in ("__main__", "__builtin__", "builtins"): main()