# Copyright 2016 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from reconbf.lib.logger import logger
import functools
import json
import os
import platform
import subprocess
import reconbf.lib.test_class as test_class
from reconbf.lib.result import Result, GroupTestResult, TestResult
from reconbf.lib import utils


@utils.idempotent
def _get_main_docker_processes():
    """Find all processes with the binary named "dockerd".
    """

    process_list = utils.running_processes()
    docker_ps = []

    for entry in process_list:
        cmdline = utils.cmdline_for_pid(entry[0])
        binary = cmdline[0]
        bin_base = os.path.basename(binary)
        if bin_base == b'dockerd' or (bin_base == b'docker' and
                                      len(cmdline) >= 2 and
                                      cmdline[1] == b'daemon'):
            docker_ps.append((entry[0], cmdline))

    return docker_ps


@utils.idempotent
def _get_docker_container():
    """Runs the docker ps -q command.

    :returns: The output of the docker ps command (minus heading).
    """
    containers = []
    try:
        containers = subprocess.check_output(['docker',
                                              'ps',
                                              '-q']).split(b'\n')
    except (subprocess.CalledProcessError, EnvironmentError):
        return None

    return [c for c in containers if c]


def _get_docker_info():
    """Runs the docker info command.

    :returns: Information about the docker installation including storage,
    kernel, os, and initpath information.
    """

    try:
        return subprocess.check_output(['docker', 'info'])
    except (subprocess.CalledProcessError, EnvironmentError):
        return None


@utils.idempotent
def _get_docker_inspect(container_id):
    """Runs the docker inspect command.

    :returns: JSON-formatted information about the container passed.
    """

    inspect = subprocess.check_output(['docker', 'inspect', container_id])
    return json.loads(inspect.decode('utf-8'))[0]


def _inspect_test(f):
    """Common wrapper for all the tests which return the result based
    on the "inspect" description of the container.

    `f` is supposed to return TestResult for a given inspect description.
    """
    @functools.wraps(f)
    def wrapper():
        containers = _get_docker_container()
        if not containers:
            return TestResult(Result.SKIP, "No Docker containers found.")

        results = GroupTestResult()
        for container_id in containers:
            inspect = _get_docker_inspect(container_id)
            check = "container " + str(container_id)

            result = f(inspect)
            results.add_result(check, result)

        return results

    return wrapper


@test_class.explanation(
    """
    Protection name: Restrict communication between containers.

    Check: In a ps output, ensure the '--icc' parameter is set to 'false'.

    Purpose: Best practice is to filter communication between containers
    through the host so that it is not necessary to duplicate the filtering
    and controls already implemented at that level.
    """)
def test_traffic():
    logger.debug("Testing for restricted traffic between containers.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--icc=false' in cmdline:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))
        else:
            reason = "Direct communication between containers is enabled."
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))

    return results


@test_class.explanation(
    """
    Protection name: Check Docker log level.

    Check: This check will look at the ps declaration to see if --log-level
    has been set there. If it is not it will look in the docker config files.
    In both cases it recommends info messages, and will warn on 'debug' level.

    Purpose: Good logging and auditing policies are general best-practice
    and it is not recommended to run at debug-level unless absolutely necessary
    both due to information collected and volume of events generated.
    """)
