mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-31 06:16:03 +00:00
utils: add base and capability rule classes
This patch adds four classes - two "base" classes and two specific for capabilities: utils/apparmor/rule/__init__.py: class base_rule(object): Base class to handle and store a single rule class base_rules(object): Base class to handle and store a collection of rules utils/apparmor/rule/capability.py: class capability_rule(base_rule): Class to handle and store a single capability rule class capability_rules(base_rules): Class to handle and store a collection of capability rules Changes: v5: - flattened my changes into Christian's patches - pull parse_modifiers into rule/__init__.py - pull parse_capability into rule/capability.py - make CapabiltyRule.parse() be the class/static method for parsing raw capability rules. - parse_capability: renamed inlinecomment and rawrule to comment and raw_rule to be consistent with CapabilityRule fields. Originally-by: Christian Boltz <apparmor@cboltz.de> Signed-off-by: Steve Beattie <steve@nxnw.org> Acked-by: Christian Boltz <apparmor@cboltz.de>
This commit is contained in:
committed by
Steve Beattie
parent
5125fca9bc
commit
e7ae4bc8e8
217
utils/apparmor/rule/__init__.py
Normal file
217
utils/apparmor/rule/__init__.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
|
||||
# Copyright (C) 2014 Christian Boltz <apparmor@cboltz.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of version 2 of the GNU General Public
|
||||
# License as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
from apparmor.common import AppArmorBug
|
||||
|
||||
# setup module translations
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
class BaseRule(object):
|
||||
'''Base class to handle and store a single rule'''
|
||||
|
||||
def __init__(self, audit=False, deny=False, allow_keyword=False,
|
||||
comment='', log_event=None, raw_rule=''):
|
||||
'''initialize variables needed by all rule types'''
|
||||
self.audit = audit
|
||||
self.deny = deny
|
||||
self.allow_keyword = allow_keyword
|
||||
|
||||
self.comment = comment
|
||||
|
||||
self.raw_rule = raw_rule.strip() if raw_rule else None
|
||||
self.log_event = log_event
|
||||
|
||||
def get_raw(self, depth=0):
|
||||
'''return raw rule (with original formatting, and leading whitespace in the depth parameter)'''
|
||||
if self.raw_rule:
|
||||
return '%s%s' % (' ' * depth, self.raw_rule)
|
||||
else:
|
||||
return self.get_clean(depth)
|
||||
|
||||
def is_equal(self, rule_obj, strict=False):
|
||||
'''compare if rule_obj == self
|
||||
Calls is_equal_localvars() to compare rule-specific variables'''
|
||||
|
||||
if self.audit != rule_obj.audit or self.deny != rule_obj.deny:
|
||||
return False
|
||||
|
||||
if strict and (
|
||||
self.allow_keyword != rule_obj.allow_keyword
|
||||
or self.comment != rule_obj.comment
|
||||
or self.raw_rule != rule_obj.raw_rule
|
||||
):
|
||||
return False
|
||||
|
||||
return self.is_equal_localvars(rule_obj)
|
||||
|
||||
def modifiers_str(self):
|
||||
'''return the allow/deny and audit keyword as string, including whitespace'''
|
||||
|
||||
if self.audit:
|
||||
auditstr = 'audit '
|
||||
else:
|
||||
auditstr = ''
|
||||
|
||||
if self.deny:
|
||||
allowstr = 'deny '
|
||||
elif self.allow_keyword:
|
||||
allowstr = 'allow '
|
||||
else:
|
||||
allowstr = ''
|
||||
|
||||
return '%s%s' % (auditstr, allowstr)
|
||||
|
||||
|
||||
class BaseRuleset(object):
|
||||
'''Base class to handle and store a collection of rules'''
|
||||
|
||||
# decides if the (G)lob and Glob w/ (E)xt options are displayed
|
||||
can_glob = True
|
||||
can_glob_ext = False
|
||||
|
||||
def __init__(self):
|
||||
'''initialize variables needed by all ruleset types
|
||||
Do not override in child class unless really needed - override _init_vars() instead'''
|
||||
self.rules = []
|
||||
self._init_vars()
|
||||
|
||||
def _init_vars(self):
|
||||
'''called by __init__() and delete_all_rules() - override in child class to initialize more variables'''
|
||||
pass
|
||||
|
||||
def add(self, rule):
|
||||
'''add a rule object'''
|
||||
self.rules.append(rule)
|
||||
|
||||
def get_raw(self, depth=0):
|
||||
'''return all raw rules (if possible/not modified in their original formatting).
|
||||
Returns an array of lines, with depth * leading whitespace'''
|
||||
|
||||
data = []
|
||||
for rule in self.rules:
|
||||
data.append(rule.get_raw(depth))
|
||||
|
||||
if data:
|
||||
data.append('')
|
||||
|
||||
return data
|
||||
|
||||
def get_clean(self, depth=0):
|
||||
'''return all rules (in clean/default formatting)
|
||||
Returns an array of lines, with depth * leading whitespace'''
|
||||
|
||||
allow_rules = []
|
||||
deny_rules = []
|
||||
|
||||
for rule in self.rules:
|
||||
if rule.deny:
|
||||
deny_rules.append(rule.get_clean(depth))
|
||||
else:
|
||||
allow_rules.append(rule.get_clean(depth))
|
||||
|
||||
allow_rules.sort()
|
||||
deny_rules.sort()
|
||||
|
||||
cleandata = []
|
||||
|
||||
if deny_rules:
|
||||
cleandata += deny_rules
|
||||
cleandata.append('')
|
||||
|
||||
if allow_rules:
|
||||
cleandata += allow_rules
|
||||
cleandata.append('')
|
||||
|
||||
return cleandata
|
||||
|
||||
def is_covered(self, rule, check_allow_deny=True, check_audit=False):
|
||||
'''return True if rule is covered by existing rules, otherwise False'''
|
||||
|
||||
for r in self.rules:
|
||||
if r.is_covered(rule, check_allow_deny, check_audit):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# def is_log_covered(self, parsed_log_event, check_allow_deny=True, check_audit=False):
|
||||
# '''return True if parsed_log_event is covered by existing rules, otherwise False'''
|
||||
#
|
||||
# rule_obj = self.new_rule()
|
||||
# rule_obj.set_log(parsed_log_event)
|
||||
#
|
||||
# return self.is_covered(rule_obj, check_allow_deny, check_audit)
|
||||
|
||||
def delete(self, rule):
|
||||
'''Delete rule from rules'''
|
||||
|
||||
rule_to_delete = False
|
||||
i = 0
|
||||
for r in self.rules:
|
||||
if r.is_equal(rule):
|
||||
rule_to_delete = True
|
||||
break
|
||||
i = i + 1
|
||||
|
||||
if rule_to_delete:
|
||||
self.rules.pop(i)
|
||||
else:
|
||||
raise AppArmorBug('Attempt to delete non-existing rule %s' % rule.get_raw(0))
|
||||
|
||||
def delete_duplicates(self, include_rules):
|
||||
'''Delete duplicate rules.
|
||||
include_rules must be a *_rules object'''
|
||||
deleted = []
|
||||
if include_rules: # avoid breakage until we have a proper function to ensure all profiles contain all *_rules objects
|
||||
for rule in self.rules:
|
||||
if include_rules.is_covered(rule, True, True):
|
||||
self.delete(rule)
|
||||
deleted.append(rule)
|
||||
|
||||
return len(deleted)
|
||||
|
||||
def get_glob_ext(self, path_or_rule):
|
||||
'''returns the next possible glob with extension (for file rules only).
|
||||
For all other rule types, raise an exception'''
|
||||
raise AppArmorBug("get_glob_ext is not available for this rule type!")
|
||||
|
||||
|
||||
def parse_modifiers(matches):
|
||||
'''returns audit, deny, allow_keyword and comment from the matches object
|
||||
- audit, deny and allow_keyword are True/False
|
||||
- comment is the comment with a leading space'''
|
||||
audit = False
|
||||
if matches.group('audit'):
|
||||
audit = True
|
||||
|
||||
deny = False
|
||||
allow_keyword = False
|
||||
|
||||
allowstr = matches.group('allow')
|
||||
if allowstr:
|
||||
if allowstr.strip() == 'allow':
|
||||
allow_keyword = True
|
||||
elif allowstr.strip() == 'deny':
|
||||
deny = True
|
||||
else:
|
||||
raise AppArmorBug("Invalid allow/deny keyword %s" % allowstr)
|
||||
|
||||
comment = ''
|
||||
if matches.group('comment'):
|
||||
# include a space so that we don't need to add it everywhere when writing the rule
|
||||
comment = ' %s' % matches.group('comment')
|
||||
|
||||
return (audit, deny, allow_keyword, comment)
|
150
utils/apparmor/rule/capability.py
Normal file
150
utils/apparmor/rule/capability.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
|
||||
# Copyright (C) 2014 Christian Boltz <apparmor@cboltz.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of version 2 of the GNU General Public
|
||||
# License as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
from apparmor.regex import RE_PROFILE_CAP
|
||||
from apparmor.common import AppArmorBug, AppArmorException
|
||||
from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers
|
||||
import re
|
||||
|
||||
# setup module translations
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
class CapabilityRule(BaseRule):
|
||||
'''Class to handle and store a single capability rule'''
|
||||
|
||||
# Nothing external should reference this class, all external users
|
||||
# should reference the class field CapabilityRule.ALL
|
||||
class __CapabilityAll(object):
|
||||
pass
|
||||
|
||||
ALL = __CapabilityAll
|
||||
|
||||
def __init__(self, cap_list, audit=False, deny=False, allow_keyword=False,
|
||||
comment='', log_event=None, raw_rule=None):
|
||||
|
||||
super(CapabilityRule, self).__init__(audit=audit, deny=deny,
|
||||
allow_keyword=allow_keyword,
|
||||
comment=comment,
|
||||
log_event=log_event,
|
||||
raw_rule=raw_rule)
|
||||
# Because we support having multiple caps in one rule,
|
||||
# initializer needs to accept a list of caps.
|
||||
self.all_caps = False
|
||||
if cap_list == CapabilityRule.ALL:
|
||||
self.all_caps = True
|
||||
self.capability = set()
|
||||
else:
|
||||
if type(cap_list) == str:
|
||||
self.capability = {cap_list}
|
||||
elif type(cap_list) == list and len(cap_list) > 0:
|
||||
self.capability = set(cap_list)
|
||||
else:
|
||||
raise AppArmorBug('Passed unknown object to CapabilityRule: %s' % 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 len(cap.strip()) == 0:
|
||||
raise AppArmorBug('Passed empty capability to CapabilityRule: %s' % str(cap_list))
|
||||
|
||||
@staticmethod
|
||||
def parse(raw_rule):
|
||||
return parse_capability(raw_rule)
|
||||
|
||||
def get_clean(self, depth=0):
|
||||
'''return rule (in clean/default formatting)'''
|
||||
|
||||
space = ' ' * depth
|
||||
if self.all_caps:
|
||||
return('%s%scapability,%s' % (space, self.modifiers_str(), self.comment))
|
||||
else:
|
||||
caps = ' '.join(self.capability).strip() # XXX return multiple lines, one for each capability, instead?
|
||||
if caps:
|
||||
return('%s%scapability %s,%s' % (space, self.modifiers_str(), ' '.join(sorted(self.capability)), self.comment))
|
||||
else:
|
||||
raise AppArmorBug("Empty capability rule")
|
||||
|
||||
def is_covered(self, rule_obj, check_allow_deny=True, check_audit=False):
|
||||
'''check if rule_obj is covered by this rule object'''
|
||||
|
||||
if not type(rule_obj) == CapabilityRule:
|
||||
raise AppArmorBug('Passes non-capability rule: %s' % str(rule_obj))
|
||||
|
||||
if check_allow_deny and self.deny != rule_obj.deny:
|
||||
return False
|
||||
|
||||
if not rule_obj.capability and not rule_obj.all_caps:
|
||||
raise AppArmorBug('No capability specified')
|
||||
|
||||
if not self.all_caps:
|
||||
if rule_obj.all_caps:
|
||||
return False
|
||||
if not rule_obj.capability.issubset(self.capability):
|
||||
return False
|
||||
|
||||
if check_audit and rule_obj.audit != self.audit:
|
||||
return False
|
||||
|
||||
if rule_obj.audit and not self.audit:
|
||||
return False
|
||||
|
||||
# still here? -> then it is covered
|
||||
return True
|
||||
|
||||
def is_equal_localvars(self, rule_obj):
|
||||
'''compare if rule-specific variables are equal'''
|
||||
|
||||
if not type(rule_obj) == CapabilityRule:
|
||||
raise AppArmorBug('Passes non-capability rule: %s' % str(rule_obj))
|
||||
|
||||
if (self.capability != rule_obj.capability
|
||||
or self.all_caps != rule_obj.all_caps):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CapabilityRuleset(BaseRuleset):
|
||||
'''Class to handle and store a collection of capability rules'''
|
||||
|
||||
def get_glob(self, path_or_rule):
|
||||
'''Return the next possible glob. For capability rules, that's always "capability," (all capabilities)'''
|
||||
return 'capability,'
|
||||
|
||||
|
||||
def parse_capability(raw_rule):
|
||||
'''parse raw_rule and return CapabilityRule'''
|
||||
|
||||
matches = RE_PROFILE_CAP.search(raw_rule)
|
||||
if not matches:
|
||||
raise AppArmorException(_("Invalid capability rule '%s'") % raw_rule)
|
||||
|
||||
raw_rule = raw_rule.strip()
|
||||
|
||||
audit, deny, allow_keyword, comment = parse_modifiers(matches)
|
||||
|
||||
capability = []
|
||||
|
||||
if matches.group('capability'):
|
||||
capability = matches.group('capability').strip()
|
||||
capability = re.split("[ \t]+", capability)
|
||||
else:
|
||||
capability = CapabilityRule.ALL
|
||||
|
||||
return CapabilityRule(capability, audit=audit, deny=deny,
|
||||
allow_keyword=allow_keyword,
|
||||
comment=comment, raw_rule=raw_rule)
|
Reference in New Issue
Block a user