Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions docker_enforcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from logging import StreamHandler

from docker import APIClient
from docker.errors import NotFound
from flask import Flask, Response, request
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
Expand All @@ -23,6 +24,7 @@
from dockerenforcer.killer import Killer, Judge, TriggerHandler
from rules.rules import rules
from request_rules.request_rules import request_rules
from request_rules.start_request_rules import start_request_rules
from whitelist_rules.whitelist_rules import whitelist_rules

config = Config()
Expand All @@ -35,6 +37,9 @@
jurek = Killer(docker_helper, config.mode)
trigger_handler = TriggerHandler()
containers_regex = re.compile("^(/v.+?)?/containers/.+?$")
containers_start_regexp = re.compile("^(/v.+?)?/containers/(.+?)/rename\\?name=(.+?)$", re.IGNORECASE)

start_requests_judge = Judge(start_request_rules, "request", config, run_whitelists=True)


def create_app():
Expand Down Expand Up @@ -146,6 +151,10 @@ def show_rules():
def show_request_rules():
return python_file_to_json(request, "request_rules/request_rules.py")

@app.route('/start_request_rules')
def show_start_request_rules():
return python_file_to_json(request, "request_rules/start_request_rules.py")


@app.route('/triggers')
def show_triggers():
Expand Down Expand Up @@ -229,15 +238,27 @@ def authz_request():
return process_positive_verdict(verdict, json_data, register=False)

operation = url.path.split("/")[-1]
if containers_regex.match(url.path) and operation == "create" and "requestbody" in json_data:
int_bytes = b64decode(json_data["requestbody"])
int_json = json.loads(int_bytes.decode(request.charset))
if containers_regex.match(url.path):
tls_user = json_data["user"] if "user" in json_data and json_data["user"] != '' else "[unknown]"
int_json = docker_helper.rename_keys_to_lower(int_json)
container = make_container_periodic_check_compatible(int_json, url, tls_user)
verdict = judge.should_be_killed(container)
if verdict.verdict:
return process_positive_verdict(verdict, json_data)
if operation == "create" and "requestbody" in json_data:
int_bytes = b64decode(json_data["requestbody"])
int_json = json.loads(int_bytes.decode(request.charset))
int_json = docker_helper.rename_keys_to_lower(int_json)
container = make_container_periodic_check_compatible(int_json, url, tls_user)

if config.merge_image_labels_when_check_authz_create_requests:
container.params["config"]["labels"] = docker_image_helper.merge_container_and_image_labels(container)

verdict = judge.should_be_killed(container)
if verdict.verdict:
return process_positive_verdict(verdict, json_data)
elif operation == "start" and request.method == "POST" and config.check_authz_start_requests:
container_id = url.path.split("/")[-2]
container = docker_helper.check_container(container_id, CheckSource.AuthzPlugin, True)
verdict = start_requests_judge.should_be_killed(container)
if verdict.verdict:
return process_positive_verdict(verdict, json_data)

return to_formatted_json(json.dumps({"Allow": True}))


Expand Down
4 changes: 3 additions & 1 deletion dockerenforcer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import copy
from typing import Dict

version = "0.8.15"
version = "0.8.16-aurea-dev-1"


class Mode(Enum):
Expand Down Expand Up @@ -32,6 +32,8 @@ def __init__(self) -> None:
self.immediate_periodical_start: bool = bool(os.getenv('IMMEDIATE_PERIODICAL_START', 'False') == 'True')
self.stop_on_first_violation: bool = bool(os.getenv('STOP_ON_FIRST_VIOLATION', 'True') == 'True')
self.log_authz_requests: bool = bool(os.getenv('LOG_AUTHZ_REQUESTS', 'False') == 'True')
self.check_authz_start_requests: bool = bool(os.getenv('CHECK_AUTHZ_START_REQUESTS', 'False') == 'True')
self.merge_image_labels_when_check_authz_create_requests: bool = bool(os.getenv('MERGE_IMAGE_LABELS_WHEN_CHECK_AUTHZ_CREATE_REQUESTS', 'False') == 'True')
self.default_allow: bool = bool(os.getenv('DEFAULT_ACTION_ALLOW', 'True') == 'True')
self.version: str = version
self.white_list_separator: str = "|"
Expand Down
26 changes: 26 additions & 0 deletions dockerenforcer/docker_image_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

logger = logging.getLogger("docker_enforcer")


class DockerImageHelper:
def __init__(self, config: Config, client: APIClient) -> None:
super().__init__()
Expand All @@ -27,3 +28,28 @@ def get_image_uniq_tag_by_id(self, image_id):
return image_inspect_data['RepoTags'][0]
else:
return image_id