def test_log_level():
    logger.debug("Checking the Docker log level.")

    # if --log-level is set in ps, else find config file and check there
    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--log-level=info' in cmdline:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

        elif '--log-level=debug' in cmdline:
            reason = ("It is not recommended to run Docker in production "
                      "in debug mode.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))

        else:
            notes = "Recommended Docker log level is 'info'."
            results.add_result("pid %s" % pid, TestResult(Result.PASS, notes))

    return results


@test_class.explanation(
    """
    Protection name: Delegate firewall rules to Docker-server.

    Check: This check will look at the ps declaration to see if the
    '--iptables' parameter is not present or set to false.

    Purpose: If a user-defined firewall is not implemented, the Docker-server
    will automatically make changes to the container firewalls as defined by
    the environment. However if a user manually sets a firewall rule, the
    Docker-server will no longer maintain the firewall rules.
    """)
def test_iptables():
    logger.debug("Checking the firewall settings.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--iptables=false' in cmdline:
            reason = ("The iptables firewall is enabled on a per-container "
                      "basis and will need to be maintained by the user.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Check for insecure registries.

    Check: The output of a ps command will contain the --insecure-registry flag

    Purpose: Registries that are specified with the --insecure-registry flag
    are remote and ignoring any security checks that Docker builds in. This
    is not best practice, and these images should be stored in a local registry
    with proper integrity, communication, and validation controls applied.
    """)
def test_insecure_registries():
    logger.debug("Testing for insecure registries.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--insecure-registry' in cmdline:
            reason = ("A registry was specified with the --insecure-registry "
                      "flag.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Check if Docker is running inside LXC memory space.

    Check: Look at the ps to ensure no occurrence of lxc exists.

    Purpose: Docker was originally built inside LXC userspace, but has now
    extended outside of it, so both from a legacy and controls perspective
    it should be outside there.
    """)
def test_no_lxc():
    logger.debug("Testing if the container is running in LXC memory.")
    reason = "No Docker containers found."

    docker_ps = _get_main_docker_processes()
    if docker_ps is None:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()

    for pid, cmdline in docker_ps:
        if b'lxc' in cmdline:
            reason = "LXC in ps output."
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Ensure container is running in user namespace.

    Check: From the output of 'docker ps' use the id to inspect the container
    user.

    Purpose: Validating the container is running in user namespace ensures
    that memory used by the container is not being used by root.
    """)
@_inspect_test
def test_user_owned(inspect):
    user = inspect.get("Config", {}).get("User")

    if not user:
        reason = ("Container is running in root namespace.")
        return TestResult(Result.FAIL, reason)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Check storage driver.

    Check: This check will ensure the storage driver isn't aufs.

    Purpose: The aufs driver is insecure and allows a process to escape
    the container. As such it should not be used.
    """)
def test_storage_driver():
    logger.debug("Checking storage driver.")

    info = _get_docker_info()
    if not info:
        return TestResult(Result.SKIP, "Cannot get docker info.")
    driver = [l for l in info.splitlines()
              if l.startswith(b'Storage Driver:')]

    if driver:
        if b'aufs' in driver[0]:
            notes = "Storage driver set to insecure aufs."
            return TestResult(Result.FAIL, notes)
        else:
            return TestResult(Result.PASS)
    else:
        # empty driver, odd failure
        return TestResult(Result.SKIP, "Cannot find storage driver")


@test_class.explanation(
    """
    Protection name: Audit Docker daemon

    Check: Look through the output of the auditctl command for an entry
    for /usr/bin/docker to show the process is being managed by the
    kernel audit system

    Purpose: Auditctl is a "Controlled Access Protection Profiles"
    framework that helps gather information about system events. These
    are managed by the auditctl policy, and ensuring the Docker daemon
    is included in these checks will help monitoring of a Docker
    environment. Check is only valid for pre-3.11 kernels.
    """)
def test_docker_daemon():
    logger.debug("Checking auditing on the Docker daemon.")

    if platform.system() != 'Linux':
        return TestResult(Result.SKIP, "applies to Linux only")

    note = "Test is invalid for newer kernels."

    kernel = tuple(int(x) for x in os.uname()[2].split('.')[:2])
    if kernel >= (3, 12):
        return TestResult(Result.SKIP, note)

    try:
        subprocess.check_output(['which', 'auditctl'])
    except subprocess.CalledProcessError:
        note = "The auditctl command is not installed."
        return TestResult(Result.SKIP, note)

    audit = subprocess.check_output(['auditctl', '-l'])
    if b'/usr/bin/docker' in audit:
        return TestResult(Result.PASS)
    else:
        note = "/usr/bin/docker is not being tracked in auditctl."
        return TestResult(Result.FAIL, note)


@test_class.explanation(
    """
    Protection name: Privileged container checks

    Check: Check to ensure that privileged containers
    are not being used.

    Purpose: The --privileged flag gives all capabilities
    to the container, and it also lifts all the limitations
    enforced by the device cgroup controller. If this flag is
    set to true, this presents a vulnerability as this flag allows
    the container can then do almost everything that the host can do.
    """)
@_inspect_test
def test_docker_privilege(inspect):
    privileged = inspect.get("HostConfig", {}).get("Privileged")

    if not privileged:
        return TestResult(Result.PASS)
    else:
        notes = "Container is running with privileged flags set to true."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Container memory limitations.

    Check: Checks to ensure that memory limitations are set.

    Purpose: If memory limitations are not set this can cause an
    inadvertant Denial of Service for the host machine as docker can
    use all of the allotted memory given to a host machine.
    """)
@_inspect_test
def test_memory_limit(inspect):
    memory = inspect.get("HostConfig", {}).get("Memory")
    if memory is None:
        return TestResult(Result.SKIP, "Memory limit cannot be found")
    elif memory <= 0:
        return TestResult(Result.FAIL, "No memory limit set")
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Network Mode value.

    Check: Containers should not use "host" Network
    Mode.

    Purpose: If Network Mode is set to host there is the
    potential that containers will be allowing processes
    to open low-numbered ports like any other root process.
    """)
