GetDockerImageLatestTag

Gets docker image latest tag. Script simulates the docker pull flow but doesn't actually pull the image. Returns an entry with the docker image latest tag if all is good, otherwise will return an error.

python · Common Scripts

Source

import re

import demistomock as demisto
import requests
from CommonServerPython import *
from packaging.version import parse as parse_version

from CommonServerUserPython import *

ACCEPT_HEADER = {
    "Accept": "application/json, "
    "application/vnd.docker.distribution.manifest.v2+json, "
    "application/vnd.docker.distribution.manifest.list.v2+json"
}

# use 10 seconds timeout for requests
TIMEOUT = 10
DEFAULT_REGISTRY = "registry-1.docker.io"


def parse_www_auth(www_auth):
    """Parse realm and service from www-authenticate string of the form:
    Bearer realm="https://auth.docker.io/token",service="registry.docker.io"

    :param www_auth: www-authenticate header value
    :type www_auth: string
    """
    match = re.match(r'.*realm="(.+)",service="(.+)".*', www_auth, re.IGNORECASE)
    if not match:
        return None
    return (match.group(1), match.group(2))


def docker_auth(image_name, verify_ssl=True, registry=DEFAULT_REGISTRY, gateway_creds=""):
    """
    Authenticate to the docker service. Return an authentication token if authentication is required.
    """
    res = requests.get(f"https://{registry}/v2/", headers=ACCEPT_HEADER, timeout=TIMEOUT, verify=verify_ssl)
    if res.status_code == 401:  # need to authenticate
        # defaults in case we fail for some reason
        realm = "https://auth.docker.io/token"
        service = "registry.docker.io"
        # Should contain header: Www-Authenticate
        www_auth = res.headers.get("www-authenticate")
        if www_auth:
            parse_auth = parse_www_auth(www_auth)
            if parse_auth:
                realm, service = parse_auth
            else:
                demisto.info(f"Failed parsing www-authenticate header: {www_auth}")
        else:
            demisto.info(f"Failed extracting www-authenticate header from registry: {registry}, final url: {res.url}")
        headers = ACCEPT_HEADER.copy()
        if gateway_creds and registry != DEFAULT_REGISTRY:
            headers["Authorization"] = f"Basic {gateway_creds}"
        res = requests.get(
            f"{realm}?scope=repository:{image_name}:pull&service={service}", headers=headers, timeout=TIMEOUT, verify=verify_ssl
        )
        res.raise_for_status()
        res_json = res.json()
        return res_json.get("token")
    else:
        res.raise_for_status()
        return None


def clear_non_numbered_tags(tags):
    """Clears a given tags list to only keep numbered tags

    Args:
        tags(list): list of docker image tag names - ordered in lexical order

    Returns:
        a tag list with only numbered tags
    """
    only_numbered_tags = []
    for tag in tags:
        number_token = 1
        split_tag = tag.split(".")
        for sub_section in split_tag:
            if not sub_section.isdigit():
                number_token = 0

        if number_token:
            only_numbered_tags.append(tag)

    return only_numbered_tags


def lexical_find_latest_tag(tags):
    """Will return the latest numeric docker image tag if possible - otherwise will return the last lexical tag.

    for example for the tag list: [2.0.2000, 2.1.2700 2.1.373, latest], will return 2.1.2700

    Args:
        tags(list): list of docker image tag names - ordered in lexical order
    """

    only_numbered_tags = clear_non_numbered_tags(tags)

    if len(only_numbered_tags) == 0:
        return tags[-1]

    max_tag = only_numbered_tags[0]

    for num_tag in only_numbered_tags:
        if parse_version(max_tag) < parse_version(num_tag):
            max_tag = num_tag

    return max_tag


def find_latest_tag_by_date(tags):
    """Get the latest tags by datetime comparison.

    Args:
        tags(list): List of dictionaries representing the docker image tags

    Returns:
        The last updated docker image tag name
    """
    latest_tag_name = "latest"
    latest_tag_date = datetime.now() - timedelta(days=400000)
    for tag in tags:
        tag_date = datetime.strptime(tag.get("last_updated"), "%Y-%m-%dT%H:%M:%S.%fZ")
        if tag_date >= latest_tag_date:
            latest_tag_date = tag_date
            latest_tag_name = tag.get("name")

    return latest_tag_name


def main():
    if demisto.args().get("use_system_proxy") == "no":
        # Remove proxy environment variables if they exist
        for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]:
            os.environ.pop(proxy_var, None)
    verify_ssl = demisto.args().get("trust_any_certificate") != "yes"
    docker_full_name = demisto.args()["docker_image"]
    gateway_creds = demisto.args().get("creds_for_opp", "")
    registry = DEFAULT_REGISTRY
    image_name = docker_full_name
    if docker_full_name.count("/") > 1:
        registry, image_name = docker_full_name.split("/", 1)
    try:
        auth_token = docker_auth(image_name, verify_ssl, registry, gateway_creds)
        headers = ACCEPT_HEADER.copy()
        if auth_token:
            headers["Authorization"] = f"Bearer {auth_token}"

        # first try to get the docker image tags using normal http request
        res = requests.get(
            url=f"https://hub.docker.com/v2/repositories/{image_name}/tags",
            verify=verify_ssl,
        )
        if res.status_code == 200:
            tags = res.json().get("results", [])
            # if http request successful find the latest tag by date in the response
            if tags:
                tag = find_latest_tag_by_date(tags)
            else:
                tag = ""
                demisto.debug(f"No tags, {tag=}")

        else:
            # if http request did not successed than get tags using the API.
            # See: https://docs.docker.com/registry/spec/api/#listing-image-tags
            res = requests.get(
                f"https://{registry}/v2/{image_name}/tags/list", headers=headers, timeout=TIMEOUT, verify=verify_ssl
            )
            res.raise_for_status()
            # the API returns tags in lexical order with no date info - so try an get the numeric highest tag
            tags = res.json().get("tags", [])
            if tags:
                tag = lexical_find_latest_tag(tags)
            else:
                tag = ""
                demisto.debug(f"No tags, {tag=}")

        demisto.results(tag)
    except Exception as ex:
        return_error(f"Failed getting tag for: {docker_full_name}. Err: {ex!s}")


# python2 uses __builtin__ python3 uses builtins
if __name__ in ("__main__", "__builtin__", "builtins"):
    main()

README

Gets docker image latest tag. Script simulates the docker pull flow but doesn’t actually pull the image. Returns an entry with the docker image latest tag if all is good, otherwise will return an error.

Script Data


Name Description
Script Type python2
Cortex XSOAR Version 5.0.0

Inputs


Argument Name Description
docker_image Docker image full name with version: For example: demisto/python
use_system_proxy Use system proxy settings
trust_any_certificate Trust any certificate (not secure)

Outputs


There are no outputs for this script.

Script Examples

Example command

!GetDockerImageLatestTag docker_image=demisto/python3

Human Readable Output

3.10.4.29342