2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-22 01:57:43 +00:00

Adding userspace support for unix mediation

This commit is contained in:
Maxime Bélair 2024-03-29 13:09:06 +00:00 committed by Christian Boltz
parent b7cf7af7e2
commit 34821d16ce
29 changed files with 574 additions and 141 deletions

View File

@ -161,6 +161,9 @@ typedef struct
char *src_name;
char *class;
char *net_addr;
char *peer_addr;
} aa_log_record;
/**

View File

@ -114,6 +114,7 @@ aa_record_event_type lookup_aa_event(unsigned int type)
%token TOK_PERIOD
%token TOK_QUESTION_MARK
%token TOK_SINGLE_QUOTE
%token TOK_NONE
%token TOK_TYPE_REJECT
%token TOK_TYPE_AUDIT
@ -187,6 +188,7 @@ aa_record_event_type lookup_aa_event(unsigned int type)
%token TOK_KEY_FSTYPE
%token TOK_KEY_FLAGS
%token TOK_KEY_SRCNAME
%token TOK_KEY_UNIX_PEER_ADDR
%token TOK_KEY_CLASS
%token TOK_SOCKLOGD_KERNEL
@ -354,6 +356,13 @@ key: TOK_KEY_OPERATION TOK_EQUALS TOK_QUOTED_STRING
{ ret_record->fsuid = $3;}
| TOK_KEY_OUID TOK_EQUALS TOK_DIGITS
{ ret_record->ouid = $3;}
| TOK_KEY_ADDR TOK_EQUALS TOK_QUESTION_MARK
| TOK_KEY_ADDR TOK_EQUALS TOK_NONE
| TOK_KEY_ADDR TOK_EQUALS safe_string
{ ret_record->net_addr = $3; }
| TOK_KEY_UNIX_PEER_ADDR TOK_EQUALS TOK_NONE
| TOK_KEY_UNIX_PEER_ADDR TOK_EQUALS safe_string
{ ret_record->peer_addr = $3; }
| TOK_KEY_FSUID_UPPER TOK_EQUALS TOK_QUOTED_STRING
{ free($3);} /* Ignore - fsuid username */
| TOK_KEY_OUID_UPPER TOK_EQUALS TOK_QUOTED_STRING
@ -363,10 +372,7 @@ key: TOK_KEY_OPERATION TOK_EQUALS TOK_QUOTED_STRING
| TOK_KEY_HOSTNAME TOK_EQUALS safe_string
{ free($3); /* Ignore - hostname from user AVC messages */ }
| TOK_KEY_HOSTNAME TOK_EQUALS TOK_QUESTION_MARK
| TOK_KEY_ADDR TOK_EQUALS TOK_QUESTION_MARK
| TOK_KEY_TERMINAL TOK_EQUALS TOK_QUESTION_MARK
| TOK_KEY_ADDR TOK_EQUALS safe_string
{ free($3); /* Ignore - IP address from user AVC messages */ }
| TOK_KEY_TERMINAL TOK_EQUALS safe_string
{ free($3); /* Ignore - TTY from user AVC messages */ }
| TOK_KEY_EXE TOK_EQUALS safe_string
@ -419,14 +425,12 @@ key: TOK_KEY_OPERATION TOK_EQUALS TOK_QUOTED_STRING
{ ret_record->dbus_member = $3; }
| TOK_KEY_SIGNAL TOK_EQUALS TOK_ID
{ ret_record->signal = $3; }
| TOK_KEY_FSTYPE TOK_EQUALS TOK_QUOTED_STRING
{ ret_record->fs_type = $3; }
| TOK_KEY_FLAGS TOK_EQUALS TOK_QUOTED_STRING
{ ret_record->flags = $3; }
| TOK_KEY_SRCNAME TOK_EQUALS TOK_QUOTED_STRING
{ ret_record->src_name = $3; }
| TOK_MSG_REST
{
ret_record->event = AA_RECORD_INVALID;

View File

@ -103,6 +103,11 @@ void free_record(aa_log_record *record)
free(record->flags);
if (record->src_name != NULL)
free(record->src_name);
if (record->net_addr != NULL)
free(record->net_addr);
if (record->peer_addr != NULL)
free(record->peer_addr);
if (record->class != NULL)
free(record->class);

View File

@ -90,6 +90,7 @@ question_mark "?"
single_quote "'"
mode_chars ([RrWwaLlMmkXx])|([Pp][Xx])|([Uu][Xx])|([Ii][Xx])|([Pp][Ii][Xx])
modes ({mode_chars}+)|({mode_chars}+::{mode_chars}*)|(::{mode_chars}*)
none "none"
/* New message types */
aa_reject_type "APPARMOR_DENIED"
@ -173,6 +174,7 @@ key_flags "flags"
key_srcname "srcname"
key_class "class"
key_tcontext "tcontext"
key_unix_peer_addr "peer_addr"
audit "audit"
/* network addrs */
@ -303,6 +305,8 @@ yy_flex_debug = 0;
{period} { return(TOK_PERIOD); }
{question_mark} { return(TOK_QUESTION_MARK); }
{single_quote} { return(TOK_SINGLE_QUOTE); }
{none} { return(TOK_NONE); }
{key_apparmor} { BEGIN(audit_types); return(TOK_KEY_APPARMOR); }
{key_type} { BEGIN(audit_types); return(TOK_KEY_TYPE); }
@ -342,7 +346,7 @@ yy_flex_debug = 0;
{key_sauid} { return(TOK_KEY_SAUID); }
{key_ses} { return(TOK_KEY_SES); }
{key_hostname} { return(TOK_KEY_HOSTNAME); }
{key_addr} { return(TOK_KEY_ADDR); }
{key_addr} { BEGIN(safe_string); return(TOK_KEY_ADDR); }
{key_terminal} { return(TOK_KEY_TERMINAL); }
{key_exe} { BEGIN(safe_string); return(TOK_KEY_EXE); }
{key_comm} { BEGIN(safe_string); return(TOK_KEY_COMM); }
@ -364,6 +368,7 @@ yy_flex_debug = 0;
{key_fstype} { return(TOK_KEY_FSTYPE); }
{key_flags} { BEGIN(safe_string); return(TOK_KEY_FLAGS); }
{key_srcname} { BEGIN(safe_string); return(TOK_KEY_SRCNAME); }
{key_unix_peer_addr} { BEGIN(safe_string); return(TOK_KEY_UNIX_PEER_ADDR); }
{key_class} { BEGIN(safe_string); return(TOK_KEY_CLASS); }
{socklogd_kernel} { BEGIN(dmesg_timestamp); return(TOK_SOCKLOGD_KERNEL); }

View File

@ -115,6 +115,8 @@ int print_results(aa_log_record *record)
print_long("Peer PID", record->peer_pid, 0);
print_string("Active hat", record->active_hat);
print_string("Net Addr", record->net_addr);
print_string("Peer Addr", record->peer_addr);
print_string("Network family", record->net_family);
print_string("Socket type", record->net_sock_type);
print_string("Protocol", record->net_protocol);

View File

@ -0,0 +1 @@
type=AVC msg=audit(1711454639.955:322): apparmor="DENIED" operation="connect" class="net" profile="/home/user/test/client.py" pid=80819 comm="client.py" family="unix" sock_type="stream" protocol=0 requested="send receive connect" denied="send receive connect" addr=none peer_addr="@test_abstract_socket" peer="/home/user/test/server.py"

View File

@ -0,0 +1,18 @@
START
File: testcase_unix_01.in
Event type: AA_RECORD_DENIED
Audit ID: 1711454639.955:322
Operation: connect
Mask: send receive connect
Denied Mask: send receive connect
Profile: /home/user/test/client.py
Peer: /home/user/test/server.py
Command: client.py
PID: 80819
Peer Addr: @test_abstract_socket
Network family: unix
Socket type: stream
Protocol: ip
Class: net
Epoch: 1711454639
Audit subid: 322

View File

@ -0,0 +1,4 @@
/home/user/test/client.py {
unix (connect, receive, send) type=stream peer=(addr=@test_abstract_socket),
}

View File

@ -0,0 +1 @@
type=AVC msg=audit(1711214183.107:298): apparmor="DENIED" operation="connect" class="net" profile="/home/user/test/client.py" pid=65262 comm="server.py" family="unix" sock_type="stream" protocol=0 requested="send receive accept" denied="send accept" addr="@test_abstract_socket" peer_addr=none peer="unconfined"

View File

@ -0,0 +1,18 @@
START
File: testcase_unix_02.in
Event type: AA_RECORD_DENIED
Audit ID: 1711214183.107:298
Operation: connect
Mask: send receive accept
Denied Mask: send accept
Profile: /home/user/test/client.py
Peer: unconfined
Command: server.py
PID: 65262
Net Addr: @test_abstract_socket
Network family: unix
Socket type: stream
Protocol: ip
Class: net
Epoch: 1711214183
Audit subid: 298

View File

@ -0,0 +1,4 @@
/home/user/test/client.py {
unix (accept, send) type=stream addr=@test_abstract_socket,
}

View File

@ -0,0 +1 @@
type=AVC msg=audit(1711214069.931:292): apparmor="DENIED" operation="bind" class="net" profile="/home/user/test/client.py" pid=64952 comm="client.py" family="unix" sock_type="stream" protocol=0 requested="bind" denied="bind" addr="@test_abstract_socket"

View File

@ -0,0 +1,17 @@
START
File: testcase_unix_03.in
Event type: AA_RECORD_DENIED
Audit ID: 1711214069.931:292
Operation: bind
Mask: bind
Denied Mask: bind
Profile: /home/user/test/client.py
Command: client.py
PID: 64952
Net Addr: @test_abstract_socket
Network family: unix
Socket type: stream
Protocol: ip
Class: net
Epoch: 1711214069
Audit subid: 292

View File

@ -0,0 +1,4 @@
/home/user/test/client.py {
unix (bind) type=stream addr=@test_abstract_socket,
}

View File

@ -40,7 +40,7 @@ from apparmor.regex import (
RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL,
RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END,
RE_PROFILE_HAT_DEF, RE_PROFILE_PIVOT_ROOT, RE_PROFILE_START,
RE_PROFILE_UNIX, RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include)
RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include)
from apparmor.rule.abi import AbiRule
from apparmor.rule.capability import CapabilityRule
from apparmor.rule.change_profile import ChangeProfileRule
@ -54,6 +54,7 @@ from apparmor.rule.userns import UserNamespaceRule
from apparmor.rule.mqueue import MessageQueueRule
from apparmor.rule.io_uring import IOUringRule
from apparmor.rule.mount import MountRule
from apparmor.rule.unix import UnixRule
from apparmor.translations import init_translation
_ = init_translation()
@ -1786,6 +1787,15 @@ def collapse_log(hashlog, ignore_null_profiles=True):
if not hat_exists or not is_known_rule(aa[profile][hat], 'io_uring', io_uring_event):
log_dict[aamode][final_name]['io_uring'].add(io_uring_event)
unix = hashlog[aamode][full_profile]['unix']
for unix_access in unix.keys():
for unix_rule in unix[unix_access]:
for unix_local in unix[unix_access][unix_rule]:
for unix_peer in unix[unix_access][unix_rule][unix_local]:
unix_event = UnixRule(unix_access, unix_rule, unix_local, unix_peer)
if not hat_exists or not is_known_rule(aa[profile][hat], 'unix', unix_event):
log_dict[aamode][final_name]['unix'].add(unix_event)
mount = hashlog[aamode][full_profile]['mount']
for operation, operation_val in mount.items():
for options, options_val in operation_val.items():
@ -2046,29 +2056,6 @@ def parse_profile_data(data, file, do_include, in_preamble):
pivot_root_rules.append(pivot_root_rule)
profile_data[profname][allow]['pivot_root'] = pivot_root_rules
elif RE_PROFILE_UNIX.search(line):
matches = RE_PROFILE_UNIX.search(line).groups()
if not profile:
raise AppArmorException(_('Syntax Error: Unexpected unix entry found in file: %(file)s line: %(line)s')
% {'file': file, 'line': lineno + 1})
audit = False
if matches[0]:
audit = True
allow = 'allow'
if matches[1] and matches[1].strip() == 'deny':
allow = 'deny'
unix = matches[2].strip()
unix_rule = parse_unix_rule(unix)
unix_rule.audit = audit
unix_rule.deny = (allow == 'deny')
unix_rules = profile_data[profname][allow].get('unix', [])
unix_rules.append(unix_rule)
profile_data[profname][allow]['unix'] = unix_rules
elif RE_PROFILE_CHANGE_HAT.search(line):
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
@ -2156,6 +2143,7 @@ def match_line_against_rule_classes(line, profile, file, lineno, in_preamble):
'mqueue',
'io_uring',
'mount',
'unix',
):
if rule_name in ruletypes:
@ -2212,10 +2200,6 @@ def parse_pivot_root_rule(line):
return aarules.Raw_Pivot_Root_Rule(line)
def parse_unix_rule(line):
# XXX Do real parsing here
return aarules.Raw_Unix_Rule(line)
def write_piece(profile_data, depth, name, nhat):
pre = ' ' * depth