@_inspect_test
def test_host_network_mode(inspect):
    net_mode = inspect.get("HostConfig", {}).get("NetworkMode")

    if net_mode != 'host':
        return TestResult(Result.PASS)
    else:
        notes = "Container is running in host Network Mode."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Check CPU priority settings.

    Check: Containers should be checked that CPU priority
    is set appropriately.

    Purpose: If a return of 0 or 1024 is given, it means
    the CPU shares are not in place. Some containers may
    require more CPU allocation than others therefore not
    setting shares could lead to an inadvertant Denial of
    Service.
    """)
@_inspect_test
def test_cpu_priority(inspect):
    shares = inspect.get("HostConfig", {}).get("CpuShares", 1024)

    if not shares or shares == 1024:
        notes = "Container do not have CPU shares in place."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Read only root file system.

    Check: Check to ensure that container's root filesystem
    is mounted as read only.

    Purpose: Data should not be written within containers. The
    data volume belonging to a container should be explicitly
    defined and administered.
    """)
@_inspect_test
def test_read_only_root_fs(inspect):
    readonly = inspect.get("HostConfig", {}).get("ReadonlyRootfs", False)

    if readonly:
        return TestResult(Result.PASS)
    else:
        notes = "Container uses a root filesystem which is not read only."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Limit container restart tries.

    Check: Check to ensure that container's restart policy settings
    are set appropriately.

    Purpose: If you indefinitely keep trying to start the container,
    it could possibly lead to a denial of service on the host, therefore
    tries to restart the container should be set to no more than 5. If a
    container's value is set to 'RestartPolicyName=no' or just 'RestartPolicy
    Name=' this is considered compliant as the container will never attempt to
    restart.
    """)
@_inspect_test
def test_restart_policy(inspect):
    policy_name = inspect.get("HostConfig", {}).get(
        "RestartPolicy", {}).get("Name")
    max_retry = inspect.get("HostConfig", {}).get(
        "RestartPolicy", {}).get("MaximumRetryCount")

    if policy_name == 'no' or policy_name is None:
        return TestResult(Result.PASS)
    elif policy_name == 'always':
        notes = ("Container will always restart regardless of max retry "
                 "count. This is not recommended.")
        return TestResult(Result.FAIL, notes)
    elif policy_name == 'on-failure':
        if int(max_retry) <= 5:
            return TestResult(Result.PASS)
        else:
            notes = "Container max retry count set to a high level."
            return TestResult(Result.FAIL, notes)
    else:
        notes = "Unknown restart policy."
        return TestResult(Result.SKIP, notes)


@test_class.explanation(
    """
    Protection name: Host process namespace sharing

    Check: Check that containers are not sharing host
    process namespaces.

    Purpose: If host process namespaces are being shared with
    containers it would allow processes within the container
    to see all of the processes on the host system. Thus breaking
    the benefit of process level isolation between the host and
    the containers.
    """)
@_inspect_test
def test_docker_pid_mode(inspect):
    pid_mode = inspect.get("HostConfig", {}).get("PidMode")

    if pid_mode == 'host':
        notes = "Container is sharing host process namespaces."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Sensitive host system directories

    Check: Check that containers are not mounting sensitive
    host system directories on containers.

    Purpose: If sensitive directories are mounted in read-write mode,
    it would be possible to make changes to files within those sensitive
    directories. The changes might bring down security implications or
    unwarranted changes that could put the Docker host in compromised
    state.
    """)
