def time_to_int(self, value, default_unit): number, unit = split_unit(value) if unit == '': unit = default_unit if unit in ['us', 'microsecond', 'microseconds']: number = number / 1000000.0 if default_unit == 'seconds': raise AppArmorException(_('Invalid unit in rlimit cpu %s rule') % value) elif unit in ['ms', 'millisecond', 'milliseconds']: number = number / 1000.0 if default_unit == 'seconds': raise AppArmorException(_('Invalid unit in rlimit cpu %s rule') % value) elif unit in ['s', 'sec', 'second', 'seconds']: # manpage doesn't list sec pass elif unit in ['min', 'minute', 'minutes']: number = number * 60 elif unit in ['h', 'hour', 'hours']: number = number * 60 * 60 elif unit in ['d', 'day', 'days']: # manpage doesn't list 'd' number = number * 60 * 60 * 24 elif unit in ['week', 'weeks']: number = number * 60 * 60 * 24 * 7 else: raise AppArmorException('Unknown unit %s in rlimit %s %s' % (unit, self.rlimit, value)) return number
def re_match_include(line): """Matches the path for include and returns the include path""" matches = RE_INCLUDE.search(line) if not matches: return None path = None if matches.group('magicpath'): path = matches.group('magicpath').strip() elif matches.group('unquotedpath'): # LP: #1738879 - parser doesn't handle unquoted paths everywhere # path = matches.group('unquotedpath').strip() raise AppArmorException( _('Syntax error: #include must use quoted path or <...>')) elif matches.group('quotedpath'): path = matches.group('quotedpath') # LP: 1738880 - parser doesn't handle relative paths everywhere, and # neither do we (see aa.py) if len(path) > 0 and path[0] != '/': raise AppArmorException( _('Syntax error: #include must use quoted path or <...>')) # if path is empty or the empty string if path is None or path == "": raise AppArmorException( _('Syntax error: #include rule with empty filename')) # LP: #1738877 - parser doesn't handle files with spaces in the name if re.search('\s', path): raise AppArmorException( _('Syntax error: #include rule filename cannot contain spaces')) return path
def split_perms(perm_string, deny): '''parse permission string - perm_string: the permission string to parse - deny: True if this is a deny rule ''' perms = set() exec_mode = None while perm_string: if perm_string[0] in file_permissions: perms.add(perm_string[0]) perm_string = perm_string[1:] elif perm_string[0] == 'x': if not deny: raise AppArmorException(_("'x' must be preceded by an exec qualifier (i, P, C or U)")) exec_mode = 'x' perm_string = perm_string[1:] elif perm_string.startswith(allow_exec_transitions): if exec_mode and exec_mode != perm_string[0:2]: raise AppArmorException(_('conflicting execute permissions found: %s and %s' % (exec_mode, perm_string[0:2]))) exec_mode = perm_string[0:2] perm_string = perm_string[2:] elif perm_string.startswith(allow_exec_fallback_transitions): if exec_mode and exec_mode != perm_string[0:3]: raise AppArmorException(_('conflicting execute permissions found: %s and %s' % (exec_mode, perm_string[0:3]))) exec_mode = perm_string[0:3] perm_string = perm_string[3:] else: raise AppArmorException(_('permission contains unknown character(s) %s' % perm_string)) return perms, exec_mode
def __init__(self, rlimit, value, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): super(RlimitRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) if audit or deny or allow_keyword: raise AppArmorBug('The audit, allow or deny keywords are not allowed in rlimit rules.') if type_is_str(rlimit): if rlimit in rlimit_all: self.rlimit = rlimit else: raise AppArmorException('Unknown rlimit keyword in rlimit rule: %s' % rlimit) else: raise AppArmorBug('Passed unknown object to RlimitRule: %s' % str(rlimit)) self.value = None self.value_as_int = None self.all_values = False if value == RlimitRule.ALL: self.all_values = True elif type_is_str(value): if not value.strip(): raise AppArmorBug('Empty value in rlimit rule') elif rlimit in rlimit_size: if not RE_UNIT_SIZE.match(value): raise AppArmorException('Invalid value or unit in rlimit %s %s rule' % (rlimit, value)) self.value_as_int = self.size_to_int(value) elif rlimit in rlimit_number: if not RE_NUMBER.match(value): raise AppArmorException('Invalid value in rlimit %s %s rule' % (rlimit, value)) self.value_as_int = int(value) elif rlimit in rlimit_time: if not RE_NUMBER_UNIT.match(value): raise AppArmorException('Invalid value in rlimit %s %s rule' % (rlimit, value)) number, unit = split_unit(value) if rlimit == 'rttime': self.value_as_int = self.time_to_int(value, 'us') else: self.value_as_int = self.time_to_int(value, 'seconds') elif rlimit in rlimit_nice: # pragma: no branch - "if rlimit in rlimit_all:" above avoids the need for an "else:" branch if not RE_NICE.match(value): raise AppArmorException('Invalid value or unit in rlimit %s %s rule' % (rlimit, value)) self.value_as_int = 0 - int(value) # lower numbers mean a higher limit for nice # still here? fine :-) self.value = value else: raise AppArmorBug('Passed unknown object to RlimitRule: %s' % str(value))
def _parse(cls, raw_rule): '''parse raw_rule and return SignalRule''' matches = cls._match(raw_rule) if not matches: raise AppArmorException(_("Invalid signal rule '%s'") % raw_rule) audit, deny, allow_keyword, comment = parse_modifiers(matches) rule_details = '' if matches.group('details'): rule_details = matches.group('details') if rule_details: details = RE_SIGNAL_DETAILS.search(rule_details) if not details: raise AppArmorException( _("Invalid or unknown keywords in 'signal %s" % rule_details)) if details.group('access'): access = details.group('access') if access.startswith('(') and access.endswith(')'): access = access[1:-1] access = access.replace( ',', ' ').split() # split by ',' or whitespace else: access = SignalRule.ALL if details.group('signal'): signal = details.group('signal') signal = RE_FILTER_SET_1.sub(r'\1', signal) # filter out 'set=' signal = RE_FILTER_SET_2.sub('', signal) # filter out 'set=' signal = RE_FILTER_QUOTES.sub(r' \1 ', signal) # filter out quote pairs signal = signal.replace( ',', ' ').split() # split at ',' or whitespace else: signal = SignalRule.ALL if details.group('peer'): peer = details.group('peer') else: peer = SignalRule.ALL else: access = SignalRule.ALL signal = SignalRule.ALL peer = SignalRule.ALL return SignalRule(access, signal, peer, audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment)
def output_policy(easyp, params, count, destdir, force=False, include=[]): '''Output policy''' policy = easyp.gen_policy(**params) # Inject include if it is specified if len(include) > 0: inject_s = '# injected via click hook' for f in include: inject_s += ''' #include "%s"''' % f policy = re.sub(r'(\s}\s+)$', '\n %s\\1' % inject_s, policy) out_fn = None if not destdir: # pragma: no cover if count: sys.stdout.write('### aa-easyprof profile #%d ###\n' % count) sys.stdout.write('%s\n' % policy) return None else: if 'profile_name' in params: appname = AppName(raw_name=params['profile_name']) # elif 'binary' in params: # out_fn = params['binary'] else: # pragma: no cover raise AppArmorException("Could not determine output filename") # Generate an absolute path, converting any path delimiters to '.' out_fn = os.path.join(destdir, appname.profile_filename) if not os.path.exists(destdir): os.mkdir(destdir) if not os.path.isdir(destdir): # pragma: no cover raise AppArmorException("'%s' is not a directory" % destdir) f, fn = tempfile.mkstemp(prefix='aa-easyprof') if not isinstance(policy, bytes): policy = policy.encode('utf-8') os.write(f, policy) os.close(f) # Only update if the contents are different policy_orig = "".encode('utf-8') if not force and os.path.exists(out_fn): with open(out_fn) as orig: policy_orig = orig.read() if not isinstance(policy_orig, bytes): policy_orig = policy_orig.encode('utf-8') if force or policy_orig != policy: shutil.move(fn, out_fn) os.chmod(out_fn, 0o644) else: os.unlink(fn) return out_fn
def __init__(self, varname, mode, values, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): super(VariableRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) # variables don't support audit or deny if audit: raise AppArmorBug('Attempt to initialize %s with audit flag' % self.__class__.__name__) if deny: raise AppArmorBug('Attempt to initialize %s with deny flag' % self.__class__.__name__) if not type_is_str(varname): raise AppArmorBug('Passed unknown type for varname to %s: %s' % (self.__class__.__name__, varname)) if not varname.startswith('@{'): raise AppArmorException( "Passed invalid varname to %s (doesn't start with '@{'): %s" % (self.__class__.__name__, varname)) if not varname.endswith('}'): raise AppArmorException( "Passed invalid varname to %s (doesn't end with '}'): %s" % (self.__class__.__name__, varname)) if not type_is_str(mode): raise AppArmorBug( 'Passed unknown type for variable assignment mode to %s: %s' % (self.__class__.__name__, mode)) if mode not in ['=', '+=']: raise AppArmorBug( 'Passed unknown variable assignment mode to %s: %s' % (self.__class__.__name__, mode)) if type(values) is not set: raise AppArmorBug('Passed unknown type for values to %s: %s' % (self.__class__.__name__, values)) if not values: raise AppArmorException('Passed empty list of values to %s: %s' % (self.__class__.__name__, values)) self.varname = varname self.mode = mode self.values = values
def aa_exec(command, opt, environ={}, verify_rules=[]): '''Execute binary under specified policy''' if opt.profile != None: policy_name = opt.profile else: opt.ensure_value("template_var", None) opt.ensure_value("name", None) opt.ensure_value("comment", None) opt.ensure_value("author", None) opt.ensure_value("copyright", None) binary = command[0] policy_name = gen_policy_name(binary) easyp = apparmor.easyprof.AppArmorEasyProfile(binary, opt) params = apparmor.easyprof.gen_policy_params(policy_name, opt) policy = easyp.gen_policy(**params) debug("\n%s" % policy) tmp = tempfile.NamedTemporaryFile(prefix='%s-' % policy_name) if sys.version_info[0] >= 3: tmp.write(bytes(policy, 'utf-8')) else: tmp.write(policy) tmp.flush() debug("using '%s' template" % opt.template) # TODO: get rid of this if opt.withx: rc, report = cmd( ['pkexec', 'apparmor_parser', '-r', '%s' % tmp.name]) else: rc, report = cmd(['sudo', 'apparmor_parser', '-r', tmp.name]) if rc != 0: raise AppArmorException("Could not load policy") rc, report = cmd(['sudo', 'apparmor_parser', '-p', tmp.name]) if rc != 0: raise AppArmorException("Could not dump policy") # Make sure the dynamic profile has the appropriate line for X for r in verify_rules: found = False for line in report.splitlines(): line = line.strip() if r == line: found = True break if not found: raise AppArmorException("Could not find required rule: %s" % r) set_environ(environ) args = ['aa-exec', '-p', policy_name, '--'] + command rc, report = cmd(args) return rc, report
def parse_manifest_name(name): '''Parse a manifest name''' (n, ext) = os.path.splitext(name) if not ext == ".json": raise AppArmorException("unable to parse manifest name %s" % (name)) out = n.split("_") if len(out) != 3: raise AppArmorException("unable to parse manifest name %s" % (name)) return tuple(out)
def get_translated_hotkey(translated, cmsg=''): msg = 'PromptUser: '******'Invalid hotkey for') # Originally (\S) was used but with translations it would not work :( if re.search('\((\S+)\)', translated): return re.search('\((\S+)\)', translated).groups()[0] else: if cmsg: raise AppArmorException(cmsg) else: raise AppArmorException('%s %s' % (msg, translated))
def _raw_transform(fin, fout): '''Copy input file to output file''' if fin.endswith('.profile'): name = os.path.basename(os.path.splitext(fin)[0]) else: name = os.path.basename(fin) orig = open_file_read(fin).read() out = re.sub(r'###PROFILEATTACH###', 'profile "%s"' % name, orig) tmp = name.split("_") if len(tmp) != 3: raise AppArmorException("unable to parse profile name %s" % (name)) (package, appname, version) = tuple(tmp) profile_vars = '''@{CLICK_DIR}="%s" @{APP_PKGNAME}="%s" @{APP_APPNAME}="%s" @{APP_VERSION}="%s"''' % (_get_click_dir_variable(), package, appname, version) out = re.sub(r'###VAR###', profile_vars, out) tmp, tmp_fn = tempfile.mkstemp(prefix='aa-profile-hook') if not isinstance(out, bytes): out = out.encode('utf-8') os.write(tmp, out) os.close(tmp) shutil.move(tmp_fn, fout) os.chmod(fout, 0o644)
def apparmor_available(parser="/sbin/apparmor_parser", apparmor_dirs=None): '''Is AppArmor available for use on this system''' dirs = apparmor_dirs if dirs is None: dirs = ['/sys/module/apparmor', apparmor_fs] for d in dirs: if mock_testenv and apparmor_dirs is None: # pragma: no cover break if not os.path.isdir(d): raise AppArmorException("Could not find '%s'" % d) if not os.path.exists(parser): # pragma: no cover rc, parser = apparmor.easyprof.cmd(['which', 'apparmor_parser']) if rc != 0: raise AppArmorException("Could not find apparmor_parser")
def profile_filename(self, profile_filename): if not profile_filename.startswith(self._CLICK_PREFIX): raise AppArmorException("invalid click profile name '%s'" % (profile_filename)) # Strip the click prefix off of the string self._clickname = profile_filename[len(self._CLICK_PREFIX):]
def click_name(self, click_name): if not click_name.endswith(self._CLICK_SUFFIX): raise AppArmorException("invalid click manifest name '%s'" % (click_name)) # strip the suffix off self._clickname = click_name[:-len(self._CLICK_SUFFIX)]
def __init__(self, access, peer, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): super(PtraceRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) self.access, self.all_access, unknown_items = check_and_split_list( access, access_keywords, PtraceRule.ALL, 'PtraceRule', 'access') if unknown_items: raise AppArmorException( _('Passed unknown access keyword to PtraceRule: %s') % ' '.join(unknown_items)) self.peer, self.all_peers = self._aare_or_all(peer, 'peer', is_path=False, log_event=log_event)
def parse_profile_start_line(line, filename): matches = RE_PROFILE_START.search(line) if not matches: raise AppArmorBug('The given line from file %(filename)s is not the start of a profile: %(line)s' % { 'filename': filename, 'line': line } ) result = {} for section in [ 'leadingspace', 'plainprofile', 'namedprofile', 'attachment', 'flags', 'comment']: if matches.group(section): result[section] = matches.group(section) # sections with optional quotes if section in ['plainprofile', 'namedprofile', 'attachment']: result[section] = strip_quotes(result[section]) else: result[section] = None if result['flags'] and result['flags'].strip() == '': raise AppArmorException(_('Invalid syntax in %(filename)s: Empty set of flags in line %(line)s.' % { 'filename': filename, 'line': line } )) if result['plainprofile']: result['profile'] = result['plainprofile'] result['profile_keyword'] = False else: result['profile'] = result['namedprofile'] result['profile_keyword'] = True return result
def __init__(self, title, geometry=None, driver=None, xauth=None, clipboard=False): self.geometry = geometry self.title = title self.pids = [] self.driver = driver self.clipboard = clipboard self.tempfiles = [] self.timeout = 5 # used by xauth and for server starts # preserve our environment self.old_environ = dict() for env in ['DISPLAY', 'XAUTHORITY', 'UBUNTU_MENUPROXY', 'QT_X11_NO_NATIVE_MENUBAR', 'LIBOVERLAY_SCROLLBAR']: if env in os.environ: self.old_environ[env] = os.environ[env] # prepare the new environment self.display, self.xauth = self.find_free_x_display() if xauth: abs_xauth = os.path.expanduser(xauth) if os.path.expanduser("~/.Xauthority") == abs_xauth: raise AppArmorException("Trusted Xauthority file specified. Aborting") self.xauth = abs_xauth self.new_environ = dict() self.new_environ['DISPLAY'] = self.display self.new_environ['XAUTHORITY'] = self.xauth # Disable the global menu for now self.new_environ["UBUNTU_MENUPROXY"] = "" self.new_environ["QT_X11_NO_NATIVE_MENUBAR"] = "1" # Disable the overlay scrollbar for now-- they don't track correctly self.new_environ["LIBOVERLAY_SCROLLBAR"] = "0"
def start(self): '''Start a nested X server (need to override)''' # clean up the old one if os.path.exists(self.xauth): os.unlink(self.xauth) rc, cookie = cmd(['mcookie']) if rc != 0: raise AppArmorException("Could not generate magic cookie") rc, out = cmd(['xauth', '-f', self.xauth, \ 'add', \ self.display, \ 'MIT-MAGIC-COOKIE-1', \ cookie.strip()]) if rc != 0: raise AppArmorException("Could not generate '%s'" % self.display)
def json_response(dialog_type): string = raw_input('\n') rh = json.loads(string.strip()) if rh["dialog"] != dialog_type: raise AppArmorException('Expected response %s got %s.' % (dialog_type, string)) return rh
def start(self): for e in ['Xephyr', 'matchbox-window-manager']: debug("Searching for '%s'" % e) rc, report = cmd(['which', e]) if rc != 0: raise AppArmorException("Could not find '%s'" % e) '''Run any setup code''' SandboxXserver.start(self) '''Start a Xephyr server''' listener_x = os.fork() if listener_x == 0: # TODO: break into config file? Which are needed? x_exts = [ '-extension', 'GLX', '-extension', 'MIT-SHM', '-extension', 'RENDER', '-extension', 'SECURITY', '-extension', 'DAMAGE' ] # verify_these x_extra_args = [ '-host-cursor', # less secure? '-fakexa', # for games? seems not needed '-nodri', # more secure? ] if not self.geometry: self.geometry = "640x480" x_args = [ '-nolisten', 'tcp', '-screen', self.geometry, '-br', # black background '-reset', # reset after last client exists '-terminate', # terminate at server reset '-title', self.generate_title(), ] + x_exts + x_extra_args args = ['/usr/bin/Xephyr'] + x_args + [self.display] debug(" ".join(args)) os.execv(args[0], args) sys.exit(0) self.pids.append(listener_x) time.sleep(1) # FIXME: detect if running # Next, start the window manager sys.stdout.flush() os.chdir(os.environ["HOME"]) listener_wm = os.fork() if listener_wm == 0: # update environment set_environ(self.new_environ) args = ['/usr/bin/matchbox-window-manager', '-use_titlebar', 'no'] debug(" ".join(args)) cmd(args) sys.exit(0) self.pids.append(listener_wm) time.sleep(1) # FIXME: detect if running
def _parse(cls, raw_rule): '''parse raw_rule and return ChangeProfileRule''' matches = cls._match(raw_rule) if not matches: raise AppArmorException( _("Invalid change_profile rule '%s'") % raw_rule) audit, deny, allow_keyword, comment = parse_modifiers(matches) execmode = matches.group('execmode') if matches.group('execcond'): execcond = strip_quotes(matches.group('execcond')) else: execcond = ChangeProfileRule.ALL if matches.group('targetprofile'): targetprofile = strip_quotes(matches.group('targetprofile')) else: targetprofile = ChangeProfileRule.ALL return ChangeProfileRule(execmode, execcond, targetprofile, audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment)
def find_free_x_display(self): '''Find a free X display''' old_lang = None if 'LANG' in os.environ: old_lang = os.environ['LANG'] os.environ['LANG'] = 'C' display = "" current = self.old_environ["DISPLAY"] for i in range(1, 257): # TODO: this puts an artificial limit of 256 # sandboxed applications tmp = ":%d" % i os.environ["DISPLAY"] = tmp rc, report = cmd(['xset', '-q']) if rc != 0 and 'Invalid MIT-MAGIC-COOKIE-1' not in report: display = tmp break if old_lang: os.environ['LANG'] = old_lang os.environ["DISPLAY"] = current if display == "": raise AppArmorException("Could not find available X display") # Use dedicated .Xauthority file xauth = os.path.join(os.path.expanduser('~'), \ '.Xauthority-sandbox%s' % display.split(':')[1]) return display, xauth
def generate_diff_with_comments(oldprofile, newprofile): if not os.path.exists(oldprofile): raise AppArmorException(_("Can't find existing profile %s to compare changes.") % oldprofile) newtemp = write_profile_to_tempfile(newprofile) difftemp = diff(oldprofile, newtemp.name) newtemp.close() return difftemp
def write_config(self, filename, config): """Writes the given config to the specified file""" filepath = self.CONF_DIR + '/' + filename permission_600 = stat.S_IRUSR | stat.S_IWUSR # Owner read and write try: # Open a temporary file in the CONF_DIR to write the config file config_file = tempfile.NamedTemporaryFile('w', prefix='aa_temp', delete=False, dir=self.CONF_DIR) if os.path.exists(self.input_file): # Copy permissions from an existing file to temporary file shutil.copymode(self.input_file, config_file.name) else: # If no existing permission set the file permissions as 0600 os.chmod(config_file.name, permission_600) if self.conf_type == 'shell': self.write_shell(filepath, config_file, config) elif self.conf_type == 'ini': self.write_configparser(filepath, config_file, config) config_file.close() except IOError: raise AppArmorException("Unable to write to %s" % filename) else: # Replace the target config file with the temporary file os.rename(config_file.name, filepath)
def unload_profile(profile): '''Unload a profile name from the kernel''' try: apparmor_available() except AppArmorException as e: # pragma: no cover raise AppArmorException("%s. Skipping unload" % e) _unload_profile(profile)
def rank_path(self, path, mode=None): """Returns the rank for the given path""" if '@' in path: # path contains variable return self.handle_variable_rank(path, mode) elif path[0] == '/': # file resource return self.handle_file(path, mode) else: raise AppArmorException("Unexpected path input: %s" % path)
def __init__(self, conf_type, conf_dir='/etc/apparmor'): self.CONF_DIR = conf_dir # The type of config file that'll be read and/or written if conf_type == 'shell' or conf_type == 'ini': self.conf_type = conf_type self.input_file = None else: raise AppArmorException("Unknown configuration file type")
def unload_profiles(profiles): '''Unload a list of profile names from the kernel''' try: apparmor_available() except AppArmorException as e: # pragma: no cover raise AppArmorException("%s. Skipping unload" % e) for profile in profiles: _unload_profile(profile)
def read_click_manifest(manifest): '''Read click manifest''' f = open(manifest, "r", encoding="UTF-8") j = json.load(f) for field in required_click_fields: if field not in j: raise AppArmorException("could not find required field " + "'%s' in json" % (field)) return j
def load_profile(profile, parser="/sbin/apparmor_parser", args=['-r', '--write-cache']): '''Load individual profile into the kernel''' try: apparmor_available(parser) except AppArmorException as e: # pragma: nocover raise AppArmorException("%s. Skipping load" % e) command = [parser] command.extend(args) command.append(profile) rc, output = apparmor.easyprof.cmd(command) if rc != 0: # pragma: nocover raise AppArmorException("policy load failed with exit status %d: %s" % (rc, output)) return (rc, output)