DockerHardeningCheck

Checks if the Docker container running this script has been hardened according to the recommended settings at: - For Docker hardening guide (Cortex XSOAR 6.13) https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.13/Cortex-XSOAR-Administrator-Guide/Docker-Hardening-Guide - For Docker hardening guide (Cortex XSOAR 8 Cloud) https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Docker-hardening-guide - For Docker hardening guide (Cortex XSOAR 8.7 On-prem) https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Docker-hardening-guide.

python · Common Scripts

Source

import re
import resource
import subprocess
import time
from multiprocessing import Process
from pathlib import Path

import demistomock as demisto  # noqa: F401
import requests
import urllib3
from CommonServerPython import *  # noqa: F401

urllib3.disable_warnings()

CLOUD_METADATA_URL = "http://169.254.169.254/"  # disable-secrets-detection


def big_string(size):
    s = "a" * 1024
    while len(s) < size:
        s = s * 2
    return len(s)


def mem_size_to_bytes(mem: str) -> int:
    res = re.match(r"(\d+)\s*([gm])?b?", mem, re.IGNORECASE)
    if not res:
        raise ValueError(f"Failed parsing memory string: {mem}")
    b = int(res.group(1))
    if res.group(2):
        b = b * 1024 * 1024  # convert to mega byte
        if res.group(2).lower() == "g":
            b = b * 1024  # convert to giga
    return b


def check_memory(target_mem: str, check_type: str) -> str:  # pragma: no cover
    """Check allocating memory

    Arguments:
        target_mem {str} -- target memory size. Can specify as 1g 1m and so on
        check_type {str} -- How to check either: cgroup (check configuration of cgroup) or allocate (check actual allocation)

    Returns:
        str -- error string if failed
    """
    size = mem_size_to_bytes(target_mem)
    if check_type == "allocate":
        LOG(f"starting process to check memory of size: {size}")
        p = Process(target=big_string, args=(size,))
        p.start()
        p.join()
        LOG(f"memory intensive process status code: {p.exitcode}")
        if p.exitcode == 0:
            return (
                f"Succeeded allocating memory of size: {target_mem}. "
                "It seems that you haven't limited the available memory to the docker container."
            )
    else:
        cgroup_file_v1 = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
        cgroup_file_v2 = "/sys/fs/cgroup/memory.max"
        if Path(cgroup_file_v1).exists():
            cgroup_file = cgroup_file_v1
        elif Path(cgroup_file_v2).exists():
            cgroup_file = cgroup_file_v2
        else:
            return (
                "Failed checking cgroup file, memory_check set to cgroup but neither v1 or v2"
                " cgroup files found, verify cgroups is enabled."
            )

        try:
            with open(cgroup_file) as f:
                mem_bytes = int(f.read().strip())
                if mem_bytes > size:
                    return (
                        f"According to memory cgroup configuration at: {cgroup_file}"
                        f" available memory in bytes [{mem_bytes}] is larger than {target_mem}"
                    )
        except Exception as ex:
            return (
                f"Failed reading memory cgroup from: {cgroup_file}. Err: {ex}."
                " You may be running a docker version which does not provide this configuration information."
                " You can try running the memory check with memory_check=allocate as an alternative."
            )
    return ""


def check_pids(pid_num: int) -> str:
    LOG(f"Starting pid check for: {pid_num}")
    processes = [Process(target=time.sleep, args=(30,)) for i in range(pid_num)]
    try:
        for p in processes:
            p.start()
        time.sleep(0.5)
        alive = 0
        for p in processes:
            if p.is_alive():
                alive += 1
        if alive >= pid_num:
            return (
                f"Succeeded creating processs of size: {pid_num}. "
                "It seems that you haven't limited the available pids to the docker container."
            )
        else:
            LOG(f"Number of processes that are alive: {alive} is smaller than {pid_num}. All good.")
    except Exception as ex:
        LOG(f"Pool startup failed (as expected): {ex}")
    finally:
        for p in processes:
            if p.is_alive():
                p.terminate()
                p.join()
    return ""


def check_fd_limits(soft, hard) -> str:
    s, h = resource.getrlimit(resource.RLIMIT_NOFILE)
    if s > soft:
        return f"FD soft limit: {s} is above desired limt: {soft}."
    if h > hard:
        return f"FD hard limit: {h} is above desired limit: {hard}."
    return ""


def check_non_root():
    uid = os.getuid()
    if uid == 0:
        return (
            f"Running as root with uid: {uid}."
            " It seems that you haven't set the docker container to run with a non-root internal user."
        )
    return ""


def intensive_calc(iter: int):
    i = 0
    x = 1
    while i < iter:
        x = x * 2
        i += 1
    return x


def check_cpus(num_cpus: int) -> str:
    iterval = 500 * 1000
    processes = [Process(target=intensive_calc, args=(iterval,)) for i in range(num_cpus)]
    start = time.time_ns()
    for p in processes:
        p.start()
    for p in processes:
        p.join()
    runtime = time.time_ns() - start
    LOG(f"cpus check runtime for {num_cpus} processes time: {runtime}")
    processes = [Process(target=intensive_calc, args=(iterval,)) for i in range(num_cpus * 2)]
    start = time.time_ns()
    for p in processes:
        p.start()
    for p in processes:
        p.join()
    runtime2 = time.time_ns() - start
    # runtime 2 should be 2 times slower. But we give it a safty as the machine itself maybe loaded
    LOG(f"cpus check runtime for {num_cpus * 2} processes time: {runtime2}")
    if runtime2 < runtime * 1.5:
        return (
            "CPU processing power increased significantly when increasing processes "
            f"from: {num_cpus} (time: {runtime}) to: {num_cpus * 2} (time: {runtime2}). "
            "Note: this test may fail even if the proper configuration has been applied and"
            " the machine itself is loaded."
        )
    return ""