@_inspect_test
def test_mount_sensitive_directories(inspect):
    binds = inspect.get("HostConfig", {}).get("Binds") or []

    for bind in binds:
        parts = bind.split(':')
        src = parts[0]
        opts = parts[-1] if len(parts) > 2 else None
        rw = bool(opts) and ('rw' in opts)

        if rw and (src.startswith('/usr') or src.startswith('/etc') or
                   src.startswith('/bin') or src.startswith('/boot')):
            notes = ("Container has sensitive host system directories "
                     "mounted read-write: %s" % src)
            return TestResult(Result.FAIL, notes)

    return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Host IPC namespace

    Check: Check that containers are not sharing namespaces with
    host IPCs.

    Purpose: IPC namespace provides separation of IPC between the host and
    containers. If the host's IPC namespace is shared with the container, it
    would basically allow processes within the container to see all of the
    IPC on the host system.
    """)
@_inspect_test
def test_IPC_host(inspect):
    ipc_mode = inspect.get("HostConfig", {}).get("IpcMode")

    if ipc_mode == 'host':
        notes = "Container is sharing IPC namespace with the host."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Ulimit default override

    Check: Check that containers are not running with ulimit
    defaults.

    Purpose: Ulimit provides control over the resources
    available to the shell and to processes started by it.
    Setting system resource limits judiciously saves you from
    many vulnerabilities such as a fork bomb.
    """)
@_inspect_test
def test_ulimit_default_override(inspect):
    ulimits = inspect.get("HostConfig", {}).get("Ulimits")

    if ulimits is None:
        notes = "Container is running with default ulimits in place."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)

# Copyright 2016 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from reconbf.lib.logger import logger
import functools
import json
import os
import platform
import subprocess
import reconbf.lib.test_class as test_class
from reconbf.lib.result import Result, GroupTestResult, TestResult
from reconbf.lib import utils


@utils.idempotent
def _get_main_docker_processes():
    """Find all processes with the binary named "dockerd".
    """

    process_list = utils.running_processes()
    docker_ps = []

    for entry in process_list:
        cmdline = utils.cmdline_for_pid(entry[0])
        binary = cmdline[0]
        bin_base = os.path.basename(binary)
        if bin_base == b'dockerd' or (bin_base == b'docker' and
                                      len(cmdline) >= 2 and
                                      cmdline[1] == b'daemon'):
            docker_ps.append((entry[0], cmdline))

    return docker_ps


@utils.idempotent
def _get_docker_container():
    """Runs the docker ps -q command.

    :returns: The output of the docker ps command (minus heading).
    """
    containers = []
    try:
        containers = subprocess.check_output(['docker',
                                              'ps',
                                              '-q']).split(b'\n')
    except (subprocess.CalledProcessError, EnvironmentError):
        return None

    return [c for c in containers if c]


def _get_docker_info():
    """Runs the docker info command.

    :returns: Information about the docker installation including storage,
    kernel, os, and initpath information.
    """

    try:
        return subprocess.check_output(['docker', 'info'])
    except (subprocess.CalledProcessError, EnvironmentError):
        return None


@utils.idempotent
def _get_docker_inspect(container_id):
    """Runs the docker inspect command.

    :returns: JSON-formatted information about the container passed.
    """

    inspect = subprocess.check_output(['docker', 'inspect', container_id])
    return json.loads(inspect.decode('utf-8'))[0]


def _inspect_test(f):
    """Common wrapper for all the tests which return the result based
    on the "inspect" description of the container.

    `f` is supposed to return TestResult for a given inspect description.
    """
    @functools.wraps(f)
    def wrapper():
        containers = _get_docker_container()
        if not containers:
            return TestResult(Result.SKIP, "No Docker containers found.")

        results = GroupTestResult()
        for container_id in containers:
            inspect = _get_docker_inspect(container_id)
            check = "container " + str(container_id)

            result = f(inspect)
            results.add_result(check, result)

        return results

    return wrapper


@test_class.explanation(
    """
    Protection name: Restrict communication between containers.

    Check: In a ps output, ensure the '--icc' parameter is set to 'false'.

    Purpose: Best practice is to filter communication between containers
    through the host so that it is not necessary to duplicate the filtering
    and controls already implemented at that level.
    """)
def test_traffic():
    logger.debug("Testing for restricted traffic between containers.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--icc=false' in cmdline:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))
        else:
            reason = "Direct communication between containers is enabled."
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))

    return results


@test_class.explanation(
    """
    Protection name: Check Docker log level.

    Check: This check will look at the ps declaration to see if --log-level
    has been set there. If it is not it will look in the docker config files.
    In both cases it recommends info messages, and will warn on 'debug' level.

    Purpose: Good logging and auditing policies are general best-practice
    and it is not recommended to run at debug-level unless absolutely necessary
    both due to information collected and volume of events generated.
    """)
