2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-09-01 14:55:10 +00:00

Merge Validate capabilities against list of known capabilities

Teach CapabilityRule about the list of known capabilities, and ensure that only valid capabilities are allowed in profiles.

This comes with several test additions (and removals from the `exception_not_raised` list for the parser simple_tests), see the individual commits for details.

Reviewing each commit on its own is probably easier than reading the merged diff.

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1117
Merged-by: John Johansen <john@jjmx.net>
This commit is contained in:
John Johansen
2023-11-03 03:09:46 +00:00
3 changed files with 101 additions and 56 deletions

View File

@@ -15,13 +15,22 @@
import re import re
from apparmor.common import AppArmorBug from apparmor.common import AppArmorBug, AppArmorException
from apparmor.regex import RE_PROFILE_CAP from apparmor.regex import RE_PROFILE_CAP
from apparmor.rule import BaseRule, BaseRuleset, logprof_value_or_all, parse_modifiers from apparmor.rule import BaseRule, BaseRuleset, logprof_value_or_all, parse_modifiers
from apparmor.translations import init_translation from apparmor.translations import init_translation
_ = init_translation() _ = init_translation()
capability_keywords = [
'audit_control', 'audit_read', 'audit_write', 'block_suspend', 'bpf', 'checkpoint_restore',
'chown', 'dac_override', 'dac_read_search', 'fowner', 'fsetid', 'ipc_lock', 'ipc_owner',
'kill', 'lease', 'linux_immutable', 'mac_admin', 'mac_override', 'mknod', 'net_admin',
'net_bind_service', 'net_broadcast', 'net_raw', 'perfmon', 'setfcap', 'setgid', 'setpcap',
'setuid', 'syslog', 'sys_admin', 'sys_boot', 'sys_chroot', 'sys_module', 'sys_nice',
'sys_pacct', 'sys_ptrace', 'sys_rawio', 'sys_resource', 'sys_time', 'sys_tty_config',
'wake_alarm']
class CapabilityRule(BaseRule): class CapabilityRule(BaseRule):
"""Class to handle and store a single capability rule""" """Class to handle and store a single capability rule"""
@@ -49,16 +58,21 @@ class CapabilityRule(BaseRule):
self.capability = set() self.capability = set()
else: else:
if isinstance(cap_list, str): if isinstance(cap_list, str):
self.capability = {cap_list} cap_list = [ cap_list ]
elif isinstance(cap_list, list) and cap_list:
if isinstance(cap_list, list):
if not cap_list:
raise AppArmorBug('Passed empty capability list to %s: %s' % (type(self).__name__, str(cap_list)))
for cap in cap_list:
if not cap.strip():
# make sure none of the cap_list arguments are blank, in
# case we decide to return one cap per output line
raise AppArmorBug('Passed empty capability to %s: %s' % (type(self).__name__, str(cap_list)))
if cap not in capability_keywords:
raise AppArmorException('Passed unknown capability to %s: %s' % (type(self).__name__, cap))
self.capability = set(cap_list) self.capability = set(cap_list)
else: else:
raise AppArmorBug('Passed unknown object to %s: %s' % (type(self).__name__, str(cap_list))) raise AppArmorBug('Passed unknown object to %s: %s' % (type(self).__name__, str(cap_list)))
# make sure none of the cap_list arguments are blank, in
# case we decide to return one cap per output line
for cap in self.capability:
if not cap.strip():
raise AppArmorBug('Passed empty capability to %s: %s' % (type(self).__name__, str(cap_list)))
@classmethod @classmethod
def _create_instance(cls, raw_rule, matches): def _create_instance(cls, raw_rule, matches):

View File

