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.
- Type
- python
- Pack
- CommonScripts
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.