def test_log_level():
    logger.debug("Checking the Docker log level.")

    # if --log-level is set in ps, else find config file and check there
    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--log-level=info' in cmdline:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

        elif '--log-level=debug' in cmdline:
            reason = ("It is not recommended to run Docker in production "
                      "in debug mode.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))

        else:
            notes = "Recommended Docker log level is 'info'."
            results.add_result("pid %s" % pid, TestResult(Result.PASS, notes))

    return results


@test_class.explanation(
    """
    Protection name: Delegate firewall rules to Docker-server.

    Check: This check will look at the ps declaration to see if the
    '--iptables' parameter is not present or set to false.

    Purpose: If a user-defined firewall is not implemented, the Docker-server
    will automatically make changes to the container firewalls as defined by
    the environment. However if a user manually sets a firewall rule, the
    Docker-server will no longer maintain the firewall rules.
    """)
def test_iptables():
    logger.debug("Checking the firewall settings.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--iptables=false' in cmdline:
            reason = ("The iptables firewall is enabled on a per-container "
                      "basis and will need to be maintained by the user.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Check for insecure registries.

    Check: The output of a ps command will contain the --insecure-registry flag

    Purpose: Registries that are specified with the --insecure-registry flag
    are remote and ignoring any security checks that Docker builds in. This
    is not best practice, and these images should be stored in a local registry
    with proper integrity, communication, and validation controls applied.
    """)
def test_insecure_registries():
    logger.debug("Testing for insecure registries.")

    dockers = _get_main_docker_processes()
    if not dockers:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()
    for pid, cmdline in dockers:
        if '--insecure-registry' in cmdline:
            reason = ("A registry was specified with the --insecure-registry "
                      "flag.")
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Check if Docker is running inside LXC memory space.

    Check: Look at the ps to ensure no occurrence of lxc exists.

    Purpose: Docker was originally built inside LXC userspace, but has now
    extended outside of it, so both from a legacy and controls perspective
    it should be outside there.
    """)
def test_no_lxc():
    logger.debug("Testing if the container is running in LXC memory.")
    reason = "No Docker containers found."

    docker_ps = _get_main_docker_processes()
    if docker_ps is None:
        return TestResult(Result.SKIP, "Docker is not running.")

    results = GroupTestResult()

    for pid, cmdline in docker_ps:
        if b'lxc' in cmdline:
            reason = "LXC in ps output."
            results.add_result("pid %s" % pid, TestResult(Result.FAIL, reason))
        else:
            results.add_result("pid %s" % pid, TestResult(Result.PASS))

    return results


@test_class.explanation(
    """
    Protection name: Ensure container is running in user namespace.

    Check: From the output of 'docker ps' use the id to inspect the container
    user.

    Purpose: Validating the container is running in user namespace ensures
    that memory used by the container is not being used by root.
    """)
@_inspect_test
def test_user_owned(inspect):
    user = inspect.get("Config", {}).get("User")

    if not user:
        reason = ("Container is running in root namespace.")
        return TestResult(Result.FAIL, reason)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Check storage driver.

    Check: This check will ensure the storage driver isn't aufs.

    Purpose: The aufs driver is insecure and allows a process to escape
    the container. As such it should not be used.
    """)
def test_storage_driver():
    logger.debug("Checking storage driver.")

    info = _get_docker_info()
    if not info:
        return TestResult(Result.SKIP, "Cannot get docker info.")
    driver = [l for l in info.splitlines()
              if l.startswith(b'Storage Driver:')]

    if driver:
        if b'aufs' in driver[0]:
            notes = "Storage driver set to insecure aufs."
            return TestResult(Result.FAIL, notes)
        else:
            return TestResult(Result.PASS)
    else:
        # empty driver, odd failure
        return TestResult(Result.SKIP, "Cannot find storage driver")


@test_class.explanation(
    """
    Protection name: Audit Docker daemon

    Check: Look through the output of the auditctl command for an entry
    for /usr/bin/docker to show the process is being managed by the
    kernel audit system

    Purpose: Auditctl is a "Controlled Access Protection Profiles"
    framework that helps gather information about system events. These
    are managed by the auditctl policy, and ensuring the Docker daemon
    is included in these checks will help monitoring of a Docker
    environment. Check is only valid for pre-3.11 kernels.
    """)
def test_docker_daemon():
    logger.debug("Checking auditing on the Docker daemon.")

    if platform.system() != 'Linux':
        return TestResult(Result.SKIP, "applies to Linux only")

    note = "Test is invalid for newer kernels."

    kernel = tuple(int(x) for x in os.uname()[2].split('.')[:2])
    if kernel >= (3, 12):
        return TestResult(Result.SKIP, note)

    try:
        subprocess.check_output(['which', 'auditctl'])
    except subprocess.CalledProcessError:
        note = "The auditctl command is not installed."
        return TestResult(Result.SKIP, note)

    audit = subprocess.check_output(['auditctl', '-l'])
    if b'/usr/bin/docker' in audit:
        return TestResult(Result.PASS)
    else:
        note = "/usr/bin/docker is not being tracked in auditctl."
        return TestResult(Result.FAIL, note)


@test_class.explanation(
    """
    Protection name: Privileged container checks

    Check: Check to ensure that privileged containers
    are not being used.

    Purpose: The --privileged flag gives all capabilities
    to the container, and it also lifts all the limitations
    enforced by the device cgroup controller. If this flag is
    set to true, this presents a vulnerability as this flag allows
    the container can then do almost everything that the host can do.
    """)
@_inspect_test
def test_docker_privilege(inspect):
    privileged = inspect.get("HostConfig", {}).get("Privileged")

    if not privileged:
        return TestResult(Result.PASS)
    else:
        notes = "Container is running with privileged flags set to true."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Container memory limitations.

    Check: Checks to ensure that memory limitations are set.

    Purpose: If memory limitations are not set this can cause an
    inadvertant Denial of Service for the host machine as docker can
    use all of the allotted memory given to a host machine.
    """)
@_inspect_test
def test_memory_limit(inspect):
    memory = inspect.get("HostConfig", {}).get("Memory")
    if memory is None:
        return TestResult(Result.SKIP, "Memory limit cannot be found")
    elif memory <= 0:
        return TestResult(Result.FAIL, "No memory limit set")
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Network Mode value.

    Check: Containers should not use "host" Network
    Mode.

    Purpose: If Network Mode is set to host there is the
    potential that containers will be allowing processes
    to open low-numbered ports like any other root process.
    """)
@_inspect_test
def test_host_network_mode(inspect):
    net_mode = inspect.get("HostConfig", {}).get("NetworkMode")

    if net_mode != 'host':
        return TestResult(Result.PASS)
    else:
        notes = "Container is running in host Network Mode."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Check CPU priority settings.

    Check: Containers should be checked that CPU priority
    is set appropriately.

    Purpose: If a return of 0 or 1024 is given, it means
    the CPU shares are not in place. Some containers may
    require more CPU allocation than others therefore not
    setting shares could lead to an inadvertant Denial of
    Service.
    """)
