[apparmor] [patch 1/3] utils: add base and capability rule classes

Steve Beattie steve at nxnw.org
Wed Dec 3 18:23:08 UTC 2014


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 at cboltz.de>
Signed-off-by: Steve Beattie <steve at nxnw.org>
---
 utils/apparmor/rule/__init__.py   |  217 ++++++++++++++++++++++++++++++++++++++
 utils/apparmor/rule/capability.py |  150 ++++++++++++++++++++++++++
 2 files changed, 367 insertions(+)

Index: b/utils/apparmor/rule/__init__.py
===================================================================
--- /dev/null
+++ b/utils/apparmor/rule/__init__.py
@@ -0,0 +1,217 @@
+# ----------------------------------------------------------------------
+#    Copyright (C) 2013 Kshitij Gupta <kgupta8592 at gmail.com>
+#    Copyright (C) 2014 Christian Boltz <apparmor at 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)
Index: b/utils/apparmor/rule/capability.py
===================================================================
--- /dev/null
+++ b/utils/apparmor/rule/capability.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+# ----------------------------------------------------------------------
+#    Copyright (C) 2013 Kshitij Gupta <kgupta8592 at gmail.com>
+#    Copyright (C) 2014 Christian Boltz <apparmor at 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)
+




More information about the AppArmor mailing list