View File

@ -61,6 +61,7 @@ class ReadLog:
'mqueue': hasher(),
'io_uring': hasher(),
'mount': hasher(),
'unix': hasher(),
}
def prefetch_next_log_entry(self):
@ -124,6 +125,12 @@ class ReadLog:
elif ev['operation'] and (ev['operation'] == 'umount'):
ev['flags'] = event.flags
ev['fs_type'] = event.fs_type
elif ev['class'] and ev['class'] == 'net' and ev['family'] and ev['family'] == 'unix':
ev['peer'] = event.peer
ev['peer_profile'] = event.peer_profile
ev['accesses'] = event.requested_mask
ev['addr'] = event.net_addr
ev['peer_addr'] = event.peer_addr
elif ev['operation'] and ev['operation'].startswith('dbus_'):
ev['peer_profile'] = event.peer_profile
ev['bus'] = event.dbus_bus
@ -218,13 +225,19 @@ class ReadLog:
if e['fs_type'] != None:
e['fs_type'] = ('=', e['fs_type'])
if e['operation'] == 'mount':
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][e['src_name']] = True
else: # Umount
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][None] = True
return
elif e['class'] and e['class'] == 'net' and e['family'] and e['family'] == 'unix':
rule = (e['sock_type'], None) # Protocol is not supported yet.
local = (e['addr'], None, e['attr'], None)
peer = (e['peer_addr'], e['peer_profile'])
self.hashlog[aamode][full_profile]['unix'][e['denied_mask']][rule][local][peer] = True
return
elif self.op_type(e) == 'file':
# Map c (create) and d (delete) to w (logging is more detailed than the profile language)
dmask = e['denied_mask']