@_inspect_test
def test_cpu_priority(inspect):
    shares = inspect.get("HostConfig", {}).get("CpuShares", 1024)

    if not shares or shares == 1024:
        notes = "Container do not have CPU shares in place."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Read only root file system.

    Check: Check to ensure that container's root filesystem
    is mounted as read only.

    Purpose: Data should not be written within containers. The
    data volume belonging to a container should be explicitly
    defined and administered.
    """)
@_inspect_test
def test_read_only_root_fs(inspect):
    readonly = inspect.get("HostConfig", {}).get("ReadonlyRootfs", False)

    if readonly:
        return TestResult(Result.PASS)
    else:
        notes = "Container uses a root filesystem which is not read only."
        return TestResult(Result.FAIL, notes)


@test_class.explanation(
    """
    Protection name: Limit container restart tries.

    Check: Check to ensure that container's restart policy settings
    are set appropriately.

    Purpose: If you indefinitely keep trying to start the container,
    it could possibly lead to a denial of service on the host, therefore
    tries to restart the container should be set to no more than 5. If a
    container's value is set to 'RestartPolicyName=no' or just 'RestartPolicy
    Name=' this is considered compliant as the container will never attempt to
    restart.
    """)
@_inspect_test
def test_restart_policy(inspect):
    policy_name = inspect.get("HostConfig", {}).get(
        "RestartPolicy", {}).get("Name")
    max_retry = inspect.get("HostConfig", {}).get(
        "RestartPolicy", {}).get("MaximumRetryCount")

    if policy_name == 'no' or policy_name is None:
        return TestResult(Result.PASS)
    elif policy_name == 'always':
        notes = ("Container will always restart regardless of max retry "
                 "count. This is not recommended.")
        return TestResult(Result.FAIL, notes)
    elif policy_name == 'on-failure':
        if int(max_retry) <= 5:
            return TestResult(Result.PASS)
        else:
            notes = "Container max retry count set to a high level."
            return TestResult(Result.FAIL, notes)
    else:
        notes = "Unknown restart policy."
        return TestResult(Result.SKIP, notes)


@test_class.explanation(
    """
    Protection name: Host process namespace sharing

    Check: Check that containers are not sharing host
    process namespaces.

    Purpose: If host process namespaces are being shared with
    containers it would allow processes within the container
    to see all of the processes on the host system. Thus breaking
    the benefit of process level isolation between the host and
    the containers.
    """)
@_inspect_test
def test_docker_pid_mode(inspect):
    pid_mode = inspect.get("HostConfig", {}).get("PidMode")

    if pid_mode == 'host':
        notes = "Container is sharing host process namespaces."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Sensitive host system directories

    Check: Check that containers are not mounting sensitive
    host system directories on containers.

    Purpose: If sensitive directories are mounted in read-write mode,
    it would be possible to make changes to files within those sensitive
    directories. The changes might bring down security implications or
    unwarranted changes that could put the Docker host in compromised
    state.
    """)