def merge_container_and_image_labels(self, container):
image = container.params['image']
image_params = {}
try:
image_params = self._client.inspect_image(image)
except NotFound as e:
logger.debug("Image {0} not found - {1}.".format(image, e))
image_labels = {}
if 'Config' in image_params and 'Labels' in image_params['Config']:
image_labels = image_params['Config']['Labels']
final_labels = self.merge_dicts(image_labels, container.params["config"]["labels"])
return final_labels

def merge_dicts(self, image_labels, container_labels):
if image_labels is None and container_labels is None:
return None

if not image_labels:
return container_labels

if not container_labels:
return image_labels

return {**image_labels, **container_labels}
6 changes: 6 additions & 0 deletions request_rules/start_request_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
start_request_rules = [
{
"name": "always false",
"rule": lambda container: False
}
]
71 changes: 70 additions & 1 deletion test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

from flask import Response

from docker_enforcer import app, judge, config, requests_judge, trigger_handler
from docker_enforcer import app, judge, config, requests_judge, trigger_handler, start_requests_judge, docker_helper
from dockerenforcer.config import Mode
from dockerenforcer.docker_helper import Container, CheckSource
from test.test_helpers import ApiTestHelper, DefaultRulesHelper


Expand All @@ -16,6 +17,8 @@ class ApiContainerTest(unittest.TestCase):
cp_request_rule_regexp = re.compile("^/v1\.[23]\d/containers/test/archive$")
cp_request_rule = {"name": "cp not allowed", "rule": lambda r, x=cp_request_rule_regexp:
r['requestmethod'] in ['GET', 'HEAD'] and x.match(r['parseduri'].path)}
start_request_rule = {"name": "start not allowed", "rule": lambda r: True}