@@ -16,15 +16,36 @@
import unittest import unittest
import apparmor.severity as severity import apparmor.severity as severity
from apparmor.common import AppArmorBug, AppArmorException, hasher from apparmor.common import AppArmorBug, AppArmorException, cmd, hasher
from apparmor.logparser import ReadLog from apparmor.logparser import ReadLog
from apparmor.rule.capability import CapabilityRule, CapabilityRuleset from apparmor.rule.capability import CapabilityRule, CapabilityRuleset, capability_keywords
from apparmor.translations import init_translation from apparmor.translations import init_translation
from common_test import AATest, setup_all_loops from common_test import AATest, setup_all_loops
_ = init_translation() _ = init_translation()
# --- check if the keyword list is up to date --- #
class CapabilityKeywordsTest(AATest):
def test_capability_keyword_list(self):
rc, output = cmd('../../common/list_capabilities.sh')
self.assertEqual(rc, 0)
cap_list = output.replace('CAP_', '').strip().lower().split('\n')
missing_caps = []
for keyword in cap_list:
if keyword not in capability_keywords:
# keywords missing in the system are ok (= older kernel), but cap_list needs to have the full list
missing_caps.append(keyword)
self.assertEqual(
missing_caps, [],
'Missing capabilities in CapabilityRule capabilities list. This test is likely running '
'on an newer kernel and will require updating the list of capability keywords in '
'utils/apparmor/rule/capability.py')
# --- tests for single CapabilityRule --- # # --- tests for single CapabilityRule --- #
class CapabilityTest(AATest): class CapabilityTest(AATest):
@@ -285,9 +306,9 @@ class WriteCapabilityTest(AATest):
self._check_write_rule(' deny capability sys_admin audit_write,# foo bar', 'deny capability audit_write sys_admin, # foo bar') self._check_write_rule(' deny capability sys_admin audit_write,# foo bar', 'deny capability audit_write sys_admin, # foo bar')
def test_write_manually(self): def test_write_manually(self):
obj = CapabilityRule(['ptrace', 'audit_write'], allow_keyword=True) obj = CapabilityRule(['sys_ptrace', 'audit_write'], allow_keyword=True)
expected = ' allow capability audit_write ptrace,' expected = ' allow capability audit_write sys_ptrace,'
self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule') self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule') self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')
@@ -425,12 +446,12 @@ class CapabilityCoveredTest(AATest):
obj = CapabilityRule('fsetid') obj = CapabilityRule('fsetid')
obj2 = CapabilityRule('fsetid') obj2 = CapabilityRule('fsetid')
obj.capability.add('sys_admin') obj.capability.add('sys_admin')
obj2.capability.add('ptrace') obj2.capability.add('sys_ptrace')
self.assertTrue(self._is_covered(obj, 'capability sys_admin,')) self.assertTrue(self._is_covered(obj, 'capability sys_admin,'))
self.assertFalse(self._is_covered(obj, 'capability ptrace,')) self.assertFalse(self._is_covered(obj, 'capability sys_ptrace,'))
self.assertFalse(self._is_covered(obj2, 'capability sys_admin,')) self.assertFalse(self._is_covered(obj2, 'capability sys_admin,'))
self.assertTrue(self._is_covered(obj2, 'capability ptrace,')) self.assertTrue(self._is_covered(obj2, 'capability sys_ptrace,'))
class CapabiliySeverityTest(AATest): class CapabiliySeverityTest(AATest):
@@ -439,7 +460,6 @@ class CapabiliySeverityTest(AATest):
('dac_read_search', 7), ('dac_read_search', 7),
(['fsetid', 'dac_read_search'], 9), (['fsetid', 'dac_read_search'], 9),
(CapabilityRule.ALL, 10), (CapabilityRule.ALL, 10),
('foo', 'unknown'),
) )
def _run_test(self, params, expected): def _run_test(self, params, expected):
@@ -448,6 +468,25 @@ class CapabiliySeverityTest(AATest):
rank = obj.severity(sev_db) rank = obj.severity(sev_db)
self.assertEqual(rank, expected) self.assertEqual(rank, expected)
def test_all_caps(self):
''' make sure all capabilities have a severity defined '''
sev_db = severity.Severity('../severity.db', 'unknown')
for cap in capability_keywords:
obj = CapabilityRule(cap)
rank = obj.severity(sev_db)
# capabilities have a severity of 7..10, with the exception of 0 for the unused CAP_NET_BROADCAST
# (might need adjustment if a new capability gets a different severity assigned)
self.assertTrue(rank in [0, 7, 8, 9, 10], 'unexpected severity for capability %s: %s' % (cap, rank))
def test_unknown_cap(self):
sev_db = severity.Severity('../severity.db', 'unknown')
obj = CapabilityRule('sys_admin')
obj.capability = {'unknown_and_broken'} # override capability with an unknown one to test for 'unknown' severity (creating obj with this invalid capability would raise an error)
rank = obj.severity(sev_db)
self.assertEqual(rank, 'unknown')
class CapabilityLogprofHeaderTest(AATest): class CapabilityLogprofHeaderTest(AATest):
tests = ( tests = (
@@ -506,18 +545,18 @@ class CapabilityRulesTest(AATest):
rules = [ rules = [
'capability chown,', 'capability chown,',
'allow capability sys_admin,', 'allow capability sys_admin,',
'deny capability chgrp, # example comment', 'deny capability fowner, # example comment',
] ]
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' capability chown,', ' capability chown,',
@@ -531,13 +570,13 @@ class CapabilityRulesTest(AATest):
self.assertEqual(expected_clean, ruleset.get_clean(1)) self.assertEqual(expected_clean, ruleset.get_clean(1))
def test_ruleset_add(self): def test_ruleset_add(self):
rule = CapabilityRule('chgrp', comment=' # example comment') rule = CapabilityRule('fowner', comment=' # example comment')
ruleset = CapabilityRuleset() ruleset = CapabilityRuleset()
ruleset.add(rule) ruleset.add(rule)
expected_raw = [ expected_raw = [
' capability chgrp, # example comment', ' capability fowner, # example comment',
'', '',
] ]
@@ -555,7 +594,7 @@ class CapabilityRulesCoveredTest(AATest):
'capability setuid setgid,', 'capability setuid setgid,',
'allow capability sys_admin,', 'allow capability sys_admin,',
'audit capability kill,', 'audit capability kill,',
'deny capability chgrp, # example comment', 'deny capability fowner, # example comment',
] ]
for rule in rules: for rule in rules:
@@ -601,15 +640,15 @@ class CapabilityRulesCoveredTest(AATest):
self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('audit capability kill,'))) self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('audit capability kill,')))
def test_ruleset_is_covered_19(self): def test_ruleset_is_covered_19(self):
self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('deny capability chgrp,'))) self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('deny capability fowner,')))
def test_ruleset_is_covered_20(self): def test_ruleset_is_covered_20(self):
self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('audit deny capability chgrp,'))) self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('audit deny capability fowner,')))
def test_ruleset_is_covered_21(self): def test_ruleset_is_covered_21(self):
self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('audit capability chgrp,'))) self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('audit capability fowner,')))
def test_ruleset_is_covered_22(self): def test_ruleset_is_covered_22(self):
self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('capability chgrp,'))) self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('capability fowner,')))
def test_ruleset_is_covered_23(self): def test_ruleset_is_covered_23(self):
self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('capability chgrp,'), check_allow_deny=False)) self.assertTrue(self.ruleset.is_covered(CapabilityRule.create_instance('capability fowner,'), check_allow_deny=False))
def test_ruleset_is_covered_24(self): def test_ruleset_is_covered_24(self):
self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('deny capability chown,'), check_allow_deny=False)) self.assertFalse(self.ruleset.is_covered(CapabilityRule.create_instance('deny capability chown,'), check_allow_deny=False))
@@ -634,12 +673,12 @@ class CapabilityRulesCoveredTest(AATest):
# def test_ruleset_is_log_covered_4(self): # def test_ruleset_is_log_covered_4(self):
# self._test_log_covered(True, 'kill') # self._test_log_covered(True, 'kill')
# def test_ruleset_is_log_covered_5(self): # def test_ruleset_is_log_covered_5(self):
# self._test_log_covered(False, 'chgrp') # self._test_log_covered(False, 'fowner')
# def test_ruleset_is_log_covered_6(self): # def test_ruleset_is_log_covered_6(self):
# event_base = 'type=AVC msg=audit(1415403814.628:662): apparmor="ALLOWED" operation="capable" profile="/bin/ping" pid=15454 comm="ping" capability=13 capname="%s"' # event_base = 'type=AVC msg=audit(1415403814.628:662): apparmor="ALLOWED" operation="capable" profile="/bin/ping" pid=15454 comm="ping" capability=13 capname="%s"'
# #
# parser = ReadLog('', '', '') # parser = ReadLog('', '', '')
# self.assertEqual(True, self.ruleset.is_log_covered(parser.parse_event(event_base%'chgrp'), False)) # ignores allow/deny # self.assertEqual(True, self.ruleset.is_log_covered(parser.parse_event(event_base%'fowner'), False)) # ignores allow/deny
class CapabilityGlobTest(AATest): class CapabilityGlobTest(AATest):
@@ -660,7 +699,7 @@ class CapabilityDeleteTest(AATest):
rules = [ rules = [
'capability chown,', 'capability chown,',
'allow capability sys_admin,', 'allow capability sys_admin,',
'deny capability chgrp, # example comment', 'deny capability fowner, # example comment',
] ]
for rule in rules: for rule in rules:
@@ -669,12 +708,12 @@ class CapabilityDeleteTest(AATest):
def test_delete(self): def test_delete(self):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' capability chown,', ' capability chown,',
'', '',
@@ -688,13 +727,13 @@ class CapabilityDeleteTest(AATest):
def test_delete_with_allcaps(self): def test_delete_with_allcaps(self):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
' capability,', ' capability,',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' capability chown,', ' capability chown,',
' capability,', ' capability,',
@@ -711,12 +750,12 @@ class CapabilityDeleteTest(AATest):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' capability chown,', ' capability chown,',
@@ -745,7 +784,7 @@ class CapabilityDeleteTest(AATest):
inc = CapabilityRuleset() inc = CapabilityRuleset()
rules = [ rules = [
'capability chown,', 'capability chown,',
'deny capability chgrp, # example comment', 'deny capability fowner, # example comment',
] ]
for rule in rules: for rule in rules:
@@ -766,7 +805,7 @@ class CapabilityDeleteTest(AATest):
inc = CapabilityRuleset() inc = CapabilityRuleset()
rules = [ rules = [
'capability audit_write,', 'capability audit_write,',
'capability chgrp, # example comment', 'capability fowner, # example comment',
] ]
for rule in rules: for rule in rules:
@@ -775,12 +814,12 @@ class CapabilityDeleteTest(AATest):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' capability chown,', ' capability chown,',
@@ -805,13 +844,13 @@ class CapabilityDeleteTest(AATest):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
' audit capability dac_override,', ' audit capability dac_override,',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' audit capability dac_override,', ' audit capability dac_override,',
@@ -831,12 +870,12 @@ class CapabilityDeleteTest(AATest):
inc.add(CapabilityRule.create_instance(rule)) inc.add(CapabilityRule.create_instance(rule))
expected_raw = [ expected_raw = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
@@ -848,12 +887,12 @@ class CapabilityDeleteTest(AATest):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' capability chown,', ' capability chown,',
@@ -868,12 +907,12 @@ class CapabilityDeleteTest(AATest):
expected_raw = [ expected_raw = [
' capability chown,', ' capability chown,',
' allow capability sys_admin,', ' allow capability sys_admin,',
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
] ]
expected_clean = [ expected_clean = [
' deny capability chgrp, # example comment', ' deny capability fowner, # example comment',
'', '',
' allow capability sys_admin,', ' allow capability sys_admin,',
' capability chown,', ' capability chown,',

View File

@@ -44,12 +44,6 @@ exception_not_raised = (
'abi/bad_11.sd', 'abi/bad_11.sd',
'abi/bad_12.sd', 'abi/bad_12.sd',
# invalid capabilities (like "foobar"), but syntactically correct
'capability/bad_1.sd',
'capability/bad_2.sd',
'capability/bad_3.sd',
'capability/bad_4.sd',
# interesting[tm] profile name # interesting[tm] profile name
'change_hat/bad_parsing.sd', 'change_hat/bad_parsing.sd',
@@ -176,8 +170,6 @@ exception_not_raised = (
'profile/flags/flags_bad_disconnected_path4.sd', 'profile/flags/flags_bad_disconnected_path4.sd',
'profile/flags/flags_bad_disconnected_path5.sd', 'profile/flags/flags_bad_disconnected_path5.sd',
'profile/profile_ns_bad8.sd', # 'profile :ns/t' without terminating ':' 'profile/profile_ns_bad8.sd', # 'profile :ns/t' without terminating ':'
'ptrace/bad_05.sd', # actually contains a capability rule with invalid (ptrace-related) keyword
'ptrace/bad_06.sd', # actually contains a capability rule with invalid (ptrace-related) keyword
'ptrace/bad_10.sd', # peer with invalid regex 'ptrace/bad_10.sd', # peer with invalid regex
'signal/bad_21.sd', # invalid regex 'signal/bad_21.sd', # invalid regex
'unix/bad_attr_1.sd', 'unix/bad_attr_1.sd',