├── audits ├── __init__.py ├── audit.py ├── host.py ├── dock.py └── containers.py ├── utils ├── __init__.py ├── decorators.py ├── confparse.py └── output.py ├── requirements.txt ├── CHANGELOG.md ├── .gitignore ├── README.md ├── drydock.py ├── conf └── default.yml └── LICENSE /audits/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | def assign_order(order): 2 | def assign(func): 3 | func.order = order 4 | return func 5 | return assign -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==3.11 2 | argparse==1.2.1 3 | backports.ssl-match-hostname==3.4.0.2 4 | colorama==0.3.3 5 | docker-py==1.5.0 6 | psutil==3.2.2 7 | requests==2.8.1 8 | six==1.10.0 9 | websocket-client==0.34.0 10 | wsgiref==0.1.2 11 | junit-xml==1.6 12 | -------------------------------------------------------------------------------- /utils/confparse.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import sys 3 | import logging 4 | 5 | class ConfParse: 6 | 7 | def load_conf(self,conf): 8 | """ 9 | Loads and parses an audit category from a configuration file 10 | """ 11 | try: 12 | with open(conf) as conf: 13 | profile = yaml.load(conf) 14 | except IOError: 15 | logging.error("Invalid file specified: %s" %(conf)) 16 | sys.exit(0) 17 | return profile -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | ### v0.3 - 09/11/15 4 | * Added support for Docker daemons listening on TCP sockets 5 | * Audit categories can be completely removed from configuration file. 6 | * Overview is now displayed last in the output screen 7 | * Removed obsolete code 8 | * Bug fixes in some audits 9 | 10 | 11 | ### v0.2 - 30/10/15 12 | * Bug fixes in some audits 13 | * Added some exception handling 14 | * Better parsing of the configuration file 15 | * Cleaner, more portable audit code 16 | 17 | ### v0.1 - 23/10/15 18 | > **Initial release** 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #What is drydock? 2 | 3 | NOTICE: Development is temporarily slowed down due to involvement with Docker's [Actuary](https://github.com/diogomonica/actuary). Feel free to make PRs, I will review ASAP, and be patient for updates :) 4 | 5 | **drydock** is a Docker security audit tool written in Python. It was initially inspired by [docker-bench-security](https://github.com/docker/docker-bench-security) but aims to provide a more flexible way for assesing Docker installations and deployments. drydock allows easy creation and use of **custom audit profiles** in order to eliminate noise and false alarms. Reports are saved in JSON format for easier parsing. drydock makes heavy use of [docker-py](https://github.com/docker/docker-py) client API to communicate with Docker. 6 | 7 | At the moment all of the security checks performed are based on the [CIS Docker 1.6 Benchmark](https://benchmarks.cisecurity.org/tools2/docker/CIS_Docker_1.6_Benchmark_v1.0.0.pdf). 8 | 9 | ## Usage 10 | Using drydock is as simple as : 11 | 12 | ```sh 13 | git clone https://github.com/zuBux/drydock.git 14 | pip install -r requirements.txt 15 | python drydock.py 16 | ``` 17 | A profile containing all checks is provided in conf/default.yaml and can be used as reference for creating custom profiles. You can disable an audit by commenting it out (and its options, if any). 18 | 19 | Since there are audits which require administrative privileges (e.x examining auditd rules) **users are advised to run drydock as root** for more accurate results. 20 | 21 | ### Local Docker host 22 | Assuming that your Docker daemon uses unix sockets (default configuration), the following options are available: 23 | 24 | * -o : Specifies the path where JSON output will be saved. Switches to output.json if none specified. 25 | * -p : The profile which will be used for the audit. Switches to conf/default.yaml if none specified. 26 | * -v : Use values 1, 2 or 3 to change verbosity level to ERROR, WARNING or DEBUG accordingly. Default is 1 27 | * -f : Output format. Supports JSON (-f json) and JUnit XML (-f xml). Default is JSON 28 | Example: 29 | ``` 30 | python drydock.py -o audit_aws -f xml -p conf/myprofile.yml -v 2 31 | ``` 32 | ### Remote Docker host 33 | If your Docker daemon listens on an exposed port, using TLS, you must provide the following : 34 | 35 | * -d <*IP:port*> Docker daemon IP and listening port 36 | * -c <*path*> Client certificate 37 | * -k <*path*> Client certificate key 38 | 39 | Example: 40 | ``` 41 | python drydock.py -d 10.0.0.2:2736 -c /home/user/cert/cert.pem -k /home/user/cert/cert.key -o audit_remote -p conf/myprofile.yml 42 | ``` 43 | ## TODO 44 | - Migrate checks to CIS Docker 1.11 Benchmark 45 | 46 | ## Contributions 47 | drydock is in beta stage and **needs testing under different environments** (currently tested only on Ubuntu/Debian deployments). All contributions ( bugs/improvements/suggestions etc. ) are welcome! 48 | -------------------------------------------------------------------------------- /drydock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import logging 4 | import sys 5 | 6 | from audits.host import HostConfAudit 7 | from audits.dock import DockerConfAudit, DockerFileAudit 8 | from audits.containers import ContainerImgAudit, ContainerRuntimeAudit 9 | 10 | from utils.confparse import ConfParse 11 | from utils.output import FormattedOutput 12 | 13 | def main(): 14 | # Argument parsing. 15 | confparser = ConfParse() 16 | parser = argparse.ArgumentParser() 17 | 18 | parser.add_argument("-p", "--profile",help="Audit configuration file") 19 | parser.add_argument("-o", "--output",help="Output file", default="output") 20 | parser.add_argument("-v", "--verbosity",help="Verbosity level") 21 | parser.add_argument("-d", "--daemon",help="Docker daemon host ") 22 | parser.add_argument("-c", "--cert",help="Client certificate") 23 | parser.add_argument("-k", "--key",help="Client certificate key") 24 | parser.add_argument("-f", "--format",help="JUnit XML or JSON", default="json") 25 | args = parser.parse_args() 26 | 27 | # Verbosity level - Default is ERROR 28 | if args.verbosity: 29 | verbosity = args.verbosity 30 | if verbosity == '1': 31 | loglevel = logging.ERROR 32 | elif verbosity == '2': 33 | loglevel = logging.WARNING 34 | elif verbosity == '3': 35 | loglevel = logging.DEBUG 36 | else: 37 | loglevel = logging.ERROR 38 | logging.basicConfig(level=loglevel, 39 | format='%(asctime)s - %(levelname)s - %(message)s') 40 | 41 | # Parse format 42 | if args.format.lower() != 'xml' and args.format.lower() != 'json': 43 | logging.error("Invalid option %s - it should be either json or xml" % (args.format)) 44 | sys.exit(1) 45 | 46 | # If no profile specified, switch to default 47 | if args.profile: 48 | conf = args.profile 49 | logging.info("Using profile %s" %(conf)) 50 | else: 51 | conf = "conf/default.yml" 52 | logging.warning("No profile selected. Using default %s" %(conf)) 53 | # If no output file is selected, switch to default 54 | outfile = args.output + "." + args.format 55 | if args.daemon: 56 | daemon = args.daemon 57 | if args.cert: 58 | cert = args.cert 59 | if args.key: 60 | key = args.key 61 | 62 | profile = confparser.load_conf(conf) 63 | audit_categories = { 64 | 'dockerconf': DockerConfAudit(), 65 | 'dockerfiles': DockerFileAudit(), 66 | } 67 | if args.daemon and args.cert and args.key: 68 | audit_categories['host'] = HostConfAudit(url=daemon, cert=cert, key=key) 69 | audit_categories['container_imgs'] = ContainerImgAudit(url=daemon, cert=cert, key=key) 70 | audit_categories['container_runtime'] = ContainerRuntimeAudit(url=daemon, cert=cert, key=key) 71 | elif args.daemon: 72 | audit_categories['host'] = HostConfAudit(url=daemon) 73 | audit_categories['container_imgs'] = ContainerImgAudit(url=daemon) 74 | audit_categories['container_runtime'] = ContainerRuntimeAudit(url=daemon) 75 | else: 76 | audit_categories['host'] = HostConfAudit() 77 | audit_categories['container_imgs'] = ContainerImgAudit() 78 | audit_categories['container_runtime'] = ContainerRuntimeAudit() 79 | 80 | out = FormattedOutput(outfile, **audit_categories) 81 | for cat,auditclass in audit_categories.iteritems(): 82 | if cat in profile.keys(): 83 | try: 84 | auditcat = profile[cat] 85 | audit = auditclass 86 | audit.run_audits(auditcat) 87 | out.save_results(cat, audit.logdict) 88 | except KeyError: 89 | logging.error("No audit category '%s' defined." %cat) 90 | 91 | out.audit_init_info(conf) 92 | if args.format == 'json': 93 | out.write_file() 94 | else: 95 | out.write_xml_file() 96 | out.terminal_output() 97 | 98 | if __name__ =='__main__': 99 | main() 100 | -------------------------------------------------------------------------------- /audits/audit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import psutil 5 | from docker import Client 6 | from collections import defaultdict 7 | 8 | BASE_URL = 'unix://var/run/docker.sock' 9 | 10 | class Audit(object): 11 | 12 | def __init__(self): 13 | #logdict stores the results of the audit category, 14 | #templog is a temp dict which stores the result of each check 15 | #and gets cleared after each check 16 | self.logdict = {} 17 | self.templog = {} 18 | 19 | def call(self,audit): 20 | """Reads YML profile and calls the equivelent method""" 21 | try: 22 | return getattr(self, audit)() 23 | except AttributeError: 24 | return logging.error("No audit named %s" %(audit)) 25 | 26 | def call_with_args(self,audit): 27 | """Same as call() but for methods with arguments""" 28 | args = [] 29 | func = audit.keys()[0] 30 | for argument in audit.values(): 31 | if type(argument) == dict: 32 | for key,value in argument.iteritems(): 33 | args.append(value) 34 | try: 35 | return getattr(self,func)(*args) 36 | except AttributeError: 37 | return logging.error("No audit named %s" %(func)) 38 | 39 | def run_audits(self,audits): 40 | for audit in audits: 41 | if (type(audit) == str): 42 | logging.debug("Running %s with no args" %audit) 43 | res = self.call(audit) 44 | self.add_check_results(audit,res) 45 | else: 46 | logging.debug("Running %s with args %s" \ 47 | %(audit.keys()[0], audit.values())) 48 | res = self.call_with_args(audit) 49 | self.add_check_results(audit.keys()[0],res) 50 | return 51 | 52 | def add_check_results(self,audit_name,results): 53 | """Adds audit results to output dict""" 54 | self.logdict[audit_name] = results 55 | self.templog = {} 56 | return 57 | 58 | def running_containers(self): 59 | """Check if there are running containers. 60 | Helper method to determine if some checks should execute. 61 | """ 62 | cont_ids = [] 63 | cli = self.cli 64 | try: 65 | running_cont = cli.containers() 66 | except: 67 | logging.error("Unable to connect to docker host. \ 68 | Verify that current user has permissions to use %s\ 69 | Aborting audit..." %(BASE_URL)) 70 | sys.exit(0) 71 | 72 | if len(running_cont): 73 | for cont in running_cont: 74 | cont_ids.append(cont['Id']) 75 | return cont_ids 76 | else: 77 | logging.error("No running containers!") 78 | return None 79 | 80 | def check_inspect_value(self,value,dct,*args): 81 | """Compare a dict entry with a value. Args define depth.""" 82 | key =args[0] 83 | if key in dct.keys(): 84 | if isinstance(dct[key],dict): 85 | if len(args) > 1: 86 | return self.check_inspect_value(value,dct[key],*args[1:]) 87 | elif dct[key] == value: 88 | return True 89 | else: 90 | return False 91 | else: 92 | return False 93 | 94 | def process_running(self,proc_name): 95 | """Check if process is running""" 96 | procs = psutil.process_iter() 97 | for proc in procs: 98 | if (proc.name() == proc_name) : 99 | cmd = proc.cmdline() 100 | return cmd 101 | logging.error("No process named %s.Are you sure %s is running?"\ 102 | %(proc_name,proc_name)) 103 | return None 104 | 105 | def compare_dicts(self,source,exclude): 106 | """ 107 | Compares keys,values of two dicts and produces a dict with their diff 108 | """ 109 | for key in source.keys(): 110 | if key in exclude.keys(): 111 | for port in source[key]: 112 | if port in exclude[key]: 113 | source.pop(key,None) 114 | return source 115 | 116 | def version_check(self, ver_soft, ver_ref ): 117 | """ Compares software version with a given value. 118 | Returns true if ver_soft is >= ver_ref""" 119 | ver_num = ver_soft.split('-')[0] 120 | ver_soft = ver_num.split('.') 121 | ver_ref = ver_ref.split('.') 122 | 123 | for dgt in range(len(ver_ref)): 124 | if int(ver_ref[dgt]) > int(ver_soft[dgt]): 125 | return False 126 | elif int(ver_ref[dgt]) < int(ver_soft[dgt]): 127 | return True 128 | else: 129 | continue 130 | return True -------------------------------------------------------------------------------- /audits/host.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import psutil 4 | import subprocess 5 | import logging 6 | 7 | from utils.decorators import assign_order 8 | from grp import getgrnam 9 | from audit import Audit 10 | from docker import Client,tls 11 | 12 | 13 | class HostConfAudit(Audit): 14 | 15 | def __init__(self,url='unix://var/run/docker.sock', cert=None, key=None): 16 | super(HostConfAudit, self).__init__() 17 | if cert and key: 18 | tls_config = tls.TLSConfig(verify=False, assert_hostname = False,\ 19 | client_cert = (cert, key)) 20 | self.cli = Client(base_url = url, tls = tls_config) 21 | else: 22 | self.cli = Client(base_url = url) 23 | 24 | @assign_order(1) 25 | def check_seperate_partition(self): 26 | """1.1 Create a seperate partition for containers""" 27 | mountpoint = "/var/lib/docker" 28 | partitions = psutil.disk_partitions() 29 | for partition in partitions: 30 | if (partition[1] == mountpoint): 31 | self.templog['status'] = "Pass" 32 | self.templog['descr'] = "%s mountpoint is %s" \ 33 | %(partition[0],partition[1]) 34 | else: 35 | self.templog['status'] = "Fail" 36 | self.templog['descr'] = "No seperate partition for containers" 37 | return self.templog 38 | 39 | @assign_order(2) 40 | def check_kernel_ver(self,ver): 41 | """1.2 Use the updated kernel version""" 42 | version = self.cli.version()['KernelVersion'] 43 | isupdate = self.version_check(version,ver) 44 | if isupdate: 45 | self.templog['status'] = "Pass" 46 | self.templog['descr'] = "Host uses an updated kernel" 47 | else: 48 | self.templog['status'] = "Fail" 49 | self.templog['descr'] = "Host uses an outdated kernel" 50 | self.templog['output'] = version 51 | return self.templog 52 | 53 | #Enhancement - Add a list of essential ports 54 | @assign_order(3) 55 | def check_listening_srv(self): 56 | """1.5 Remove all non-essential services from the host""" 57 | openports= [] 58 | conns = psutil.net_connections() 59 | for con in conns: 60 | if (con[5] == 'LISTEN'): 61 | openports.append([con[3][0],con[3][1]]) 62 | self.templog['descr'] = "Host has %d open ports" %(len(openports)) 63 | self.templog['output'] = openports 64 | return self.templog 65 | 66 | @assign_order(4) 67 | def check_docker_ver(self,ver): 68 | """1.6 Keep Docker up to date""" 69 | cli = self.cli 70 | version = cli.version()['Version'] 71 | isupdate = self.version_check(version,ver) 72 | if isupdate: 73 | self.templog['status'] = "Pass" 74 | self.templog['descr'] = "Host uses an updated Docker version" 75 | else: 76 | self.templog['status'] = "Fail" 77 | self.templog['descr'] = "Host uses an outdated Docker version" 78 | self.templog['output'] = version 79 | return self.templog 80 | 81 | #Enhancement - Add a list of trusted users 82 | @assign_order(5) 83 | def list_trusted_users(self): 84 | """1.7 Only allow trusted users to control Docker daemon""" 85 | dockergroup = getgrnam('docker') 86 | users = dockergroup[3] 87 | self.templog['descr'] = "%d users in docker group" %(len(users)) 88 | self.templog['output'] = users 89 | return self.templog 90 | 91 | @assign_order(6) 92 | def check_auditd_rules(self,rules): 93 | """1.8 - 1.19 Audit docker daemon, files and directories""" 94 | found = [] 95 | missing = [] 96 | results = {'found': found, 97 | 'missing' : missing} 98 | try: 99 | auditcmd = subprocess.check_output("auditctl -l", shell=True) 100 | for rule in rules: 101 | if (re.search(rule, auditcmd)): 102 | found.append(rule) 103 | else: 104 | missing.append(rule) 105 | except subprocess.CalledProcessError: 106 | logging.error("auditd is not installed. REMINDER: \ 107 | safedock should be run as root") 108 | 109 | if len(missing) > 0: 110 | self.templog['status'] = "Fail" 111 | self.templog['descr'] = "%d out of %d auditd rules are missing" \ 112 | %(len(missing),len(rules)) 113 | else: 114 | self.templog['status'] = "Pass" 115 | self.templog['descr'] = "All auditd rules are in place" 116 | self.templog['output'] = results 117 | return self.templog -------------------------------------------------------------------------------- /conf/default.yml: -------------------------------------------------------------------------------- 1 | host: 2 | - check_seperate_partition #1.1 Create a seperate partition for containers 3 | - check_kernel_ver: #1.2 Use the updated kernel version 4 | version : "3.13" 5 | - check_listening_srv #1.5 Remove all non-essential services from the host 6 | - check_docker_ver: #1.6 Keep Docker up to date 7 | version : "1.10.0" 8 | - list_trusted_users #1.7 Only allow trusted users to control Docker daemon 9 | - check_auditd_rules: #1.8 - 1.19 Audit docker daemon, files and directories 10 | paths: ["/usr/bin/docker", 11 | "/var/lib/docker", 12 | "/etc/docker", 13 | "/usr/lib/systemd/system/docker-registry.service", 14 | "/usr/lib/systemd/system/docker.service", 15 | "/var/run/docker.sock", 16 | "/etc/sysconfig/docker", 17 | "/etc/sysconfig/docker-network", 18 | "/etc/sysconfig/docker-registry", 19 | "/etc/sysconfig/docker-storage", 20 | "/etc/default/docker" 21 | ] 22 | 23 | 24 | dockerconf: 25 | - check_unwanted_args: 26 | args: ["--exec-driver=lxc", 27 | "--iptables=false", 28 | "--log-level", 29 | "--insecure-registry", 30 | "-H" 31 | ] 32 | - check_wanted_args: 33 | args: ["--icc=false", 34 | "--registry-mirror", 35 | "--default-ulimit" 36 | ] 37 | dockerfiles: 38 | - check_owner: 39 | user: "root" 40 | paths: ["/usr/lib/systemd/system/docker.service", 41 | "/usr/lib/systemd/system/docker-registry.service", 42 | "/usr/lib/systemd/system/docker.socket", 43 | "/etc/sysconfig/docker", 44 | "/etc/default/docker", 45 | "/var/run/docker.sock", 46 | "/etc/sysconfig/docker-network", 47 | "/etc/sysconfig/docker-registry", 48 | "/etc/sysconfig/docker-storage", 49 | "/etc/docker", 50 | #"/etc/docker/certs.d/" directory 51 | # 52 | # 53 | # 54 | "/var/run/docker.sock", 55 | ] 56 | - check_permissions: 57 | paths: 58 | /usr/lib/systemd/system/docker.service: "644" 59 | /usr/lib/systemd/system/docker-registry.service: "644" 60 | /usr/lib/systemd/system/docker.socket: "644" 61 | /etc/sysconfig/docker: "644" 62 | /etc/default/docker: "644" 63 | /etc/sysconfig/docker-network: "644" 64 | /etc/sysconfig/docker-registry: "644" 65 | /etc/sysconfig/docker-storage: "644" 66 | /etc/docker: "755" 67 | #/etc/docker/certs.d/: "444" directory 68 | #: "444" 69 | #: "444" 70 | #: "400" 71 | /var/run/docker.sock: "660" 72 | 73 | container_imgs: 74 | - container_user #4.1 Create a user for the container 75 | 76 | container_runtime: 77 | - verify_apparmor #5.1 Verify AppArmor Profile, if applicable 78 | - verify_selinux #5.2 Verify SELinux security options, if applicable 79 | - single_process #5.3 Verify that containers are running only a single main process 80 | - kernel_capabilities #5.4 Restrict Linux Kernel Capabilities within containers 81 | - privileged_containers #5.5 Do not use privileged containers 82 | - mounted_hostdirs #5.6 Do not mount sensitive host system directories on containers 83 | - ssh_running #5.7 Do not run ssh within containers 84 | - privileged_ports: #5.8 Do not map privileged ports within containers 85 | # ignore: 86 | # : 87 | - open_ports: #5.9 Open only needed ports on container 88 | # ignore: 89 | # : e.x 80/tcp 90 | - host_network_mode #5.10 Do not use host network mode on container 91 | - memory_usage_limit #5.11 Limit memory usage for container 92 | - cpu_priority #5.12 Set container CPU priority appropriately 93 | - readonly_root_fs #5.13 Mount container's root filesystem as read-only 94 | - bind_host_interface #5.14 Bind incoming container traffic to a specific host interface 95 | - failure_restart_policy #5.15 Set the 'on-failure' container restart policy to 5 96 | - host_process_namespace #5.16 Do not share the host's process namespace 97 | - host_ipc_namespace #5.17 Do not share the host's IPC namespace 98 | - expose_host_devices #5.18 Do not directly expose host devices to containers 99 | 100 | 101 | -------------------------------------------------------------------------------- /utils/output.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import logging 5 | from collections import OrderedDict 6 | from datetime import datetime 7 | from colorama import init, Fore, Back, Style 8 | from junit_xml import TestSuite, TestCase 9 | 10 | from audits.host import HostConfAudit 11 | from audits.dock import DockerConfAudit, DockerFileAudit 12 | from audits.containers import ContainerImgAudit, ContainerRuntimeAudit 13 | 14 | class FormattedOutput: 15 | 16 | def __init__(self,outfile,**kwargs): 17 | self.output = outfile 18 | self.log = {} 19 | self.audit_categories = {} 20 | for key in kwargs: 21 | self.audit_categories[key] = kwargs[key] 22 | 23 | def audit_init_info(self,profile): 24 | info = {} 25 | (passed, total) = self.get_score() 26 | 27 | info['date'] = str(datetime.now()) 28 | info['profile'] = profile 29 | info['score'] = "%s/%s" %(passed,total) 30 | self.log['info'] = info 31 | 32 | def save_results(self,name,res): 33 | self.log[name] = res 34 | 35 | def write_file(self): 36 | if os.path.isfile(self.output): 37 | logging.warn("File exists,deleting...") 38 | os.remove(self.output) 39 | with open(self.output,'a') as f: 40 | json_data = json.dumps(self.log,sort_keys=True, 41 | indent=4, separators=(',', ': ')) 42 | # print json_data 43 | f.write(json_data) 44 | 45 | def write_xml_file(self): 46 | test_cases = [] 47 | if os.path.isfile(self.output): 48 | logging.warn("File exists,deleting...") 49 | os.remove(self.output) 50 | with open(self.output,'a') as f: 51 | for _, elements in self.log.items(): 52 | for j in elements.viewitems(): 53 | if j[0] == 'date' or j[0] == 'profile' or j[0] == 'score': 54 | # we really don't care 55 | pass 56 | else: 57 | try: 58 | test_case = TestCase(j[0], j[1]['descr'], '', '', '') 59 | if j[1]['status'] == 'Fail': 60 | test_case.add_failure_info(j[1]['output']) 61 | else: 62 | test_case = TestCase(j[0], '', '', '', '') 63 | test_cases.append(test_case) 64 | except KeyError: 65 | # the world's smallest violin playin' for KeyError 66 | pass 67 | ts = [TestSuite("Docker Security Benchmarks", test_cases)] 68 | TestSuite.to_file(f, ts) 69 | 70 | def get_score(self): 71 | """ 72 | Calculates benchmark score by taking account 73 | results containing 'status' key 74 | """ 75 | allchecks = 0 76 | passed = 0 77 | for cat,check in self.log.iteritems(): 78 | for result in check.iteritems(): 79 | try: 80 | if (result[1]['status'] == 'Pass'): 81 | passed = passed +1 82 | allchecks = allchecks +1 83 | else : 84 | allchecks = allchecks +1 85 | except TypeError: 86 | continue 87 | except KeyError: 88 | continue 89 | return passed, allchecks 90 | 91 | def print_results(self,results): 92 | try: 93 | if results['status'] == 'Pass': 94 | print ("Status: " + Fore.GREEN + 'Pass' + Fore.RESET) 95 | elif results['status'] == 'Fail': 96 | print ("Status: " + Fore.RED + 'Fail' + Fore.RESET) 97 | except KeyError: 98 | pass 99 | except TypeError: 100 | pass 101 | print "Description: " + results['descr'] 102 | try: 103 | res = str(results['output']) 104 | print "Output: " 105 | print(Style.DIM + res + Style.RESET_ALL) 106 | except KeyError: 107 | pass 108 | print "\n" 109 | 110 | def create_ordereddict(self,dct,auditcat): 111 | """Creates a sorted dict of audits from an unsorted one""" 112 | tempdict = {} 113 | auditclass = self.audit_categories[auditcat] 114 | for key in dct.keys(): 115 | order = getattr(auditclass,key).order 116 | tempdict[key] = order 117 | ordered = OrderedDict(sorted(tempdict.items(), key=lambda t: t[1])) 118 | return ordered 119 | 120 | def terminal_output(self): 121 | output = self.log 122 | tempdict = {} 123 | auditcats = {'host': '1.Host Configuration', 124 | 'dockerconf': '2.Docker Daemon Configuration', 125 | 'dockerfiles': '3.Docker daemon configuration files', 126 | 'container_imgs': '4.Container Images and Build File', 127 | 'container_runtime': '5.Container Runtime', 128 | } 129 | 130 | print '''drydock v0.3 Audit Results\n==========================\n''' 131 | # Print results 132 | for cat, catdescr in auditcats.iteritems(): 133 | cat_inst = self.audit_categories[cat] 134 | try: 135 | if output[cat]: 136 | audits = self.create_ordereddict(output[cat],cat) 137 | print(Style.BRIGHT + "\n" + catdescr + "\n" + \ 138 | '-'*len(catdescr) + '\n'+ Style.RESET_ALL) 139 | for audit in audits.keys(): 140 | results = output[cat][audit] 141 | descr = getattr(cat_inst,audit).__doc__ 142 | print( descr + '\n' + '-'*len(descr) ) 143 | self.print_results(results) 144 | except KeyError: 145 | logging.warn("No audit category %s" %auditcats[cat]) 146 | continue 147 | 148 | # Print Overview info for the audit 149 | print(Style.BRIGHT + "Overview\n--------" +Style.RESET_ALL) 150 | print('Profile: ' + output['info']['profile']) 151 | print('Date: ' + output['info']['date']) 152 | success,total = output['info']['score'].split('/') 153 | success = float(success) 154 | total = float(total) 155 | if 0 <= success/total <= 0.5: 156 | print('Score: ' + Fore.RED + output['info']['score'] + Fore.RESET) 157 | elif 0.5 < success/total <= 0.8: 158 | print('Score: ' + Fore.YELLOW + output['info']['score'] + Fore.RESET) 159 | else: 160 | print('Score: ' + Fore.GREEN + output['info']['score'] + Fore.RESET) 161 | -------------------------------------------------------------------------------- /audits/dock.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | import psutil 4 | import logging 5 | from audit import Audit 6 | from pwd import getpwnam, getpwuid 7 | from grp import getgrnam, getgrgid 8 | from utils.decorators import assign_order 9 | 10 | 11 | 12 | class DockerFileAudit(Audit): 13 | """ 14 | Checks assosiated with Docker installation files 15 | """ 16 | # Enhancement - Identify more strict permissions?Recursive for directories? 17 | @assign_order(1) 18 | def check_permissions(self,paths): 19 | """Check permissions, according to perms for a given file or folder""" 20 | #Owner permissions dictionary 21 | bad_files = [] 22 | usrperms = {"0": 0, 23 | "1": stat.S_IXUSR, 24 | "2": stat.S_IWUSR, 25 | "3": (stat.S_IXUSR & stat.S_IWUSR), 26 | "4": stat.S_IRUSR, 27 | "5": (stat.S_IRUSR & stat.S_IXUSR), 28 | "6": (stat.S_IRUSR & stat.S_IWUSR), 29 | "7": stat.S_IRWXU 30 | } 31 | # Group permissions dictionary 32 | grpperms = {"0": 0, 33 | "1": stat.S_IXGRP, 34 | "2": stat.S_IWGRP, 35 | "3": (stat.S_IXGRP & stat.S_IWGRP), 36 | "4": stat.S_IRGRP, 37 | "5": (stat.S_IRGRP & stat.S_IXGRP), 38 | "6": (stat.S_IRGRP & stat.S_IWGRP), 39 | "7": stat.S_IRWXG 40 | } 41 | #Others permissions dictionary 42 | othperms = {"0": 0, 43 | "1": stat.S_IXOTH, 44 | "2": stat.S_IWOTH, 45 | "3": (stat.S_IXOTH & stat.S_IWOTH), 46 | "4": stat.S_IROTH, 47 | "5": (stat.S_IROTH & stat.S_IXOTH), 48 | "6": (stat.S_IROTH & stat.S_IWOTH), 49 | "7": stat.S_IRWXO 50 | } 51 | for fpath,perms in paths.iteritems(): 52 | try: 53 | # Split permission decimal values and OR corresponding 54 | #dict. values for final bitmask 55 | st = os.stat(fpath).st_mode 56 | mask = usrperms[perms[0]] | grpperms[perms[1]] | othperms[perms[2]] 57 | except KeyError: 58 | logging.error('''Wrong permission value for %s. 59 | Check your configuration''' %fpath) 60 | continue 61 | except OSError: 62 | logging.warning("No file or directory found: %s" %fpath) 63 | continue 64 | if not bool(st & mask): 65 | bad_files.append(fpath) 66 | 67 | if len(bad_files): 68 | self.templog['status'] = "Fail" 69 | self.templog['descr'] = "%d file(s) with wrong permissions"\ 70 | %len(bad_files) 71 | self.templog['output'] = bad_files 72 | else: 73 | self.templog['status'] = "Pass" 74 | self.templog['descr'] = "All files have appropriate permissions" 75 | 76 | #self.add_check_results('check_permissions') 77 | return self.templog 78 | 79 | #Enhancement - If path is directory, do the check recursively 80 | @assign_order(2) 81 | def check_owner(self,paths,owner): 82 | """Check file user and group owner.""" 83 | bad_files = [] 84 | # Get uid and gid for given user 85 | usruid = getpwnam(owner)[2] 86 | grpuid = getgrnam(owner)[2] 87 | for fpath in paths: 88 | try: 89 | st = os.stat(fpath) 90 | except OSError: 91 | logging.warning("No file or directory found: %s"% fpath) 92 | continue 93 | #Get uid and gid for given file 94 | fileuid = st.st_uid 95 | filegid = st.st_uid 96 | fileusr = getpwuid(fileuid)[0] 97 | filegrp = getgrgid(fileuid)[0] 98 | 99 | if not (fileuid == usruid and grpuid == filegid): 100 | bad_files.append(fpath) 101 | 102 | if len(bad_files): 103 | self.templog['status'] = "Fail" 104 | self.templog['descr'] = "The following files should be owned by %s:%s"\ 105 | %(owner,owner) 106 | self.templog['output'] = bad_files 107 | else: 108 | self.templog['status'] = "Pass" 109 | self.templog['descr'] = "File user and group owner are correct" 110 | 111 | #self.add_check_results('check_owner') 112 | return self.templog 113 | 114 | class DockerConfAudit(Audit): 115 | """Checks assosiated with Docker server configuration""" 116 | 117 | @assign_order(1) 118 | def check_unwanted_args(self,args): 119 | """Generic method to detect insecure arguments for running docker""" 120 | found =[] 121 | 122 | cmd = self.process_running('docker') 123 | try: 124 | for arg in args: 125 | if (arg in cmd): 126 | found.append(arg) 127 | except TypeError: 128 | logging.error("Aborting check.") 129 | return 130 | 131 | if len(found): 132 | self.templog['status'] = "Fail" 133 | self.templog['descr'] = "Docker is running with %d unwanted arguments"\ 134 | %(len(found)) 135 | self.templog['output'] = found 136 | else: 137 | self.templog['status'] = "Pass" 138 | self.templog['descr'] = "No insecure arguments found" 139 | #return self.add_check_results('check_unwanted_args') 140 | return self.templog 141 | 142 | @assign_order(2) 143 | def check_wanted_args(self,args): 144 | """Generic method to detect missing security-hardening arguments""" 145 | missing =[] 146 | 147 | cmd = self.process_running('docker') 148 | try: 149 | for arg in args: 150 | if not (arg in cmd): 151 | missing.append(arg) 152 | except TypeError: 153 | logging.error("Aborting check.") 154 | return 155 | 156 | if len(missing): 157 | self.templog['status'] = "Fail" 158 | self.templog['descr'] = "Docker is running with %d arguments missing"\ 159 | %(len(missing)) 160 | self.templog['output'] = missing 161 | else: 162 | self.templog['status'] = "Pass" 163 | self.templog['descr'] = "Docker is running with security hardening arguments" 164 | 165 | #return self.add_check_results('check_wanted_args') 166 | return self.templog 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /audits/containers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | import psutil 4 | import logging 5 | from audit import Audit 6 | from docker import Client,tls 7 | from collections import defaultdict 8 | from utils.decorators import assign_order 9 | 10 | 11 | class ContainerImgAudit(Audit): 12 | 13 | def __init__(self,url='unix://var/run/docker.sock', cert=None, key=None): 14 | super(ContainerImgAudit, self).__init__() 15 | if cert and key: 16 | tls_config = tls.TLSConfig(verify=False, assert_hostname = False,\ 17 | client_cert = (cert, key)) 18 | print tls_config 19 | self.cli = Client(base_url = url, tls = tls_config) 20 | else: 21 | self.cli = Client(base_url = url) 22 | self.running = self.running_containers() 23 | 24 | @assign_order(1) 25 | def container_user(self): 26 | """4.1 Create a user for the container""" 27 | nouser = [] 28 | try: 29 | for container in self.running: 30 | info = self.cli.inspect_container(container) 31 | root = self.check_inspect_value(0, info, 'Config', 'User') 32 | if root == False: 33 | nouser.append(container) 34 | except TypeError: 35 | return None 36 | if len(nouser): 37 | self.templog['status'] = 'Fail' 38 | self.templog['descr'] = "%d container(s) running as root "\ 39 | %len(nouser) 40 | self.templog['output'] = nouser 41 | else: 42 | self.templog['status'] = 'Pass' 43 | self.templog['descr'] = "No container running as root" 44 | return self.templog 45 | 46 | 47 | class ContainerRuntimeAudit(Audit): 48 | 49 | def __init__(self,url='unix://var/run/docker.sock', cert=None, key=None): 50 | super(ContainerRuntimeAudit, self).__init__() 51 | if cert and key: 52 | print "contrun %s %s" %(cert,key) 53 | tls_config = tls.TLSConfig(verify = False,assert_hostname = False,\ 54 | client_cert = (cert, key)) 55 | self.cli = Client(base_url = url, tls = tls_config) 56 | else: 57 | self.cli = Client(base_url = url) 58 | self.running = self.running_containers() 59 | 60 | @assign_order(1) 61 | def verify_apparmor(self): 62 | """5.1 Verify AppArmor profile""" 63 | badconts = [] 64 | try: 65 | for container in self.running: 66 | info = self.cli.inspect_container(container) 67 | noapparmor = self.check_inspect_value('', info, 'AppArmorProfile') 68 | if noapparmor == True: 69 | badconts.append(container) 70 | except TypeError: 71 | return None 72 | if len(badconts): 73 | self.templog['status'] = 'Fail' 74 | self.templog['descr'] = "%d container(s) with no AppArmor profile."\ 75 | %len(badconts) 76 | self.templog['output'] = badconts 77 | else: 78 | self.templog['status'] = 'Pass' 79 | self.templog['descr'] = "All containers have AppArmor profiles" 80 | return self.templog 81 | 82 | @assign_order(2) 83 | def verify_selinux(self): 84 | """5.2 Verify SELinux security options""" 85 | badconts = [] 86 | try: 87 | for container in self.running: 88 | info = self.cli.inspect_container(container) 89 | noselinux = self.check_inspect_value(None, info, \ 90 | 'HostConfig','SecurityOpt') 91 | if noselinux == True: 92 | badconts.append(container) 93 | except TypeError: 94 | return None 95 | if len(badconts): 96 | self.templog['status'] = 'Fail' 97 | self.templog['descr'] = "%d containers with no SELinux policies."\ 98 | %len(badconts) 99 | self.templog['output'] = badconts 100 | else: 101 | self.templog['status'] = 'Pass' 102 | self.templog['descr'] = "All containers have SELinux policies" 103 | return self.templog 104 | 105 | @assign_order(3) 106 | def single_process(self): 107 | """5.3 Verify that containers are running only a single main process""" 108 | badconts = [] 109 | try: 110 | for container in self.running: 111 | processes = self.cli.top(container)['Processes'] 112 | procnames = [] 113 | for process in processes: 114 | procnames.append(process[7]) 115 | if len(set(procnames)) > 1: 116 | badconts.append(container) 117 | except TypeError: 118 | return None 119 | if len(badconts): 120 | self.templog['status'] = 'Fail' 121 | self.templog['descr'] = "%d containers have more than one main process"\ 122 | %len(badconts) 123 | self.templog['output'] = badconts 124 | else: 125 | self.templog['status'] = 'Pass' 126 | self.templog['descr'] = "All containers have one main single process" 127 | return self.templog 128 | 129 | @assign_order(4) 130 | def kernel_capabilities(self): 131 | """5.4 Restrict Linux Kernel Capabilities within containers""" 132 | container_caps = {} 133 | try: 134 | for container in self.running: 135 | info = self.cli.inspect_container(container) 136 | caps = defaultdict(list) 137 | capadd = info['HostConfig']['CapAdd'] 138 | if capadd: 139 | caps['CapAdd'] = capadd 140 | if caps: 141 | container_caps[container] = caps 142 | except TypeError: 143 | return None 144 | 145 | if len(container_caps): 146 | self.templog['status'] = 'Fail' 147 | self.templog['descr'] = "%d added kernel capabilities found"\ 148 | %len(container_caps) 149 | self.templog['output'] = container_caps 150 | else: 151 | self.templog['status'] = 'Pass' 152 | self.templog['descr'] = "No kernel capabilities within containers" 153 | return self.templog 154 | 155 | @assign_order(5) 156 | def privileged_containers(self): 157 | """5.5 Do not use privileged containers""" 158 | badconts = [] 159 | try: 160 | for container in self.running: 161 | info = self.cli.inspect_container(container) 162 | nopriv = self.check_inspect_value(False, info, \ 163 | 'HostConfig','Privileged') 164 | if nopriv == False: 165 | badconts.append(container) 166 | except TypeError: 167 | return None 168 | 169 | if len(badconts): 170 | self.templog['status'] = 'Fail' 171 | self.templog['descr'] = "%d privileged containers found"\ 172 | %len(badconts) 173 | self.templog['output'] = badconts 174 | else: 175 | self.templog['status'] = 'Pass' 176 | self.templog['descr'] = "No privileged containers detected." 177 | return self.templog 178 | 179 | @assign_order(6) 180 | def mounted_hostdirs(self): 181 | """5.6 Do not mount sensitive host system directories on containers""" 182 | bad_dirs = [ '/', '/boot', '/etc', '/dev', \ 183 | '/lib', '/proc', '/sys', '/usr'] 184 | 185 | badconts = defaultdict(list) 186 | try: 187 | for container in self.running: 188 | info = self.cli.inspect_container(container) 189 | mounts = info['Mounts'] 190 | for mount in mounts: 191 | if mount['Source'] in bad_dirs and mount['RW'] == True: 192 | badconts[container].append(mount['Source']) 193 | except TypeError: 194 | return None 195 | 196 | if badconts: 197 | self.templog['status'] = 'Fail' 198 | self.templog['descr'] = "Sensive dirs mounted with RW" 199 | self.templog['output'] = [(v, k) for k, v in badconts.iteritems()] 200 | else: 201 | self.templog['status'] = 'Pass' 202 | self.templog['descr'] = 'No sensitive dirs mounted' 203 | return self.templog 204 | 205 | @assign_order(7) 206 | def ssh_running(self): 207 | """5.7 Do not run ssh within containers""" 208 | badconts = [] 209 | try: 210 | for container in self.running: 211 | processes = self.cli.top(container)['Processes'] 212 | for process in processes: 213 | procname = process[7] 214 | if 'sshd' in procname: 215 | badconts.append(container) 216 | except TypeError: 217 | return None 218 | 219 | if len(badconts): 220 | self.templog['status'] = 'Fail' 221 | self.templog['descr'] = "%d containers are running ssh"\ 222 | %len(badconts) 223 | self.templog['output'] = badconts 224 | else: 225 | self.templog['status'] = 'Pass' 226 | self.templog['descr'] = "No container is running ssh" 227 | 228 | return self.templog 229 | 230 | @assign_order(8) 231 | def privileged_ports(self,ignore_ports=None): 232 | """5.8 Do not map privileged ports within containers""" 233 | exclude = defaultdict(list) 234 | bad_mappings = defaultdict(list) 235 | 236 | if ignore_ports != None: 237 | for k,v in ignore_ports.iteritems(): 238 | exclude[k].append(v) 239 | 240 | for cont in self.running: 241 | info = self.cli.inspect_container(cont) 242 | img_name = info['Config']['Image'] 243 | ports = info['NetworkSettings']['Ports'] 244 | try: 245 | for mappings in ports.values(): 246 | for mapping in mappings: 247 | try: 248 | pubport = int(mapping['HostPort']) 249 | if pubport < 1024: 250 | bad_mappings[img_name].append(pubport) 251 | except KeyError: 252 | continue 253 | except TypeError: 254 | logging.info("No port mappings") 255 | continue 256 | if exclude: 257 | privports = self.compare_dicts(bad_mappings,exclude) 258 | else: 259 | privports = bad_mappings 260 | if privports: 261 | self.templog['status'] = 'Fail' 262 | self.templog['descr'] = "Mapped privileged ports found" 263 | self.templog['output'] = [(v, k) for k, v in privports.iteritems()] 264 | else: 265 | self.templog['status'] = 'Pass' 266 | self.templog['descr'] = 'No unauthorized privileged ports found' 267 | return self.templog 268 | 269 | @assign_order(9) 270 | def open_ports(self,hostports=None): 271 | """5.9 Open only needed ports on container""" 272 | exclude = defaultdict(list) 273 | mappings = defaultdict(list) 274 | if hostports != None: 275 | for k,v in hostports.iteritems(): 276 | exclude[k].append(v) 277 | try: 278 | for cont in self.running: 279 | info = self.cli.inspect_container(cont) 280 | img_name = info['Config']['Image'] 281 | ports = info['NetworkSettings']['Ports'] 282 | for port in ports.keys(): 283 | mappings[img_name].append(port) 284 | except TypeError: 285 | logging.error("Abort check") 286 | return None 287 | 288 | if exclude: 289 | openports = self.compare_dicts(mappings,exclude) 290 | else: 291 | openports = mappings 292 | if openports: 293 | self.templog['status'] = 'Fail' 294 | self.templog['descr'] = "Uneeded exposed ports found" 295 | self.templog['output'] = [(v, k) for k, v in openports.iteritems()] 296 | else: 297 | self.templog['status'] = 'Pass' 298 | self.templog['descr'] = 'Only needed ports are exposed' 299 | return self.templog 300 | 301 | @assign_order(10) 302 | def host_network_mode(self): 303 | """5.10 Do not use host network mode on container""" 304 | badconts = [] 305 | try: 306 | for container in self.running: 307 | info = self.cli.inspect_container(container) 308 | hostmode = self.check_inspect_value('host', info,\ 309 | 'HostConfig','NetworkMode') 310 | if hostmode == True: 311 | badconts.append(container) 312 | except TypeError: 313 | return None 314 | 315 | if len(badconts): 316 | self.templog['status'] = 'Fail' 317 | self.templog['descr'] = "%d container(s)' networking is not containerized"\ 318 | %len(badconts) 319 | self.templog['output'] = badconts 320 | else: 321 | self.templog['status'] = 'Pass' 322 | self.templog['descr'] = "All containers are inside a seperate network stack" 323 | 324 | return self.templog 325 | 326 | @assign_order(11) 327 | def memory_usage_limit(self): 328 | """5.11 Limit memory usage for container""" 329 | badconts = [] 330 | try: 331 | for container in self.running: 332 | info = self.cli.inspect_container(container) 333 | nolimit = self.check_inspect_value(0, info, 'HostConfig','Memory') 334 | if nolimit == True: 335 | badconts.append(container) 336 | except TypeError: 337 | return None 338 | 339 | if len(badconts): 340 | self.templog['status'] = 'Fail' 341 | self.templog['descr'] = "%d container(s) have no memory limits"\ 342 | %len(badconts) 343 | self.templog['output'] = badconts 344 | else: 345 | self.templog['status'] = 'Pass' 346 | self.templog['descr'] = "All containers have memory limits in place" 347 | 348 | return self.templog 349 | 350 | @assign_order(12) 351 | #1024 also means no shares.SHOULD FIX 352 | def cpu_priority(self): 353 | """5.12 Set container CPU priority appropriately""" 354 | badconts = [] 355 | try: 356 | for container in self.running: 357 | info = self.cli.inspect_container(container) 358 | noshares = self.check_inspect_value(0, info, 'HostConfig','CpuShares') 359 | if noshares == True: 360 | badconts.append(container) 361 | except TypeError: 362 | return None 363 | 364 | if len(badconts): 365 | self.templog['status'] = 'Fail' 366 | self.templog['descr'] = "%d container(s) have no CPU shares set"\ 367 | %len(badconts) 368 | self.templog['output'] = badconts 369 | else: 370 | self.templog['status'] = 'Pass' 371 | self.templog['descr'] = "All containers have CPU shares in place" 372 | 373 | #return self.add_check_results('cpu_priority') 374 | return self.templog 375 | 376 | @assign_order(13) 377 | def readonly_root_fs(self): 378 | """5.13 Mount container's root filesystem as read-only""" 379 | badconts = [] 380 | try: 381 | for container in self.running: 382 | info = self.cli.inspect_container(container) 383 | noreadonly = self.check_inspect_value(False, info, \ 384 | 'HostConfig','ReadonlyRootfs') 385 | if noreadonly == True: 386 | badconts.append(container) 387 | except TypeError: 388 | return None 389 | 390 | if len(badconts): 391 | self.templog['status'] = 'Fail' 392 | self.templog['descr'] = "%d container(s) have writable root FS"\ 393 | %len(badconts) 394 | self.templog['output'] = badconts 395 | else: 396 | self.templog['status'] = 'Pass' 397 | self.templog['descr'] = "All containers have read-only root FS" 398 | return self.templog 399 | 400 | @assign_order(14) 401 | def bind_host_interface(self): 402 | """5.14 Bind incoming container traffic to a specific host interface""" 403 | bad_interface = defaultdict(list) 404 | 405 | for cont in self.running: 406 | info = self.cli.inspect_container(cont) 407 | contimg = info['Image'] 408 | ports = info['NetworkSettings']['Ports'] 409 | try: 410 | for mappings in ports.values(): 411 | for mapping in mappings: 412 | try: 413 | hostip = mapping['HostIp'] 414 | if hostip == '0.0.0.0': 415 | pubport = mapping['HostPort'] 416 | bad_interface[contimg].append(pubport) 417 | except KeyError: 418 | continue 419 | except TypeError: 420 | logging.info("No port mappings") 421 | continue 422 | 423 | if bad_interface: 424 | self.templog['status'] = 'Fail' 425 | self.templog['descr'] = "Containers listen to any host interface" 426 | self.templog['output'] = [(v, k) for k, v in bad_interface.iteritems()] 427 | else: 428 | self.templog['status'] = 'Pass' 429 | self.templog['descr'] = 'Traffic bound to specific host interface' 430 | return self.templog 431 | 432 | @assign_order(15) 433 | def failure_restart_policy(self): 434 | """5.15 Set the 'on-failure' container restart policy to 5""" 435 | badconts = [] 436 | try: 437 | for container in self.running: 438 | info = self.cli.inspect_container(container) 439 | restartpol = info['HostConfig']['RestartPolicy']['Name'] 440 | if restartpol == 'always': 441 | badconts.append(container) 442 | elif restartpol == 'on-failure': 443 | retries = info['HostConfig']['RestartPolicy']['MaximumRetryCount'] 444 | if retries > 5: 445 | badconts.append(container) 446 | except TypeError: 447 | return None 448 | 449 | if len(badconts): 450 | self.templog['status'] = 'Fail' 451 | self.templog['descr'] = "%d container(s) have bad restart policy"\ 452 | %len(badconts) 453 | self.templog['output'] = badconts 454 | else: 455 | self.templog['status'] = 'Pass' 456 | self.templog['descr'] = "All containers have proper restart policy" 457 | return self.templog 458 | 459 | @assign_order(16) 460 | def host_process_namespace(self): 461 | """5.16 Do not share the host's process namespace""" 462 | badconts = [] 463 | try: 464 | for container in self.running: 465 | info = self.cli.inspect_container(container) 466 | hostproc = self.check_inspect_value('host', info, \ 467 | 'HostConfig','PidMode') 468 | if hostproc == True: 469 | badconts.append(container) 470 | except TypeError: 471 | return None 472 | 473 | if len(badconts): 474 | self.templog['status'] = 'Fail' 475 | self.templog['descr'] = "%d container(s) share host's process namespace"\ 476 | %len(badconts) 477 | self.templog['output'] = badconts 478 | else: 479 | self.templog['status'] = 'Pass' 480 | self.templog['descr'] = "All containers' process namespace is isolated" 481 | return self.templog 482 | 483 | @assign_order(17) 484 | def host_ipc_namespace(self): 485 | """5.17 Do not share the host's IPC namespace""" 486 | badconts = [] 487 | try: 488 | for container in self.running: 489 | info = self.cli.inspect_container(container) 490 | hostipc = self.check_inspect_value('host', info, \ 491 | 'HostConfig','IpcMode') 492 | if hostipc == True: 493 | badconts.append(container) 494 | except TypeError: 495 | return None 496 | 497 | if len(badconts): 498 | self.templog['status'] = 'Fail' 499 | self.templog['descr'] = "%d container(s) share host's process namespace"\ 500 | %len(badconts) 501 | self.templog['output'] = badconts 502 | else: 503 | self.templog['status'] = 'Pass' 504 | self.templog['descr'] = "All containers' process namespace is isolated" 505 | return self.templog 506 | 507 | @assign_order(18) 508 | def expose_host_devices(self): 509 | """5.18 Do not directly expose host devices to containers""" 510 | containers_exposed = defaultdict(list) 511 | try: 512 | for container in self.running: 513 | info = self.cli.inspect_container(container) 514 | devices = info['HostConfig']['Devices'] 515 | if devices: 516 | containers_exposed[container] = devices 517 | except TypeError: 518 | return None 519 | 520 | if containers_exposed: 521 | self.templog['descr'] = "Host devices are exposed to %d container(s)"\ 522 | %len(containers_exposed) 523 | self.templog['output'] = [(v, k) \ 524 | for k, v in containers_exposed.iteritems()] 525 | else: 526 | self.templog['descr'] = "No host devices exposed to containers" 527 | return self.templog 528 | --------------------------------------------------------------------------------