def get_default_gateway():
    res = subprocess.check_output(["ip", "route", "list"], text=True, stderr=subprocess.STDOUT)
    LOG(f"result of ip route list: {res}")
    line1 = res.splitlines()[0]
    if not line1.startswith("default via"):
        raise ValueError(f'Excpected "ip route list" to start with "default via" but not found. Got: [{line1}]')
    return line1.split()[2]


def check_network(network_check: str) -> str:
    """
    Check that Cloud provider metadata service is not exposed and that access to localhost is not available.
    """
    return_res = ""
    if network_check in ("all", "cloud_metadata"):
        LOG("Check cloud metadata server access...")
        try:
            res = requests.get(CLOUD_METADATA_URL, timeout=1)
            LOG(f"cloud metadata server returned successfuly: {res.status_code} {res.headers}")
            return_res += (
                f"Access to cloud metadata server: {CLOUD_METADATA_URL} is open. It seems that you haven't blocked "
                f"access to the cloud metadata server. Response status code: [{res.status_code}]. "
                f"Response headers: {res.headers}"
            )
        except Exception as ex:
            LOG(f"Cloud metadata server returned an exception (this is good. It means there is no access to the server.): {ex}")
    if network_check in ("all", "host_machine"):
        LOG("Check host access")
        gateway_ip = get_default_gateway()
        try:
            res = requests.get(f"https://{gateway_ip}/", verify=False, timeout=1)  # nosec # guardrails-disable-line
            LOG(f"Host https request returned successfully: {res.status_code} {res.headers}")
            if return_res:
                return_res += "\n"
            return_res += (
                f"Access to host server via default gateway ip: {gateway_ip} is open. It seems that "
                f"you haven't blocked access to the host server. Response status code: [{res.status_code}]."
                f"Response headers: {res.headers}"
            )
        except Exception as ex:
            LOG(
                "The host gateway server returned an exception (this is good."
                f" It means that there is no access to the host server.): {ex}"
            )
    return return_res


def main():
    if os.getenv("container") == "podman":
        return_error("This script only works in Docker containers. Podman is not supported")
        return

    mem = demisto.args().get("memory", "1g")
    mem_check = demisto.args().get("memory_check", "cgroup")
    network_check = demisto.args().get("network_check", "all")
    pids = int(demisto.args().get("pids", 256))
    fds_soft = int(demisto.args().get("fds_soft", 1024))
    fds_hard = int(demisto.args().get("fds_hard", 8192))
    cpus = int(demisto.args().get("cpus", 1))
    success = "Success"
    check = "Check"
    status = "Status"
    res = [
        {
            check: "Non-root User",
            status: check_non_root() or success,
        },
        {
            check: "Memory",
            status: check_memory(mem, mem_check) or success,
        },
        {
            check: "File Descriptors",
            status: check_fd_limits(fds_soft, fds_hard) or success,
        },
        {
            check: "CPUs",
            status: check_cpus(cpus) or success,
        },
        {
            check: "PIDs",
            status: check_pids(pids) or success,
        },
        {check: "Network", status: check_network(network_check) or success},
    ]
    failed = False
    failed_msg = ""
    for v in res:
        if v[status] != success:
            failed = True
            v[status] = "Failed: " + v[status]
            failed_msg += f"* {v[status]}\n"
    table = tableToMarkdown("Docker Hardening Results Check", res, [check, status])
    return_outputs(table)
    if failed:
        return_error(
            f"Failed verifying docker hardening:\n{failed_msg}"
            "More details at: https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Docker-Hardening-Guide"
        )  # noqa


# python2 uses __builtin__ python3 uses builtins
if __name__ == "__builtin__" or __name__ == "builtins":
    main()

README

Checks if the Docker container running this script has been hardened according to the recommended settings located in the Docker hardening guide (Cortex XSOAR 6.13) or Docker hardening guide (Cortex XSOAR 8 Cloud) or Docker hardening guide (Cortex XSOAR 8.7 On-prem).

Script Data


Name Description
Script Type python3
Tags Utility
Cortex XSOAR Version 5.0.0+

Inputs


Argument Name Description
memory The amount of memory to check. This is specified in bytes or append MB/GB for Mega/Giga bytes. The default is 1 GB.
memory_check The memory check type to perform: cgroup - check memory cgroup configuration, allocate - try allocating actual memory and verify that the allocation fails. Note the allocate test on some configurations may cause the container to be killed by the linux memory manager and the whole test will then time out.
pids The maximum number of PIDs to check.
fds_soft The soft file descriptor limit to check.
fds_hard The hard file descriptor limit to check.
cpus The number of CPUs limit to check.
network_check The network check to perform. cloud_metadata - check that access is blocked to cloud metadata server, host_machine - check that access is blocked to the host machine on the default gateway IP, all - perform all network tests.

Outputs


There are no outputs for this script.

Notes

  • Network Host Check: The network host check only checks available access on the default gateway’s IP using an https request to port 443. There still may be access available to the host network either on a different IP or port and this check will not detect it.