ResolveShortenedURL
This script resolves the original URL from a given shortened URL and places the resolved URL in the playbook context and output.
- Type
- python
- Pack
- CommonScripts
Source
from abc import ABCMeta
from typing import NamedTuple
import demistomock as demisto # noqa: F401
import urllib3
from CommonServerPython import * # noqa: F401
from requests import Response
urllib3.disable_warnings() # Disable insecure warnings
DEFAULT_SERVICE = "Built-In"
DEFAULT_REDIRECT_LIMIT = "0"
class URLUnshorteningData(NamedTuple):
"""
A tuple containing data for unshortend URLs.
Attributes:
original_url (str): The original URL.
resolved_url (str): The resolved URL.
service_name (str): The name of the service used to resolve the URL.
redirect_history (list): A list of URLs that were redirected to get to the resolved URL.
raw_data (dict | list[dict] | None, optional): The raw data returned by the service. None if not available.
encountered_error (bool, optional): Whether an error was encountered while resolving the URL. Defaults to False.
api_usage (int | None, optional): The API usage count for the current IP. None if not relevant to the service.
api_rate_limit (int | None, optional): The maximum number of API calls allowed by the service
for a defined period of time. None if not relevant to the service.
"""
original_url: str
resolved_url: str
service_name: str
redirect_history: list[str]
raw_data: dict | list[dict] | None = None
encountered_error: bool = False
api_usage: int | None = None
api_rate_limit: int | None = None
def to_context_dict(self) -> dict:
"""
Converts the data to a dictionary that will be used as the context data.
Adds recursion data only if relevant.
Note:
We subtract 1 from RedirectCount because the original URL is included in the recursion history.
Returns:
dict: A dictionary containing the data in context format.
"""
data = {
"OriginalURL": self.original_url,
"ResolvedURL": self.resolved_url,
"ServiceName": self.service_name,
"RedirectCount": len(self.redirect_history) - 1,
"RedirectHistory": self.redirect_history,
"EncounteredError": self.encountered_error,
}
if self.api_usage is not None:
data["APIUsageCount"] = self.api_usage
if self.api_rate_limit is not None:
data["APIRateLimit"] = self.api_rate_limit
return data
def to_hr_dict(self) -> dict:
"""
Converts the data to a dictionary that will be used as the human-readable data.
Returns:
dict: A dictionary containing the data in human-readable format.
"""
api_usage_hr: str | None = None
if self.api_usage is not None:
api_usage_hr = f"{self.api_usage}"
if self.api_rate_limit is not None:
api_usage_hr += f"/{self.api_rate_limit}"
return {
"Original URL": self.original_url,
"Resolved URL": self.resolved_url,
"Service Used": self.service_name,
"Redirect History": self.redirect_history if len(self.redirect_history) > 1 else None,
"API Usage Count": api_usage_hr if api_usage_hr is not None else None,
}
class URLUnshortingService(BaseClient, metaclass=ABCMeta):
"""
An abstract base class for URL unshorteners.
Note:
To add a new service, create a new class that inherits from this class, and implements the `resolve_url` method.
The class attribute `service_name` must match the name used in the under `service` on the YAML file.
Once created new service should be automatically detected and used.
Attributes:
base_url (str): The base URL of the service that will be used for sending requests.
service_name (str): The name of the service.
redirect_limit (int | None): The maximum number of redirects to follow. None if no limit.
"""
base_url: str
service_name: str
service_rate_limit: int | None = None
def __init__(self, redirect_limit: int | None = None, **kwargs):
super().__init__(base_url=self.base_url, **kwargs)
self.redirect_limit = redirect_limit if redirect_limit is not None else 0
def hit_redirect_limit(self, redirect_history: list) -> bool:
"""
Checks whether the redirect limit has been reached.
Args:
redirect_history (list): The redirect history of the URL.
Returns:
bool: True if the redirect limit has been reached, False otherwise.
"""
if self.redirect_limit == 0:
return False
return len(redirect_history) >= self.redirect_limit
@staticmethod
def find_matching_service(service_name: str) -> type["URLUnshortingService"]:
"""
Finds a matching service class by name.
Args:
service_name (str): The service name to find (has to match the service_name class attribute).
Returns:
Type[URLUnshortingService]: A subclass of URLUnshortingService that matches the service name.
"""
for service_class in URLUnshortingService.__subclasses__():
if service_class.service_name.casefold() == service_name.casefold():
return service_class
raise ValueError(f'No matching service was found for: "{service_name}".')
@abstractmethod
def resolve_url(self, url: str) -> URLUnshorteningData: # pragma: no cover
"""
Resolve a shortened URL.
Args:
url (str): The URL to resolve.
Returns:
URLUnshorteningData: A NamedTuple containing the data for the resolved URL.
"""
class BuiltInShortener(URLUnshortingService):
"""A class for unshortening URLs using Python requests."""
base_url = ""
service_name = "Built-In"
def resolve_url(self, url: str) -> URLUnshorteningData:
encountered_error = False
original_url = url
url = url if url.startswith("http") else f"https://{url}"
try:
response: Response = self._http_request(
method="GET",
full_url=url,
resp_type="response",
allow_redirects=False,
)
except Exception:
encountered_error = True
redirect_history: list[str] = []
while not encountered_error and (response.is_redirect and (not self.hit_redirect_limit(redirect_history))):
url = response.headers["location"]
redirect_history.append(url)
try:
response = self._http_request(
method="GET",
full_url=url,
resp_type="response",
allow_redirects=False,
)
except Exception:
if not self.hit_redirect_limit(redirect_history):
encountered_error = True
break
return URLUnshorteningData(
original_url=original_url,
resolved_url=url,
service_name=self.service_name,
redirect_history=[original_url] + redirect_history,
encountered_error=encountered_error,
)
def unshorten_url(
service_name: str, url: str, redirect_limit: int, use_system_proxy: bool = False, session_verify: bool = True
) -> CommandResults:
"""
Unshorten a shortened URL.
Args:
service_name (str): The service to use for unshortening.
url (str): The URL to un-shorten.
use_system_proxy (bool): Whether to use the system proxy.
session_verify (bool): Whether to verify the SSL certificate of the request.
redirect_limit (int): A maximum number of recursions to run. Use 0 for unlimited.
"""
error_message = (
"There was an error while attempting to unshorten the final URL in the redirect chain.\n"
"It is possible that the unshortening process was not fully completed.\n\n"
)
service_class = URLUnshortingService.find_matching_service(service_name=service_name)
service_instance = service_class( # pylint: disable=E0110
redirect_limit=redirect_limit, # pylint: disable=E0110
proxy=use_system_proxy,
verify=session_verify,
)
returned_data = service_instance.resolve_url(url=url)
readable_output = ""
if returned_data.encountered_error:
readable_output += error_message
readable_output += tableToMarkdown(
name="URL Unshortening Results",
t=returned_data.to_hr_dict(),
headers=list(returned_data.to_hr_dict().keys()),
removeNull=True,
)
return CommandResults(
outputs_prefix="ResolveShortenedURL",
outputs_key_field="OriginalURL",
outputs=returned_data.to_context_dict(),
readable_output=readable_output,
indicator=Common.URL(
url=returned_data.resolved_url,
dbot_score=Common.DBotScore(
indicator=returned_data.resolved_url,
indicator_type=DBotScoreType.URL,
integration_name="ResolveShortenedURL",
score=Common.DBotScore.NONE,
),
),
raw_response=returned_data.raw_data,
)
def main(): # pragma: no cover
args = demisto.args()
try:
url: str = args["url"]
use_system_proxy = argToBoolean(args.get("use_system_proxy", "False"))
redirect_limit = arg_to_number(args.get("redirect_limit", DEFAULT_REDIRECT_LIMIT))
# `arg_to_number` returns `None` if int conversion was unsuccessful.
if redirect_limit is None:
raise ValueError("'redirect_limit' must be a natural number.")
session_verify = not argToBoolean(args.get("insecure", "False"))
result = unshorten_url(
service_name=DEFAULT_SERVICE,
url=url,
use_system_proxy=use_system_proxy,
redirect_limit=redirect_limit,
session_verify=session_verify,
)
return_results(result)
except Exception as e:
return_error(f"Error: {e!s}")
if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover
main()
README
This script resolves the original URL from a given shortened URL and places the resolved URL in the playbook context and output.
Disclaimer:
- The service visits the URL and follows redirects on the server, which exposes the server IP address to the redirect URLs.
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Tags | Utility |
| Cortex XSOAR Version | 6.5.0 |
Inputs
| Argument Name | Description |
|---|---|
| url | The URL to resolve. |
| redirect_limit | A maximum number of recursions to run in case of nested shortened-URLs. Use 0 for unlimited (not recommended). |
| insecure | Trust any certificate (not secure). |
Outputs
| Path | Description | Type |
|---|---|---|
| URL.Data | The resolved URL data. | string |
| ResolveShortenedURL.OriginalURL | The original shortened URL. | string |
| ResolveShortenedURL.ResolvedURL | The resolved URL. | string |
| ResolveShortenedURL.RedirectCount | The number of redirects followed to resolve the URL. | int |
| ResolveShortenedURL.RedirectHistory | The history of redirects followed to resolve the URL. | list |