mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-22 10:07:12 +00:00
Adding userspace support for unix mediation
This commit is contained in:
parent
b7cf7af7e2
commit
34821d16ce
@ -161,6 +161,9 @@ typedef struct
|
|||||||
char *src_name;
|
char *src_name;
|
||||||
|
|
||||||
char *class;
|
char *class;
|
||||||
|
|
||||||
|
char *net_addr;
|
||||||
|
char *peer_addr;
|
||||||
} aa_log_record;
|
} aa_log_record;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,6 +114,7 @@ aa_record_event_type lookup_aa_event(unsigned int type)
|
|||||||
%token TOK_PERIOD
|
%token TOK_PERIOD
|
||||||
%token TOK_QUESTION_MARK
|
%token TOK_QUESTION_MARK
|
||||||
%token TOK_SINGLE_QUOTE
|
%token TOK_SINGLE_QUOTE
|
||||||
|
%token TOK_NONE
|
||||||
|
|
||||||
%token TOK_TYPE_REJECT
|
%token TOK_TYPE_REJECT
|
||||||
%token TOK_TYPE_AUDIT
|
%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_FSTYPE
|
||||||
%token TOK_KEY_FLAGS
|
%token TOK_KEY_FLAGS
|
||||||
%token TOK_KEY_SRCNAME
|
%token TOK_KEY_SRCNAME
|
||||||
|
%token TOK_KEY_UNIX_PEER_ADDR
|
||||||
%token TOK_KEY_CLASS
|
%token TOK_KEY_CLASS
|
||||||
|
|
||||||
%token TOK_SOCKLOGD_KERNEL
|
%token TOK_SOCKLOGD_KERNEL
|
||||||
@ -354,6 +356,13 @@ key: TOK_KEY_OPERATION TOK_EQUALS TOK_QUOTED_STRING
|
|||||||
{ ret_record->fsuid = $3;}
|
{ ret_record->fsuid = $3;}
|
||||||
| TOK_KEY_OUID TOK_EQUALS TOK_DIGITS
|
| TOK_KEY_OUID TOK_EQUALS TOK_DIGITS
|
||||||
{ ret_record->ouid = $3;}
|
{ 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
|
| TOK_KEY_FSUID_UPPER TOK_EQUALS TOK_QUOTED_STRING
|
||||||
{ free($3);} /* Ignore - fsuid username */
|
{ free($3);} /* Ignore - fsuid username */
|
||||||
| TOK_KEY_OUID_UPPER TOK_EQUALS TOK_QUOTED_STRING
|
| 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
|
| TOK_KEY_HOSTNAME TOK_EQUALS safe_string
|
||||||
{ free($3); /* Ignore - hostname from user AVC messages */ }
|
{ free($3); /* Ignore - hostname from user AVC messages */ }
|
||||||
| TOK_KEY_HOSTNAME TOK_EQUALS TOK_QUESTION_MARK
|
| 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_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
|
| TOK_KEY_TERMINAL TOK_EQUALS safe_string
|
||||||
{ free($3); /* Ignore - TTY from user AVC messages */ }
|
{ free($3); /* Ignore - TTY from user AVC messages */ }
|
||||||
| TOK_KEY_EXE TOK_EQUALS safe_string
|
| 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; }
|
{ ret_record->dbus_member = $3; }
|
||||||
| TOK_KEY_SIGNAL TOK_EQUALS TOK_ID
|
| TOK_KEY_SIGNAL TOK_EQUALS TOK_ID
|
||||||
{ ret_record->signal = $3; }
|
{ ret_record->signal = $3; }
|
||||||
|
|
||||||
| TOK_KEY_FSTYPE TOK_EQUALS TOK_QUOTED_STRING
|
| TOK_KEY_FSTYPE TOK_EQUALS TOK_QUOTED_STRING
|
||||||
{ ret_record->fs_type = $3; }
|
{ ret_record->fs_type = $3; }
|
||||||
| TOK_KEY_FLAGS TOK_EQUALS TOK_QUOTED_STRING
|
| TOK_KEY_FLAGS TOK_EQUALS TOK_QUOTED_STRING
|
||||||
{ ret_record->flags = $3; }
|
{ ret_record->flags = $3; }
|
||||||
| TOK_KEY_SRCNAME TOK_EQUALS TOK_QUOTED_STRING
|
| TOK_KEY_SRCNAME TOK_EQUALS TOK_QUOTED_STRING
|
||||||
{ ret_record->src_name = $3; }
|
{ ret_record->src_name = $3; }
|
||||||
|
|
||||||
| TOK_MSG_REST
|
| TOK_MSG_REST
|
||||||
{
|
{
|
||||||
ret_record->event = AA_RECORD_INVALID;
|
ret_record->event = AA_RECORD_INVALID;
|
||||||
|
@ -103,6 +103,11 @@ void free_record(aa_log_record *record)
|
|||||||
free(record->flags);
|
free(record->flags);
|
||||||
if (record->src_name != NULL)
|
if (record->src_name != NULL)
|
||||||
free(record->src_name);
|
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)
|
if (record->class != NULL)
|
||||||
free(record->class);
|
free(record->class);
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ question_mark "?"
|
|||||||
single_quote "'"
|
single_quote "'"
|
||||||
mode_chars ([RrWwaLlMmkXx])|([Pp][Xx])|([Uu][Xx])|([Ii][Xx])|([Pp][Ii][Xx])
|
mode_chars ([RrWwaLlMmkXx])|([Pp][Xx])|([Uu][Xx])|([Ii][Xx])|([Pp][Ii][Xx])
|
||||||
modes ({mode_chars}+)|({mode_chars}+::{mode_chars}*)|(::{mode_chars}*)
|
modes ({mode_chars}+)|({mode_chars}+::{mode_chars}*)|(::{mode_chars}*)
|
||||||
|
none "none"
|
||||||
/* New message types */
|
/* New message types */
|
||||||
|
|
||||||
aa_reject_type "APPARMOR_DENIED"
|
aa_reject_type "APPARMOR_DENIED"
|
||||||
@ -173,6 +174,7 @@ key_flags "flags"
|
|||||||
key_srcname "srcname"
|
key_srcname "srcname"
|
||||||
key_class "class"
|
key_class "class"
|
||||||
key_tcontext "tcontext"
|
key_tcontext "tcontext"
|
||||||
|
key_unix_peer_addr "peer_addr"
|
||||||
audit "audit"
|
audit "audit"
|
||||||
|
|
||||||
/* network addrs */
|
/* network addrs */
|
||||||
@ -303,6 +305,8 @@ yy_flex_debug = 0;
|
|||||||
{period} { return(TOK_PERIOD); }
|
{period} { return(TOK_PERIOD); }
|
||||||
{question_mark} { return(TOK_QUESTION_MARK); }
|
{question_mark} { return(TOK_QUESTION_MARK); }
|
||||||
{single_quote} { return(TOK_SINGLE_QUOTE); }
|
{single_quote} { return(TOK_SINGLE_QUOTE); }
|
||||||
|
{none} { return(TOK_NONE); }
|
||||||
|
|
||||||
|
|
||||||
{key_apparmor} { BEGIN(audit_types); return(TOK_KEY_APPARMOR); }
|
{key_apparmor} { BEGIN(audit_types); return(TOK_KEY_APPARMOR); }
|
||||||
{key_type} { BEGIN(audit_types); return(TOK_KEY_TYPE); }
|
{key_type} { BEGIN(audit_types); return(TOK_KEY_TYPE); }
|
||||||
@ -342,7 +346,7 @@ yy_flex_debug = 0;
|
|||||||
{key_sauid} { return(TOK_KEY_SAUID); }
|
{key_sauid} { return(TOK_KEY_SAUID); }
|
||||||
{key_ses} { return(TOK_KEY_SES); }
|
{key_ses} { return(TOK_KEY_SES); }
|
||||||
{key_hostname} { return(TOK_KEY_HOSTNAME); }
|
{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_terminal} { return(TOK_KEY_TERMINAL); }
|
||||||
{key_exe} { BEGIN(safe_string); return(TOK_KEY_EXE); }
|
{key_exe} { BEGIN(safe_string); return(TOK_KEY_EXE); }
|
||||||
{key_comm} { BEGIN(safe_string); return(TOK_KEY_COMM); }
|
{key_comm} { BEGIN(safe_string); return(TOK_KEY_COMM); }
|
||||||
@ -364,6 +368,7 @@ yy_flex_debug = 0;
|
|||||||
{key_fstype} { return(TOK_KEY_FSTYPE); }
|
{key_fstype} { return(TOK_KEY_FSTYPE); }
|
||||||
{key_flags} { BEGIN(safe_string); return(TOK_KEY_FLAGS); }
|
{key_flags} { BEGIN(safe_string); return(TOK_KEY_FLAGS); }
|
||||||
{key_srcname} { BEGIN(safe_string); return(TOK_KEY_SRCNAME); }
|
{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); }
|
{key_class} { BEGIN(safe_string); return(TOK_KEY_CLASS); }
|
||||||
|
|
||||||
{socklogd_kernel} { BEGIN(dmesg_timestamp); return(TOK_SOCKLOGD_KERNEL); }
|
{socklogd_kernel} { BEGIN(dmesg_timestamp); return(TOK_SOCKLOGD_KERNEL); }
|
||||||
|
@ -115,6 +115,8 @@ int print_results(aa_log_record *record)
|
|||||||
print_long("Peer PID", record->peer_pid, 0);
|
print_long("Peer PID", record->peer_pid, 0);
|
||||||
print_string("Active hat", record->active_hat);
|
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("Network family", record->net_family);
|
||||||
print_string("Socket type", record->net_sock_type);
|
print_string("Socket type", record->net_sock_type);
|
||||||
print_string("Protocol", record->net_protocol);
|
print_string("Protocol", record->net_protocol);
|
||||||
|
@ -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"
|
@ -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
|
@ -0,0 +1,4 @@
|
|||||||
|
/home/user/test/client.py {
|
||||||
|
unix (connect, receive, send) type=stream peer=(addr=@test_abstract_socket),
|
||||||
|
|
||||||
|
}
|
@ -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"
|
@ -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
|
@ -0,0 +1,4 @@
|
|||||||
|
/home/user/test/client.py {
|
||||||
|
unix (accept, send) type=stream addr=@test_abstract_socket,
|
||||||
|
|
||||||
|
}
|
@ -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"
|
@ -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
|
@ -0,0 +1,4 @@
|
|||||||
|
/home/user/test/client.py {
|
||||||
|
unix (bind) type=stream addr=@test_abstract_socket,
|
||||||
|
|
||||||
|
}
|
@ -40,7 +40,7 @@ from apparmor.regex import (
|
|||||||
RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL,
|
RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL,
|
||||||
RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END,
|
RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END,
|
||||||
RE_PROFILE_HAT_DEF, RE_PROFILE_PIVOT_ROOT, RE_PROFILE_START,
|
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.abi import AbiRule
|
||||||
from apparmor.rule.capability import CapabilityRule
|
from apparmor.rule.capability import CapabilityRule
|
||||||
from apparmor.rule.change_profile import ChangeProfileRule
|
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.mqueue import MessageQueueRule
|
||||||
from apparmor.rule.io_uring import IOUringRule
|
from apparmor.rule.io_uring import IOUringRule
|
||||||
from apparmor.rule.mount import MountRule
|
from apparmor.rule.mount import MountRule
|
||||||
|
from apparmor.rule.unix import UnixRule
|
||||||
from apparmor.translations import init_translation
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
_ = 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):
|
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)
|
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']
|
mount = hashlog[aamode][full_profile]['mount']
|
||||||
for operation, operation_val in mount.items():
|
for operation, operation_val in mount.items():
|
||||||
for options, options_val in operation_val.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)
|
pivot_root_rules.append(pivot_root_rule)
|
||||||
profile_data[profname][allow]['pivot_root'] = pivot_root_rules
|
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):
|
elif RE_PROFILE_CHANGE_HAT.search(line):
|
||||||
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
|
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',
|
'mqueue',
|
||||||
'io_uring',
|
'io_uring',
|
||||||
'mount',
|
'mount',
|
||||||
|
'unix',
|
||||||
):
|
):
|
||||||
|
|
||||||
if rule_name in ruletypes:
|
if rule_name in ruletypes:
|
||||||
@ -2212,10 +2200,6 @@ def parse_pivot_root_rule(line):
|
|||||||
return aarules.Raw_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):
|
def write_piece(profile_data, depth, name, nhat):
|
||||||
pre = ' ' * depth
|
pre = ' ' * depth
|
||||||
|
@ -61,6 +61,7 @@ class ReadLog:
|
|||||||
'mqueue': hasher(),
|
'mqueue': hasher(),
|
||||||
'io_uring': hasher(),
|
'io_uring': hasher(),
|
||||||
'mount': hasher(),
|
'mount': hasher(),
|
||||||
|
'unix': hasher(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def prefetch_next_log_entry(self):
|
def prefetch_next_log_entry(self):
|
||||||
@ -124,6 +125,12 @@ class ReadLog:
|
|||||||
elif ev['operation'] and (ev['operation'] == 'umount'):
|
elif ev['operation'] and (ev['operation'] == 'umount'):
|
||||||
ev['flags'] = event.flags
|
ev['flags'] = event.flags
|
||||||
ev['fs_type'] = event.fs_type
|
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_'):
|
elif ev['operation'] and ev['operation'].startswith('dbus_'):
|
||||||
ev['peer_profile'] = event.peer_profile
|
ev['peer_profile'] = event.peer_profile
|
||||||
ev['bus'] = event.dbus_bus
|
ev['bus'] = event.dbus_bus
|
||||||
@ -218,13 +225,19 @@ class ReadLog:
|
|||||||
if e['fs_type'] != None:
|
if e['fs_type'] != None:
|
||||||
e['fs_type'] = ('=', e['fs_type'])
|
e['fs_type'] = ('=', e['fs_type'])
|
||||||
|
|
||||||
|
|
||||||
if e['operation'] == 'mount':
|
if e['operation'] == 'mount':
|
||||||
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][e['src_name']] = True
|
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][e['src_name']] = True
|
||||||
else: # Umount
|
else: # Umount
|
||||||
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][None] = True
|
self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][None] = True
|
||||||
return
|
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':
|
elif self.op_type(e) == 'file':
|
||||||
# Map c (create) and d (delete) to w (logging is more detailed than the profile language)
|
# Map c (create) and d (delete) to w (logging is more detailed than the profile language)
|
||||||
dmask = e['denied_mask']
|
dmask = e['denied_mask']
|
||||||
|
@ -32,6 +32,7 @@ from apparmor.rule.userns import UserNamespaceRule, UserNamespaceRuleset
|
|||||||
from apparmor.rule.mqueue import MessageQueueRule, MessageQueueRuleset
|
from apparmor.rule.mqueue import MessageQueueRule, MessageQueueRuleset
|
||||||
from apparmor.rule.io_uring import IOUringRule, IOUringRuleset
|
from apparmor.rule.io_uring import IOUringRule, IOUringRuleset
|
||||||
from apparmor.rule.mount import MountRule, MountRuleset
|
from apparmor.rule.mount import MountRule, MountRuleset
|
||||||
|
from apparmor.rule.unix import UnixRule, UnixRuleset
|
||||||
|
|
||||||
from apparmor.translations import init_translation
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ ruletypes = {
|
|||||||
'mqueue': {'rule': MessageQueueRule, 'ruleset': MessageQueueRuleset},
|
'mqueue': {'rule': MessageQueueRule, 'ruleset': MessageQueueRuleset},
|
||||||
'io_uring': {'rule': IOUringRule, 'ruleset': IOUringRuleset},
|
'io_uring': {'rule': IOUringRule, 'ruleset': IOUringRuleset},
|
||||||
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
|
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
|
||||||
|
'unix': {'rule': UnixRule, 'ruleset': UnixRuleset},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,11 +90,9 @@ class ProfileStorage:
|
|||||||
data['allow'] = dict()
|
data['allow'] = dict()
|
||||||
data['deny'] = 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['allow']['pivot_root'] = []
|
||||||
data['deny']['pivot_root'] = []
|
data['deny']['pivot_root'] = []
|
||||||
data['allow']['unix'] = []
|
|
||||||
data['deny']['unix'] = []
|
|
||||||
|
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
@ -184,7 +184,6 @@ class ProfileStorage:
|
|||||||
# "old" write functions for rule types not implemented as *Rule class yet
|
# "old" write functions for rule types not implemented as *Rule class yet
|
||||||
write_functions = {
|
write_functions = {
|
||||||
'pivot_root': write_pivot_root,
|
'pivot_root': write_pivot_root,
|
||||||
'unix': write_unix,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write_order = [
|
write_order = [
|
||||||
@ -327,23 +326,3 @@ def write_pivot_root(prof_data, depth):
|
|||||||
data = write_pivot_root_rules(prof_data, depth, 'deny')
|
data = write_pivot_root_rules(prof_data, depth, 'deny')
|
||||||
data.extend(write_pivot_root_rules(prof_data, depth, 'allow'))
|
data.extend(write_pivot_root_rules(prof_data, depth, 'allow'))
|
||||||
return data
|
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
|
|
||||||
|
@ -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_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_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_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_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_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)
|
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
240
utils/apparmor/rule/unix.py
Normal 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'''
|
@ -76,7 +76,7 @@ $foo = false
|
|||||||
pivot_root oldroot=/mnt/root/old/,
|
pivot_root oldroot=/mnt/root/old/,
|
||||||
|
|
||||||
deny owner link /some/thing -> /foo/bar ,
|
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/**,
|
link subset /alpha/beta -> /tmp/**,
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ $bar = true
|
|||||||
pivot_root oldroot=/mnt/root/old/,
|
pivot_root oldroot=/mnt/root/old/,
|
||||||
|
|
||||||
unix (receive) type=dgram,
|
unix (receive) type=dgram,
|
||||||
unix shutdown addr=@HypotheticalServiceDaemon,
|
|
||||||
|
|
||||||
deny owner link /some/thing -> /foo/bar,
|
deny owner link /some/thing -> /foo/bar,
|
||||||
|
|
||||||
|
@ -65,6 +65,8 @@ class TestLibapparmorTestMulti(AATest):
|
|||||||
'src_name', # pivotroot
|
'src_name', # pivotroot
|
||||||
'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus
|
'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus
|
||||||
'peer_pid', 'peer_profile', # dbus
|
'peer_pid', 'peer_profile', # dbus
|
||||||
|
'net_addr', 'peer_addr', # unix
|
||||||
|
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
elif parsed_items['operation'] == 'exec' and label in ('sock_type', 'family', 'protocol'):
|
elif parsed_items['operation'] == 'exec' and label in ('sock_type', 'family', 'protocol'):
|
||||||
|
@ -171,35 +171,16 @@ exception_not_raised = (
|
|||||||
'profile/profile_ns_bad8.sd', # 'profile :ns/t' without terminating ':'
|
'profile/profile_ns_bad8.sd', # 'profile :ns/t' without terminating ':'
|
||||||
'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_2.sd',
|
# Invalid regexes
|
||||||
'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',
|
|
||||||
'unix/bad_regex_01.sd',
|
'unix/bad_regex_01.sd',
|
||||||
'unix/bad_regex_02.sd',
|
'unix/bad_regex_02.sd',
|
||||||
'unix/bad_regex_03.sd',
|
'unix/bad_regex_03.sd',
|
||||||
'unix/bad_regex_04.sd',
|
'unix/bad_regex_04.sd',
|
||||||
'unix/bad_shutdown_1.sd',
|
|
||||||
'unix/bad_shutdown_2.sd',
|
'unix/bad_modifier_2.sd', # We do not check for duplicated keywords
|
||||||
'unix/bad_peer_2.sd',
|
'unix/bad_bind_2.sd', # We do not check bind coherency
|
||||||
'unix/bad_attr_5.sd',
|
|
||||||
'unix/bad_opt_5.sd',
|
|
||||||
'unix/bad_shutdown_3.sd',
|
|
||||||
'vars/vars_bad_3.sd',
|
'vars/vars_bad_3.sd',
|
||||||
'vars/vars_bad_4.sd',
|
'vars/vars_bad_4.sd',
|
||||||
'vars/vars_bad_5.sd',
|
'vars/vars_bad_5.sd',
|
||||||
@ -348,6 +329,13 @@ unknown_line = (
|
|||||||
|
|
||||||
# Options should be comma separated
|
# Options should be comma separated
|
||||||
'mount/in_4.sd', # also order option then fstype is invalid
|
'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
|
# testcases with various unexpected failures
|
||||||
|
@ -15,7 +15,7 @@ import apparmor.aa as aa
|
|||||||
from apparmor.common import AppArmorBug, AppArmorException
|
from apparmor.common import AppArmorBug, AppArmorException
|
||||||
from apparmor.regex import (
|
from apparmor.regex import (
|
||||||
RE_PROFILE_CAP, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL,
|
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)
|
re_match_include_parse, strip_parenthesis, strip_quotes)
|
||||||
from common_test import AATest, setup_aa, setup_all_loops
|
from common_test import AATest, setup_aa, setup_all_loops
|
||||||
|
|
||||||
@ -337,19 +337,21 @@ class AARegexUnix(AARegexTest):
|
|||||||
"""Tests for RE_PROFILE_UNIX"""
|
"""Tests for RE_PROFILE_UNIX"""
|
||||||
|
|
||||||
def AASetup(self):
|
def AASetup(self):
|
||||||
self.regex = aa.RE_PROFILE_UNIX
|
self.regex = RE_PROFILE_UNIX
|
||||||
|
|
||||||
tests = (
|
tests = (
|
||||||
(' unix,', (None, None, 'unix,', None)),
|
(' unix,', (None, None, 'unix,', None, None)),
|
||||||
(' audit unix,', ('audit', None, 'unix,', None)),
|
(' audit unix,', ('audit', None, 'unix,', None, None)),
|
||||||
(' unix accept,', (None, None, 'unix accept,', None)),
|
(' unix accept,', (None, None, 'unix accept,', 'accept', None)),
|
||||||
(' allow unix connect,', (None, 'allow', 'unix connect,', None)),
|
(' allow unix connect,', (None, 'allow', 'unix connect,', 'connect', None)),
|
||||||
(' audit allow unix bind,', ('audit', 'allow', 'unix bind,', None)),
|
(' audit allow unix bind,', ('audit', 'allow', 'unix bind,', 'bind', None)),
|
||||||
(' deny unix bind,', (None, 'deny', 'unix bind,', None)),
|
(' deny unix bind,', (None, 'deny', 'unix bind,', 'bind', None)),
|
||||||
('unix peer=(label=@{profile_name}),', (None, None, 'unix peer=(label=@{profile_name}),', 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),', 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),', 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-*"),', 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),
|
('unixlike', False),
|
||||||
('deny unixlike,', False),
|
('deny unixlike,', False),
|
||||||
)
|
)
|
||||||
|
181
utils/test/test-unix.py
Normal file
181
utils/test/test-unix.py
Normal 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)
|
@ -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)
|
|
Loading…
x
Reference in New Issue
Block a user