@_inspect_test
def test_mount_sensitive_directories(inspect):
    binds = inspect.get("HostConfig", {}).get("Binds") or []

    for bind in binds:
        parts = bind.split(':')
        src = parts[0]
        opts = parts[-1] if len(parts) > 2 else None
        rw = bool(opts) and ('rw' in opts)

        if rw and (src.startswith('/usr') or src.startswith('/etc') or
                   src.startswith('/bin') or src.startswith('/boot')):
            notes = ("Container has sensitive host system directories "
                     "mounted read-write: %s" % src)
            return TestResult(Result.FAIL, notes)

    return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Host IPC namespace

    Check: Check that containers are not sharing namespaces with
    host IPCs.

    Purpose: IPC namespace provides separation of IPC between the host and
    containers. If the host's IPC namespace is shared with the container, it
    would basically allow processes within the container to see all of the
    IPC on the host system.
    """)
@_inspect_test
def test_IPC_host(inspect):
    ipc_mode = inspect.get("HostConfig", {}).get("IpcMode")

    if ipc_mode == 'host':
        notes = "Container is sharing IPC namespace with the host."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)


@test_class.explanation(
    """
    Protection name: Ulimit default override

    Check: Check that containers are not running with ulimit
    defaults.

    Purpose: Ulimit provides control over the resources
    available to the shell and to processes started by it.
    Setting system resource limits judiciously saves you from
    many vulnerabilities such as a fork bomb.
    """)
@_inspect_test
def test_ulimit_default_override(inspect):
    ulimits = inspect.get("HostConfig", {}).get("Ulimits")

    if ulimits is None:
        notes = "Container is running with default ulimits in place."
        return TestResult(Result.FAIL, notes)
    else:
        return TestResult(Result.PASS)

# Copyright 2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # #     http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. Find all processes with the binary named "dockerd". Runs the docker ps -q command.      :returns: The output of the docker ps command (minus heading). Runs the docker info command.      :returns: Information about the docker installation including storage,     kernel, os, and initpath information. Runs the docker inspect command.      :returns: JSON-formatted information about the container passed. Common wrapper for all the tests which return the result based     on the "inspect" description of the container.      `f` is supposed to return TestResult for a given inspect description. Protection name: Restrict communication between containers.      Check: In a ps output, ensure the '--icc' parameter is set to 'false'.      Purpose: Best practice is to filter communication between containers     through the host so that it is not necessary to duplicate the filtering     and controls already implemented at that level. Protection name: Check Docker log level.      Check: This check will look at the ps declaration to see if --log-level     has been set there. If it is not it will look in the docker config files.     In both cases it recommends info messages, and will warn on 'debug' level.      Purpose: Good logging and auditing policies are general best-practice     and it is not recommended to run at debug-level unless absolutely necessary     both due to information collected and volume of events generated. # if --log-level is set in ps, else find config file and check there Protection name: Delegate firewall rules to Docker-server.      Check: This check will look at the ps declaration to see if the     '--iptables' parameter is not present or set to false.      Purpose: If a user-defined firewall is not implemented, the Docker-server     will automatically make changes to the container firewalls as defined by     the environment. However if a user manually sets a firewall rule, the     Docker-server will no longer maintain the firewall rules. Protection name: Check for insecure registries.      Check: The output of a ps command will contain the --insecure-registry flag      Purpose: Registries that are specified with the --insecure-registry flag     are remote and ignoring any security checks that Docker builds in. This     is not best practice, and these images should be stored in a local registry     with proper integrity, communication, and validation controls applied. Protection name: Check if Docker is running inside LXC memory space.      Check: Look at the ps to ensure no occurrence of lxc exists.      Purpose: Docker was originally built inside LXC userspace, but has now     extended outside of it, so both from a legacy and controls perspective     it should be outside there. Protection name: Ensure container is running in user namespace.      Check: From the output of 'docker ps' use the id to inspect the container     user.      Purpose: Validating the container is running in user namespace ensures     that memory used by the container is not being used by root. Protection name: Check storage driver.      Check: This check will ensure the storage driver isn't aufs.      Purpose: The aufs driver is insecure and allows a process to escape     the container. As such it should not be used. # empty driver, odd failure Protection name: Audit Docker daemon      Check: Look through the output of the auditctl command for an entry     for /usr/bin/docker to show the process is being managed by the     kernel audit system      Purpose: Auditctl is a "Controlled Access Protection Profiles"     framework that helps gather information about system events. These     are managed by the auditctl policy, and ensuring the Docker daemon     is included in these checks will help monitoring of a Docker     environment. Check is only valid for pre-3.11 kernels. Protection name: Privileged container checks      Check: Check to ensure that privileged containers     are not being used.      Purpose: The --privileged flag gives all capabilities     to the container, and it also lifts all the limitations     enforced by the device cgroup controller. If this flag is     set to true, this presents a vulnerability as this flag allows     the container can then do almost everything that the host can do. Protection name: Container memory limitations.      Check: Checks to ensure that memory limitations are set.      Purpose: If memory limitations are not set this can cause an     inadvertant Denial of Service for the host machine as docker can     use all of the allotted memory given to a host machine. Protection name: Network Mode value.      Check: Containers should not use "host" Network     Mode.      Purpose: If Network Mode is set to host there is the     potential that containers will be allowing processes     to open low-numbered ports like any other root process. Protection name: Check CPU priority settings.      Check: Containers should be checked that CPU priority     is set appropriately.      Purpose: If a return of 0 or 1024 is given, it means     the CPU shares are not in place. Some containers may     require more CPU allocation than others therefore not     setting shares could lead to an inadvertant Denial of     Service. Protection name: Read only root file system.      Check: Check to ensure that container's root filesystem     is mounted as read only.      Purpose: Data should not be written within containers. The     data volume belonging to a container should be explicitly     defined and administered. Protection name: Limit container restart tries.      Check: Check to ensure that container's restart policy settings     are set appropriately.      Purpose: If you indefinitely keep trying to start the container,     it could possibly lead to a denial of service on the host, therefore     tries to restart the container should be set to no more than 5. If a     container's value is set to 'RestartPolicyName=no' or just 'RestartPolicy     Name=' this is considered compliant as the container will never attempt to     restart. Protection name: Host process namespace sharing      Check: Check that containers are not sharing host     process namespaces.      Purpose: If host process namespaces are being shared with     containers it would allow processes within the container     to see all of the processes on the host system. Thus breaking     the benefit of process level isolation between the host and     the containers. Protection name: Sensitive host system directories      Check: Check that containers are not mounting sensitive     host system directories on containers.      Purpose: If sensitive directories are mounted in read-write mode,     it would be possible to make changes to files within those sensitive     directories. The changes might bring down security implications or     unwarranted changes that could put the Docker host in compromised     state. Protection name: Host IPC namespace      Check: Check that containers are not sharing namespaces with     host IPCs.      Purpose: IPC namespace provides separation of IPC between the host and     containers. If the host's IPC namespace is shared with the container, it     would basically allow processes within the container to see all of the     IPC on the host system. Protection name: Ulimit default override      Check: Check that containers are not running with ulimit     defaults.      Purpose: Ulimit provides control over the resources     available to the shell and to processes started by it.     Setting system resource limits judiciously saves you from     many vulnerabilities such as a fork bomb.