View File

@ -32,6 +32,7 @@ from apparmor.rule.userns import UserNamespaceRule, UserNamespaceRuleset
from apparmor.rule.mqueue import MessageQueueRule, MessageQueueRuleset
from apparmor.rule.io_uring import IOUringRule, IOUringRuleset
from apparmor.rule.mount import MountRule, MountRuleset
from apparmor.rule.unix import UnixRule, UnixRuleset
from apparmor.translations import init_translation
@ -53,6 +54,7 @@ ruletypes = {
'mqueue': {'rule': MessageQueueRule, 'ruleset': MessageQueueRuleset},
'io_uring': {'rule': IOUringRule, 'ruleset': IOUringRuleset},
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
'unix': {'rule': UnixRule, 'ruleset': UnixRuleset},
}
@ -88,11 +90,9 @@ class ProfileStorage:
data['allow'] = dict()
data['deny'] = dict()
# pivot_root, unix have a .get() fallback to list() - initialize them nevertheless
# pivot_root has a .get() fallback to list() - initialize it nevertheless
data['allow']['pivot_root'] = []
data['deny']['pivot_root'] = []
data['allow']['unix'] = []
data['deny']['unix'] = []
self.data = data
@ -184,7 +184,6 @@ class ProfileStorage:
# "old" write functions for rule types not implemented as *Rule class yet
write_functions = {
'pivot_root': write_pivot_root,
'unix': write_unix,
}
write_order = [
@ -327,23 +326,3 @@ def write_pivot_root(prof_data, depth):
data = write_pivot_root_rules(prof_data, depth, 'deny')
data.extend(write_pivot_root_rules(prof_data, depth, 'allow'))
return data
def write_unix(prof_data, depth):
data = write_unix_rules(prof_data, depth, 'deny')
data.extend(write_unix_rules(prof_data, depth, 'allow'))
return data
def write_unix_rules(prof_data, depth, allow):
pre = ' ' * depth
data = []
# no unix rules, so return
if not prof_data[allow].get('unix', False):
return data
for unix_rule in prof_data[allow]['unix']:
data.append('%s%s' % (pre, unix_rule.serialize()))
data.append('')
return data

View File

@ -51,7 +51,7 @@ RE_PROFILE_MOUNT = re.compile(RE_AUDIT_DENY + r'((?P<operation>mount|remount|umo
RE_PROFILE_SIGNAL = re.compile(RE_AUDIT_DENY + r'(signal\s*,|signal(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_PTRACE = re.compile(RE_AUDIT_DENY + r'(ptrace\s*,|ptrace(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_PIVOT_ROOT = re.compile(RE_AUDIT_DENY + r'(pivot_root\s*,|pivot_root\s+[^#]*\s*,)' + RE_EOL)
RE_PROFILE_UNIX = re.compile(RE_AUDIT_DENY + r'(unix\s*,|unix\s+[^#]*\s*,)' + RE_EOL)
RE_PROFILE_UNIX = re.compile(RE_AUDIT_DENY + r'(unix\s*,|unix(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_USERNS = re.compile(RE_AUDIT_DENY + r'(userns\s*,|userns(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_MQUEUE = re.compile(RE_AUDIT_DENY + r'(mqueue\s*,|mqueue(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_IO_URING = re.compile(RE_AUDIT_DENY + r'(io_uring\s*,|io_uring(?P<details>\s+[^#]*)\s*,)' + RE_EOL)

240
utils/apparmor/rule/unix.py Normal file
View File

@ -0,0 +1,240 @@
# ----------------------------------------------------------------------
# Copyright (C) 2024 Canonical, Ltd.
#
# 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.
#
# ----------------------------------------------------------------------
import re
from apparmor.common import AppArmorException
from apparmor.regex import RE_PROFILE_UNIX, strip_parenthesis
from apparmor.rule import AARE
from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers, logprof_value_or_all, check_and_split_list
from apparmor.translations import init_translation
_ = init_translation()
_aare = r'([][!/\\\,().*@{}\w^-]+)'
_quoted_aare = r'"([][!/\\\,().*@{}\w\s^-]+)"'
aare = rf'({_aare}|{_quoted_aare}|\(({_aare}|{_quoted_aare})\))'
aare_set = rf'({_aare}|{_quoted_aare}|\(({_aare}|{_quoted_aare})+\))'
def re_cond_set(x, y=None):
return rf'\s*({x}\s*=\s*(?P<{y or x}_cond_set>{aare_set}))\s*'
def re_cond(x, y=None):
return rf'\s*({x}\s*=\s*(?P<{y or x}_cond>{aare}))\s*'
access_flags = [
'create', 'bind', 'listen', 'accept', 'connect', 'shutdown', 'getattr', 'setattr', 'getopt', 'setopt', 'send',
'receive', 'r', 'w', 'rw'
]
join_access = r'(\s*(' + '|'.join(access_flags) + '))'
sep = r'\s*[\s,]\s*'
unix_accesses = rf'\s*(\s*(?P<accesses>\({join_access}({sep}{join_access})*\s*\)|{join_access}))?'
unix_rule_conds = rf'(\s*({re_cond_set("type")}|{re_cond_set("protocol")}))*'
unix_local_expr = rf'(\s*({re_cond("addr")}|{re_cond("label")}|{re_cond("attr")}|{re_cond("opt")}))*'
unix_peer_expr = rf'peer\s*=\s*\((\s*({re_cond("addr", "addr_peer")}|{re_cond("label", "label_peer")}))*\)'
RE_UNIX_DETAILS = re.compile(rf'^(\s*{unix_accesses})?(\s*{unix_rule_conds})?(\s*{unix_local_expr})?(\s*{unix_peer_expr})?\s*$')
class UnixRule(BaseRule):
'''Class to handle and store a single unix rule'''
# Nothing external should reference this class, all external users
# should reference the class field UnixRule.ALL
class __UnixAll(object):
pass
ALL = __UnixAll
rule_name = 'unix'
_match_re = RE_PROFILE_UNIX
def __init__(self, accesses, rule_conds, local_expr, peer_expr, audit=False, deny=False, allow_keyword=False, comment='', log_event=None):
super().__init__(audit=audit, deny=deny,
allow_keyword=allow_keyword,
comment=comment,
log_event=log_event)
if type(rule_conds) is tuple: # This comes from the logparser, we convert it to dicts
accesses = strip_parenthesis(accesses).replace(',', ' ').split()
rule_conds = _tuple_to_dict(rule_conds, ['type', 'protocol'])
local_expr = _tuple_to_dict(local_expr, ['addr', 'label', 'attr', 'opt'])
peer_expr = _tuple_to_dict(peer_expr, ['addr', 'label'])
self.accesses, self.all_accesses, unknown_items = check_and_split_list(accesses, access_flags, self.ALL, type(self).__name__, 'accesses')
if unknown_items:
raise AppArmorException(f'Invalid access in Unix rule: {unknown_items}')
self.rule_conds = _check_dict_keys(rule_conds, {'type', 'protocol'})
self.local_expr = _check_dict_keys(local_expr, {'addr', 'label', 'attr', 'opt'})
self.peer_expr = _check_dict_keys(peer_expr, {'addr', 'label'})
if not self.all_accesses and self.peer_expr != self.ALL and self.accesses & {'create', 'bind', 'listen', 'shutdown', 'getattr', 'setattr', 'getopt', 'setopt'}:
raise AppArmorException('Cannot use a peer_expr and an access in {create, bind, listen, shutdown, getattr, setattr, getopt, setopt} simultaneously')
self.can_glob = not (self.accesses or self.rule_conds or self.local_expr or self.peer_expr)
@classmethod
def _create_instance(cls, raw_rule, matches):
'''parse raw_rule and return instance of this class'''
audit, deny, allow_keyword, comment = parse_modifiers(matches)
rule_details = ''
if matches.group('details'):
rule_details = matches.group('details')
parsed = RE_UNIX_DETAILS.search(rule_details)
if not parsed:
raise AppArmorException('Cannot parse unix rule ' + raw_rule)
r = parsed.groupdict()
if r['accesses']:
accesses = strip_parenthesis(r['accesses']).replace(',', ' ').split()
else:
accesses = cls.ALL
rule_conds = _initialize_cond_dict(r, ['type', 'protocol'], '_cond_set')
local_expr = _initialize_cond_dict(r, ['addr', 'label', 'attr', 'opt'], '_cond')
peer_expr = _initialize_cond_dict(r, ['addr', 'label'], '_peer_cond')
else:
accesses = cls.ALL
rule_conds = cls.ALL
local_expr = cls.ALL
peer_expr = cls.ALL
return cls(accesses=accesses, rule_conds=rule_conds, local_expr=local_expr, peer_expr=peer_expr, audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment)
def get_clean(self, depth=0):
space = ' ' * depth
accesses = ' (%s)' % (', '.join(sorted(self.accesses))) if not self.all_accesses else ''
rule_conds = _print_dict_values(self.rule_conds)
local_expr = _print_dict_values(self.local_expr)
peer_expr = _print_dict_values(self.peer_expr, 'peer')
return f'{space}unix{self.modifiers_str()}{accesses}{rule_conds}{local_expr}{peer_expr},{self.comment}'
def _is_covered_localvars(self, other_rule):
if not self._is_covered_list(self.accesses, self.all_accesses, other_rule.accesses, other_rule.all_accesses, 'accesses'):
return False
if not self._is_covered_dict(self.rule_conds, other_rule.rule_conds):
return False
if not self._is_covered_dict(self.local_expr, other_rule.local_expr):
return False
if not self._is_covered_dict(self.peer_expr, other_rule.peer_expr):
return False
return True
def _is_equal_localvars(self, rule_obj, strict):
if self.accesses != rule_obj.accesses:
return False
if self.rule_conds != rule_obj.rule_conds:
return False
if self.local_expr != rule_obj.local_expr:
return False
if self.peer_expr != rule_obj.peer_expr:
return False
return True
def glob(self):
'''Change path to next possible glob'''
if self.peer_expr != self.ALL:
self.peer_expr = self.ALL
elif self.local_expr != self.ALL:
self.local_expr = self.ALL
elif self.rule_conds != self.ALL:
self.rule_conds = self.ALL
else: # not self.all_accesses:
self.accesses = None
self.all_accesses = True
self.raw_rule = None
def _logprof_header_localvars(self):
accesses = logprof_value_or_all(self.accesses, self.all_accesses)
rule_conds = logprof_value_or_all(self.rule_conds, self.rule_conds == UnixRule.ALL)
local_expr = logprof_value_or_all(self.local_expr, self.local_expr == UnixRule.ALL)
peer_expr = logprof_value_or_all(self.peer_expr, self.peer_expr == UnixRule.ALL)
return (
_('Accesses'), accesses,
_('Rule'), rule_conds,
_('Local'), local_expr,
_('Peer'), peer_expr,
)
def _is_covered_dict(self, d, other):
if d is self.ALL:
return True
elif other is self.ALL:
return False
for it in other:
if it not in d:
continue # No constraints on this item.
else:
if not self._is_covered_aare(AARE(d[it], False), False, AARE(other[it], False), False, it):
return False
return True
def _print_dict_values(d, prefix=None):
if d == UnixRule.ALL:
return ''
to_print = ' '.join(f'{k}={v}' for k, v in d.items())
if prefix:
return f' {prefix}=({to_print})'
else:
return f' {to_print}'
def _initialize_cond_dict(d, keys, suffix):
out = {
key: d[f'{key}{suffix}']
for key in keys
if f'{key}{suffix}' in d and d[f'{key}{suffix}'] is not None
}
return out if out != {} else UnixRule.ALL
def _check_dict_keys(d, possible_keys):
if d == UnixRule.ALL or d == {}:
return UnixRule.ALL
if not possible_keys >= d.keys():
raise AppArmorException(f'Incorrect key in dict {d}. Possible keys are {possible_keys},')
return d
def _tuple_to_dict(t, keys):
d = {}
for idx, k in enumerate(keys):
if t[idx] is not None:
d[k] = t[idx]
return d
class UnixRuleset(BaseRuleset):
'''Class to handle and store a collection of Unix rules'''

View File

@ -76,7 +76,7 @@ $foo = false
pivot_root oldroot=/mnt/root/old/,
deny owner link /some/thing -> /foo/bar ,
unix shutdown addr=@HypotheticalServiceDaemon,
unix shutdown addr=@HypotheticalServiceDaemon, # covered in abstractions/base, will be removed
link subset /alpha/beta -> /tmp/**,

View File

@ -35,7 +35,6 @@ $bar = true
pivot_root oldroot=/mnt/root/old/,
unix (receive) type=dgram,
unix shutdown addr=@HypotheticalServiceDaemon,
deny owner link /some/thing -> /foo/bar,

View File

@ -65,6 +65,8 @@ class TestLibapparmorTestMulti(AATest):
'src_name', # pivotroot
'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus
'peer_pid', 'peer_profile', # dbus
'net_addr', 'peer_addr', # unix
):
pass
elif parsed_items['operation'] == 'exec' and label in ('sock_type', 'family', 'protocol'):

View File

@ -171,35 +171,16 @@ exception_not_raised = (
'profile/profile_ns_bad8.sd', # 'profile :ns/t' without terminating ':'
'ptrace/bad_10.sd', # peer with invalid regex
'signal/bad_21.sd', # invalid regex
'unix/bad_attr_1.sd',
'unix/bad_attr_2.sd',
'unix/bad_attr_3.sd',
'unix/bad_attr_4.sd',
'unix/bad_bind_1.sd',
'unix/bad_bind_2.sd',
'unix/bad_create_1.sd',
'unix/bad_create_2.sd',
'unix/bad_listen_1.sd',
'unix/bad_listen_2.sd',
'unix/bad_modifier_1.sd',
'unix/bad_modifier_2.sd',
'unix/bad_modifier_3.sd',
'unix/bad_modifier_4.sd',
'unix/bad_opt_1.sd',
'unix/bad_opt_2.sd',
'unix/bad_opt_3.sd',
'unix/bad_opt_4.sd',
'unix/bad_peer_1.sd',
# Invalid regexes
'unix/bad_regex_01.sd',
'unix/bad_regex_02.sd',
'unix/bad_regex_03.sd',
'unix/bad_regex_04.sd',
'unix/bad_shutdown_1.sd',
'unix/bad_shutdown_2.sd',
'unix/bad_peer_2.sd',
'unix/bad_attr_5.sd',
'unix/bad_opt_5.sd',
'unix/bad_shutdown_3.sd',
'unix/bad_modifier_2.sd', # We do not check for duplicated keywords
'unix/bad_bind_2.sd', # We do not check bind coherency
'vars/vars_bad_3.sd',
'vars/vars_bad_4.sd',
'vars/vars_bad_5.sd',
@ -348,6 +329,13 @@ unknown_line = (
# Options should be comma separated
'mount/in_4.sd', # also order option then fstype is invalid
# Unsupported \\" in unix AARE
'unix/ok_regex_03.sd',
'unix/ok_regex_09.sd',
'unix/ok_regex_13.sd',
'unix/ok_regex_19.sd',
)
# testcases with various unexpected failures

View File

@ -15,7 +15,7 @@ import apparmor.aa as aa
from apparmor.common import AppArmorBug, AppArmorException
from apparmor.regex import (
RE_PROFILE_CAP, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL,
RE_PROFILE_START, parse_profile_start_line, re_match_include,
RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX,
re_match_include_parse, strip_parenthesis, strip_quotes)
from common_test import AATest, setup_aa, setup_all_loops
@ -337,19 +337,21 @@ class AARegexUnix(AARegexTest):
"""Tests for RE_PROFILE_UNIX"""
def AASetup(self):
self.regex = aa.RE_PROFILE_UNIX
self.regex = RE_PROFILE_UNIX
tests = (
(' unix,', (None, None, 'unix,', None)),
(' audit unix,', ('audit', None, 'unix,', None)),
(' unix accept,', (None, None, 'unix accept,', None)),
(' allow unix connect,', (None, 'allow', 'unix connect,', None)),
(' audit allow unix bind,', ('audit', 'allow', 'unix bind,', None)),
(' deny unix bind,', (None, 'deny', 'unix bind,', None)),
('unix peer=(label=@{profile_name}),', (None, None, 'unix peer=(label=@{profile_name}),', None)),
('unix (receive) peer=(label=unconfined),', (None, None, 'unix (receive) peer=(label=unconfined),', None)),
(' unix (getattr, shutdown) peer=(addr=none),', (None, None, 'unix (getattr, shutdown) peer=(addr=none),', None)),
('unix (connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/dbus-*"),', (None, None, 'unix (connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/dbus-*"),', None)),
(' unix,', (None, None, 'unix,', None, None)),
(' audit unix,', ('audit', None, 'unix,', None, None)),
(' unix accept,', (None, None, 'unix accept,', 'accept', None)),
(' allow unix connect,', (None, 'allow', 'unix connect,', 'connect', None)),
(' audit allow unix bind,', ('audit', 'allow', 'unix bind,', 'bind', None)),
(' deny unix bind,', (None, 'deny', 'unix bind,', 'bind', None)),
('unix peer=(label=@{profile_name}),', (None, None, 'unix peer=(label=@{profile_name}),', 'peer=(label=@{profile_name})', None)),
('unix (receive) peer=(label=unconfined),', (None, None, 'unix (receive) peer=(label=unconfined),', '(receive) peer=(label=unconfined)', None)),
(' unix (getattr, shutdown) peer=(addr=none),', (None, None, 'unix (getattr, shutdown) peer=(addr=none),', '(getattr, shutdown) peer=(addr=none)', None)),
('unix (connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/dbus-*"),', (None, None, 'unix (connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/dbus-*"),',
'(connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/dbus-*")',
None)),
('unixlike', False),
('deny unixlike,', False),
)

181
utils/test/test-unix.py Normal file
View File

@ -0,0 +1,181 @@
#!/usr/bin/python3
# ----------------------------------------------------------------------
# Copyright (C) 2024 Canonical, Ltd.
#
# 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.
#
# ----------------------------------------------------------------------
import unittest
from common_test import AATest, setup_all_loops
from apparmor.common import AppArmorException
from apparmor.translations import init_translation
from apparmor.rule.unix import UnixRule
_ = init_translation()
class UnixTestParse(AATest):
tests = (
# Rule Accesses Rule conds Local expr Peer expr Audit Deny Allow Comment
('unix,', UnixRule(UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, False, False, False, '')),
('unix rw,', UnixRule('rw', UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, False, False, False, '')),
('unix (accept, rw),', UnixRule(('accept', 'rw'), UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, False, False, False, '')),
('unix peer=(addr=AA label=bb),', UnixRule(UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, {'addr': 'AA', 'label': 'bb'}, False, False, False, '')),
('unix opt=AA label=bb,', UnixRule(UnixRule.ALL, UnixRule.ALL, {'opt': 'AA', 'label': 'bb'}, UnixRule.ALL, False, False, False, '')),
('unix (accept rw) type=AA protocol=BB,', UnixRule(('accept', 'rw'), {'type': 'AA', 'protocol': 'BB'}, UnixRule.ALL, UnixRule.ALL, False, False, False, '')),
('unix (accept, rw) protocol=AA type=BB,', UnixRule(('accept', 'rw'), {'type': 'BB', 'protocol': 'AA'}, UnixRule.ALL, UnixRule.ALL, False, False, False, '')),
('unix shutdown addr=@srv,', UnixRule('shutdown', UnixRule.ALL, {'addr': '@srv'}, UnixRule.ALL, False, False, False, '')),
('unix send addr=@foo{a,b} peer=(label=splat),', UnixRule('send', UnixRule.ALL, {'addr': '@foo{a,b}'}, {'label': 'splat'}, False, False, False, '')),
('unix (accept, rw) protocol=AA type=BB opt=AA label=bb peer=(addr=a label=bb),',
UnixRule(('accept', 'rw'), {'type': 'BB', 'protocol': 'AA'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'a', 'label': 'bb'}, False, False, False, '')),
)
def _run_test(self, rawrule, expected):
self.assertTrue(UnixRule.match(rawrule))
obj = UnixRule.create_instance(rawrule)
expected.raw_rule = rawrule.strip()
self.assertTrue(obj.is_equal(expected, True))
def test_diff_local(self):
obj1 = UnixRule('send', UnixRule.ALL, {'addr': 'foo'}, UnixRule.ALL, )
obj2 = UnixRule('send', UnixRule.ALL, UnixRule.ALL, {'addr': 'bar'})
self.assertFalse(obj1.is_equal(obj2, False))
def test_diff_peer(self):
obj1 = UnixRule('send', UnixRule.ALL, UnixRule.ALL, {'addr': 'foo'})
obj2 = UnixRule('send', UnixRule.ALL, UnixRule.ALL, {'addr': 'bar'})
self.assertFalse(obj1.is_equal(obj2, False))
class UnixTestParseInvalid(AATest):
tests = (
('unix invalid,', AppArmorException),
('unix (invalid),', AppArmorException),
)
def _run_test(self, rawrule, expected):
self.assertTrue(UnixRule.match(rawrule)) # the above invalid rules still match the main regex!
with self.assertRaises(expected):
UnixRule.create_instance(rawrule)
def test_parse_fail(self):
with self.assertRaises(AppArmorException):
UnixRule.create_instance('foo,')
def test_invalid_key(self):
with self.assertRaises(AppArmorException):
UnixRule('send', UnixRule.ALL, {'invalid': 'whatever'}, UnixRule.ALL, False, False, False, '')
def test_invalid_access(self):
with self.assertRaises(AppArmorException):
UnixRule('invalid', UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, False, False, False, '')
def test_invalid_access2(self):
with self.assertRaises(AppArmorException):
UnixRule(('rw', 'invalid'), UnixRule.ALL, UnixRule.ALL, UnixRule.ALL, False, False, False, '')
def test_invalid_peer_expr(self):
with self.assertRaises(AppArmorException):
UnixRule('create', UnixRule.ALL, UnixRule.ALL, {'addr': 'foo'}, False, False, False, '')
class UnixIsCoveredTest(AATest):
def test_is_covered(self):
obj = UnixRule(('accept', 'rw'), {'type': 'F*', 'protocol': 'AA'}, {'addr': 'AA'}, {'addr': 'AA', 'label': 'bb'})
tests = [
(('accept',), {'type': 'F*', 'protocol': 'AA'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'F*'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'Foo'}, {'addr': 'AA'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'Foo'}, {'addr': 'AA', 'opt': 'BB'}, {'addr': 'AA', 'label': 'bb'})
]
for test in tests:
self.assertTrue(obj.is_covered(UnixRule(*test)))
self.assertFalse(obj.is_equal(UnixRule(*test)))
def test_is_covered2(self):
obj = UnixRule(('accept', 'rw'), UnixRule.ALL, {'addr': 'AA'}, {'addr': 'AA', 'label': 'bb'})
tests = [
(('accept',), {'type': 'F*', 'protocol': 'AA'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'F*'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'Foo'}, {'addr': 'AA'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'Foo'}, {'addr': 'AA', 'opt': 'BB'}, {'addr': 'AA', 'label': 'bb'})
]
for test in tests:
self.assertTrue(obj.is_covered(UnixRule(*test)))
self.assertFalse(obj.is_equal(UnixRule(*test)))
def test_is_not_covered(self):
obj = UnixRule(('accept', 'rw'), {'type': 'F'}, {'opt': 'AA'}, {'addr': 'AA', 'label': 'bb'})
tests = [
(('r',), {'type': 'F*', 'protocol': 'AA'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'B'}, {'opt': 'AA', 'label': 'bb'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'F'}, {'opt': 'AA', 'label': 'bb'}, UnixRule.ALL),
(('accept', 'rw'), {'type': 'F'}, {'opt': 'notcovered'}, {'addr': 'AA', 'label': 'bb'}),
(('accept', 'rw'), {'type': 'F'}, {'opt': 'AA'}, {'addr': 'notcovered'}),
]
for test in tests:
self.assertFalse(obj.is_covered(UnixRule(*test)), test)
self.assertFalse(obj.is_equal(UnixRule(*test)))
class UnixLogprofHeaderTest(AATest):
tests = (
('unix,', [_('Accesses'), 'ALL', _('Rule'), 'ALL', _('Local'), 'ALL', _('Peer'), 'ALL']),
('unix rw,', [_('Accesses'), 'rw', _('Rule'), 'ALL', _('Local'), 'ALL', _('Peer'), 'ALL']),
('unix send addr=@foo{one,two peer=(label=splat),', [_('Accesses'), 'send', _('Rule'), 'ALL', _('Local'), {'addr': '@foo{one,two'}, _('Peer'), {'label': 'splat'}])
)
def _run_test(self, params, expected):
obj = UnixRule.create_instance(params)
self.assertEqual(obj.logprof_header(), expected)
class UnixTestGlob(AATest):
def test_glob(self):
glob_list = [(
'unix (accept, rw) type=BB protocol=AA label=bb opt=AA peer=(addr=a label=bb),',
'unix (accept, rw) type=BB protocol=AA label=bb opt=AA,',
'unix (accept, rw) type=BB protocol=AA,',
'unix (accept, rw),',
'unix,',
)]
for globs in glob_list:
for i in range(len(globs) - 1):
rule = UnixRule.create_instance(globs[i])
rule.glob()
self.assertEqual(rule.get_clean(), globs[i + 1])
class UnixTestClean(AATest):
tests = (
(' unix , # foo ', 'unix, # foo'),
(' unix addr = foo , ', 'unix addr=foo,'),
(' unix ( accept , rw) protocol = AA type = BB opt = myopt label = bb peer = (addr = a label = bb ) , ', 'unix (accept, rw) type=BB protocol=AA label=bb opt=myopt peer=(addr=a label=bb),'),
)
def _run_test(self, rawrule, expected):
self.assertTrue(UnixRule.match(rawrule))
obj = UnixRule.create_instance(rawrule)
clean = obj.get_clean()
raw = obj.get_raw()
self.assertEqual(expected, clean, 'unexpected clean rule')
self.assertEqual(rawrule.strip(), raw, 'unexpected raw rule')
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=1)

View File

@ -1,42 +0,0 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2014 Canonical Ltd.
#
# 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 published by the Free Software Foundation.
#
# ------------------------------------------------------------------
import apparmor.aa as aa
import unittest
from common_test import AAParseTest, setup_aa, setup_regex_tests
class AAParseUnixTest(AAParseTest):
def setUp(self):
self.parse_function = aa.parse_unix_rule
tests = (
('unix,', 'unix base keyword'),
('unix r,', 'unix r rule'),
('unix w,', 'unix w rule'),
('unix rw,', 'unix rw rule'),
('unix send,', 'unix send rule'),
('unix receive,', 'unix receive rule'),
('unix (r),', 'unix (r) rule'),
('unix (w),', 'unix (w) rule'),
('unix (rw),', 'unix (rw) rule'),
('unix (send),', 'unix (send) rule'),
('unix (receive),', 'unix (receive) rule'),
('unix (connect, receive, send) type=stream peer=(label=unconfined,addr="@/tmp/.X11-unix/X[0-9]*"),', 'complex unix rule'),
)
setup_aa(aa)
if __name__ == '__main__':
setup_regex_tests(AAParseUnixTest)
unittest.main(verbosity=1)