test_trigger_flag = False
test_trigger = {"name": "set local flag", "trigger": lambda v: ApiContainerTest.set_trigger_flag()}
forbid_privileged_rule = {
Expand All @@ -36,6 +39,7 @@ def set_trigger_flag():
def setUpClass(cls):
config.mode = Mode.Kill
config.log_authz_requests = True
config.check_authz_start_requests = True
cls.de = app
cls.de.testing = True
cls.app = cls.de.test_client()
Expand All @@ -48,6 +52,13 @@ def setUp(self):
judge._image_per_rule_whitelist = {}
judge._custom_whitelist_rules = {}

start_requests_judge._rules = []
start_requests_judge._global_whitelist = []
start_requests_judge._image_global_whitelist = []
start_requests_judge._per_rule_whitelist = {}
start_requests_judge._image_per_rule_whitelist = {}
start_requests_judge._custom_whitelist_rules = {}

def _check_response(self, response, allow, msg=None, code=200):
self.assertEqual(response.status_code, code)
json_res = json.loads(response.data.decode(response.charset))
Expand Down Expand Up @@ -80,6 +91,56 @@ def test_request_rule_no_cp_to(self):
res = self.app.post('/AuthZPlugin.AuthZReq', data=ApiTestHelper.authz_req_copy_to_cont)
self._check_response(res, False, "cp not allowed")

def test_start_request_rule(self):
params = {'name': 'test', 'config': {'image': 'test', 'labels': {}}}
test_containers = Container('6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11', params, {}, 0,
CheckSource.AuthzPlugin)

start_requests_judge._rules = [self.start_request_rule]
with mock.patch.object(docker_helper, 'check_container') as mock_check_container:
mock_check_container.return_value = test_containers

res = self.app.post('/AuthZPlugin.AuthZReq', data=ApiTestHelper.authz_req_start_container)
self._check_response(res, False, self.start_request_rule['name'])

mock_check_container.assert_called_with('6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11',
CheckSource.AuthzPlugin, True)

def test_start_request_rule_but_on_whitelist(self):
params = {'name': 'test', 'config': {'image': 'test_image', 'labels': {}}}
test_containers = Container('6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11', params, {}, 0,
CheckSource.AuthzPlugin)

def global_whitelist():
start_requests_judge._global_whitelist = [re.compile('^test$')]

def image_global_whitelist():
start_requests_judge._global_whitelist = []
start_requests_judge._image_global_whitelist = [re.compile('^test_image$')]

def per_rule_whitelist():
start_requests_judge._image_global_whitelist = []
start_requests_judge._per_rule_whitelist = {self.start_request_rule['name']: [re.compile('^tes.*$')]}

def image_per_rule_whitelist():
start_requests_judge._per_rule_whitelist = {}
start_requests_judge._image_per_rule_whitelist = {self.start_request_rule['name']: [re.compile('^test_ima.*$')]}

tests_parameters = [global_whitelist, image_global_whitelist, per_rule_whitelist, image_per_rule_whitelist]

for modifier in tests_parameters:
with self.subTest(modifier.__name__):
start_requests_judge._rules = [self.start_request_rule]
modifier()
with mock.patch.object(docker_helper, 'check_container') as mock_check_container:
mock_check_container.return_value = test_containers

res = self.app.post('/AuthZPlugin.AuthZReq', data=ApiTestHelper.authz_req_start_container)
self._check_response(res, True)

mock_check_container.assert_called_with('6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11',
CheckSource.AuthzPlugin, True)

def test_trigger_when_rule_fails_run_with_mem_check(self):
judge._rules = [self.mem_rule]
trigger_handler._triggers = [self.test_trigger]
Expand Down Expand Up @@ -209,3 +270,11 @@ def test_fetch_request_rules(self):
def test_fetch_request_rules_html(self):
res = self.app.get('/request_rules', headers={"Accept": "text/html"})
self._check_rules_response(res, "text/html")

def test_fetch_start_request_rules(self):
res = self.app.get('/start_request_rules')
self._check_rules_response(res, "application/json", DefaultRulesHelper.start_request_rules)

def test_fetch_start_request_rules_html(self):
res = self.app.get('/start_request_rules', headers={"Accept": "text/html"})
self._check_rules_response(res, "text/html")
36 changes: 36 additions & 0 deletions test/test_docker_image_helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import types
import unittest
from unittest.mock import create_autospec

Expand Down Expand Up @@ -43,3 +44,38 @@ def test_get_image_uniq_tag_by_id__when_many_repo_tags(self):
self._client.inspect_image.return_value = {'RepoTags': [self._image_name2, self._image_name1]}
image_tag = self._helper.get_image_uniq_tag_by_id(self._image_id)
self.assertEqual(self._image_name2, image_tag)

def test_merge_container_and_image_labels(self):
image_labels = {'label1': 'label1_image_value', 'label2': 'label2_image_value'}
container_labels = {'label2': 'label2_container_value', 'label3': 'label3_container_value'}

self._client.inspect_image.return_value = {'Config': {'Labels': image_labels}}

container = types.SimpleNamespace()
container.params = {'image': 'test', 'config': {'labels': container_labels}}

final_labels = self._helper.merge_container_and_image_labels(container=container)

self.assertIn('label1', final_labels)
self.assertEquals('label1_image_value', final_labels['label1'])

self.assertIn('label2', final_labels)
self.assertEquals('label2_container_value', final_labels['label2'])

self.assertIn('label3', final_labels)
self.assertEquals('label3_container_value', final_labels['label3'])

def test_merge_dicts(self):
image_labels = {'label1': 'label1_image_value', 'label2': 'label2_image_value'}
container_labels = {'label2': 'label2_container_value', 'label3': 'label3_container_value'}

res = self._helper.merge_dicts(None, None)
self.assertIsNone(res)

res = self._helper.merge_dicts(image_labels, None)
self.assertEqual(image_labels, res)

res = self._helper.merge_dicts(None, container_labels)
self.assertEqual(container_labels, res)


3 changes: 3 additions & 0 deletions test/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ class ApiTestHelper:
authz_req_run_with_privileged_name_test = b'{"RequestMethod":"POST","RequestUri":"/v1.30/containers/create?name=test","RequestBody":"eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOltdLCJDbWQiOlsic2giXSwiSW1hZ2UiOiJhbHBpbmUiLCJWb2x1bWVzIjp7fSwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6e30sIkhvc3RDb25maWciOnsiQmluZHMiOm51bGwsIkNvbnRhaW5lcklERmlsZSI6IiIsIkxvZ0NvbmZpZyI6eyJUeXBlIjoiIiwiQ29uZmlnIjp7fX0sIk5ldHdvcmtNb2RlIjoiZGVmYXVsdCIsIlBvcnRCaW5kaW5ncyI6e30sIlJlc3RhcnRQb2xpY3kiOnsiTmFtZSI6Im5vIiwiTWF4aW11bVJldHJ5Q291bnQiOjB9LCJBdXRvUmVtb3ZlIjp0cnVlLCJWb2x1bWVEcml2ZXIiOiIiLCJWb2x1bWVzRnJvbSI6bnVsbCwiQ2FwQWRkIjpudWxsLCJDYXBEcm9wIjpudWxsLCJEbnMiOltdLCJEbnNPcHRpb25zIjpbXSwiRG5zU2VhcmNoIjpbXSwiRXh0cmFIb3N0cyI6bnVsbCwiR3JvdXBBZGQiOm51bGwsIklwY01vZGUiOiIiLCJDZ3JvdXAiOiIiLCJMaW5rcyI6bnVsbCwiT29tU2NvcmVBZGoiOjAsIlBpZE1vZGUiOiIiLCJQcml2aWxlZ2VkIjp0cnVlLCJQdWJsaXNoQWxsUG9ydHMiOmZhbHNlLCJSZWFkb25seVJvb3RmcyI6ZmFsc2UsIlNlY3VyaXR5T3B0IjpudWxsLCJVVFNNb2RlIjoiIiwiVXNlcm5zTW9kZSI6IiIsIlNobVNpemUiOjAsIkNvbnNvbGVTaXplIjpbMCwwXSwiSXNvbGF0aW9uIjoiIiwiQ3B1U2hhcmVzIjowLCJNZW1vcnkiOjEwNDg1NzYwMCwiTmFub0NwdXMiOjAsIkNncm91cFBhcmVudCI6IiIsIkJsa2lvV2VpZ2h0IjowLCJCbGtpb1dlaWdodERldmljZSI6bnVsbCwiQmxraW9EZXZpY2VSZWFkQnBzIjpudWxsLCJCbGtpb0RldmljZVdyaXRlQnBzIjpudWxsLCJCbGtpb0RldmljZVJlYWRJT3BzIjpudWxsLCJCbGtpb0RldmljZVdyaXRlSU9wcyI6bnVsbCwiQ3B1UGVyaW9kIjowLCJDcHVRdW90YSI6MCwiQ3B1UmVhbHRpbWVQZXJpb2QiOjAsIkNwdVJlYWx0aW1lUnVudGltZSI6MCwiQ3B1c2V0Q3B1cyI6IiIsIkNwdXNldE1lbXMiOiIiLCJEZXZpY2VzIjpbXSwiRGV2aWNlQ2dyb3VwUnVsZXMiOm51bGwsIkRpc2tRdW90YSI6MCwiS2VybmVsTWVtb3J5IjowLCJNZW1vcnlSZXNlcnZhdGlvbiI6MCwiTWVtb3J5U3dhcCI6MCwiTWVtb3J5U3dhcHBpbmVzcyI6LTEsIk9vbUtpbGxEaXNhYmxlIjpmYWxzZSwiUGlkc0xpbWl0IjowLCJVbGltaXRzIjpudWxsLCJDcHVDb3VudCI6MCwiQ3B1UGVyY2VudCI6MCwiSU9NYXhpbXVtSU9wcyI6MCwiSU9NYXhpbXVtQmFuZHdpZHRoIjowfSwiTmV0d29ya2luZ0NvbmZpZyI6eyJFbmRwb2ludHNDb25maWciOnt9fX0K","RequestHeaders":{"Content-Length":"1437","Content-Type":"application/json","User-Agent":"Docker-Client/17.06.0-ce (linux)"}}\n'
# sudo curl -H "Content-Type: application/json" -X POST -d '{"Image":"ubuntu"}' http://localhost:2375/containers/create
authz_req_create_with_only_image = b'{"RequestMethod":"POST","RequestUri":"/containers/create","RequestBody":"eyJJbWFnZSI6InVidW50dSJ9","RequestHeaders":{"Accept":"*/*","Content-Length":"18","Content-Type":"application/json","User-Agent":"curl/7.47.0"}}\n'
# docker start 6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11
authz_req_start_container = b'{"RequestMethod":"POST","RequestUri":"/v1.31/containers/6b5d46fc77fb2f182ead9a16e2dd5ea444fcbf94754c7704e459d0aa28bb6b11/start","RequestHeaders":{"Content-Length":"0","Content-Type":"text/plain","User-Agent":"Docker-Client/17.07.0-ce (linux)"}}\n'

class DefaultRulesHelper:
rules_json = b'"rules = [\\n {\\n \\"name\\": \\"always false\\",\\n \\"rule\\": lambda container: False\\n }\\n]\\n"'
triggers_json = b'"import json\\nimport threading\\n\\nfrom flask import logging\\n\\nlogger = logging.getLogger(\\"docker_enforcer\\")\\n\\ntriggers = [\\n {\\n \\"name\\": \\"additional log verdict\\",\\n \\"trigger\\": lambda v: logger.debug(\\"[{0}] Trigger: container {1} is detected to violate the rule \\\\\\"{2}\\\\\\".\\"\\n .format(threading.current_thread().name, v.subject, json.dumps(v.reasons)))\\n }\\n]\\n"'
request_rules = b'"import json\\nimport threading\\n\\nfrom flask import logging\\n\\nlogger = logging.getLogger(\\"docker_enforcer\\")\\n\\nrequest_rules = [\\n {\\n \\"name\\": \\"always false\\",\\n \\"rule\\": lambda request: False\\n }\\n]\\n"'
start_request_rules = b'"start_request_rules = [\\n {\\n \\"name\\": \\"always false\\",\\n \\"rule\\": lambda container: False\\n }\\n]\\n"'