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

Compare commits

...

8 Commits

Author SHA1 Message Date
Georgia Garcia
278ef217fe Merge add improved comparisons in policy conditionals
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1723
Merged-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-18 20:57:14 -03:00
John Johansen
db74dda3c6 Merge profiles: add /run/snapd.socket rule for curl
This ideally is a temporary fix because we do not want to allow all users
of curl to be able to access the snapd socket. However, this will work for
now until we can mediate the accesses better.

Fixes: LP: #2120669

Signed-off-by: Ryan Lee <ryan.lee@canonical.com>

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1774
Approved-by: John Johansen <john@jjmx.net>
Merged-by: John Johansen <john@jjmx.net>
2025-08-18 23:54:40 +00:00
Ryan Lee
0e58e3d7fb profiles: add /run/snapd.socket rule for curl
This ideally is a temporary fix because we do not want to allow all users
of curl to be able to access the snapd socket. However, this will work for
now until we can mediate the accesses better.

Fixes: LP: #2120669

Signed-off-by: Ryan Lee <ryan.lee@canonical.com>
2025-08-18 12:15:40 -07:00
Georgia Garcia
19f0ac1773 parser: fix validation of "defined" boolean variable conditional
"Defined" operations on boolean variables were returning the value of
the boolean variable instead of checking if it existed or not. That
caused the parser to fail to compile the profile if the boolean
variable was not defined, which is the whole purpose of the "defined"
operation.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-13 12:33:18 -03:00
Georgia Garcia
f9690cdb6c parser/tst: create generic target to generate parser test cases
Instead of having to add a new target to build generated test cases in
.gitlab-ci.yml, use a generic "gen_tests" that calls the others.

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-13 12:31:52 -03:00
Georgia Garcia
85290ca14c utils: add conditionals support 2025-08-13 12:31:52 -03:00
Georgia Garcia
8d0c248fe4 parser: expand conditionals to allow comparisons
The apparmor parser supports if comparisons of boolean variables and
the definition status of set variables.

This commit expands the currently supported set to include comparisons
such as 'in', '>', '>=', '<', '<=', '==', and '!=' between
variables and/or text.

The comparison is done in lexicographical order, and since that can
cause issues comparing numbers, comparison between sets and numbers is
not allowed and the profile will fail to compile. Please refer to
apparmor.d.pod for example and details.

This commit also adds a file that generates test cases in the
parser. It is generated automatically with make check, but you can
generate them by running

make -C tst gen_conditionals

The generated tests will be under
tst/simple_tests/generated_conditional/

Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com>
2025-08-12 20:45:31 -03:00
John Johansen
bf82f41d90 parser: add support for boolean expressions in IF conditionals
Currently the parser doesn't support 'and' and 'or'
operations. However these are going to become important once being
able to test for what features are supported lands.

Signed-off-by: John Johansen <john.johansen@canonical.com>
2025-08-12 20:37:10 -03:00
50 changed files with 1953 additions and 113 deletions

View File

@ -103,8 +103,7 @@ test-utils:
- printf '\e[0K%s:%s:%s\r\e[0K\n' section_end "$(date +%s)" install_extra_deps
# See apparmor/apparmor#221
- make -C parser/tst gen_dbus
- make -C parser/tst gen_xtrans
- make -C parser/tst gen_tests
- make -C utils check
- make -C utils/test coverage-regression
artifacts:

View File

@ -72,10 +72,12 @@ B<PROFILE FILE> = ( [ I<PREAMBLE> ] [ I<PROFILE> ] )*
B<PREAMBLE> = ( I<COMMENT> | I<VARIABLE ASSIGNMENT> | I<ALIAS RULE> | I<INCLUDE> | I<ABI> )*
Variable assignment and alias rules must come before the profile.
B<VARIABLE ASSIGNMENT> = I<VARIABLE> ('=' | '+=') (space separated values)
B<VARIABLE ASSIGNMENT> = ( I<VARIABLE> | I<BOOL_VAR> ) ('=' | '+=') (space separated values)
B<VARIABLE> = '@{' I<ALPHA> [ ( I<ALPHANUMERIC> | '_' ) ... ] '}'
B<BOOL_VAR> = '${' I<ALPHA> [ ( I<ALPHANUMERIC> | '_' ) ... ] '}'
B<ALIAS RULE> = 'alias' I<ABS PATH> '-E<gt>' I<REWRITTEN ABS PATH> ','
B<INCLUDE> = ( '#include' | 'include' ) [ 'if exists' ] ( I<ABS PATH> | I<MAGIC PATH> )
@ -130,7 +132,7 @@ B<LINE RULES> = ( I<COMMENT> | I<INCLUDE> ) [ '\r' ] '\n'
B<COMMA RULES> = ( I<CAPABILITY RULE> | I<NETWORK RULE> | I<MOUNT RULE> | I<PIVOT ROOT RULE> | I<UNIX RULE> | I<FILE RULE> | I<LINK RULE> | I<CHANGE_PROFILE RULE> | I<RLIMIT RULE> | I<DBUS RULE> | I<MQUEUE RULE> | I<IO_URING RULE> | I<USERNS RULE> | I<ALL RULE>)
B<BLOCK RULES> = ( I<SUBPROFILE> | I<HAT> | I<QUALIFIER BLOCK> )
B<BLOCK RULES> = ( I<SUBPROFILE> | I<HAT> | I<QUALIFIER BLOCK> | I<IF_BLOCK> )
B<SUBPROFILE> = 'profile' I<PROFILE NAME> [ I<ATTACHMENT SPECIFICATION> ] [ I<PROFILE FLAG CONDS> ] '{' ( I<RULES> )* '}'
@ -142,6 +144,16 @@ B<QUALIFIER BLOCK> = I<QUALIFIERS> I<BLOCK>
B<INTEGER> = (+ | -)? [[:digit:]]+
B<IF_BLOCK> = 'if' I<EXPR> I<BLOCK> [ ( 'else' 'if' I<EXPR> I<BLOCK> )* ] [ 'else' I<BLOCK> ]
B<EXPR> = ( I<EXPR> ( 'and' | 'or' ) I<EXPR> | 'not' I<EXPR> | '(' I<EXPR> ')' | I<FACTOR> )
B<FACTOR> = ( 'defined' I<VARIABLE> | [ 'defined' ] I<BOOL_VAR> | ( I<TEXT> | I<VARIABLE> ) I<OPERATOR> ( I<TEXT> | I<VARIABLE> ) )
B<OPERATOR> = ( 'in' | '==' | '!=' | '>' | '>=' | '<' | '<=' )
B<BLOCK> = '{' ( I<RULES> )* '}'
B<ACCESS TYPE> = ( 'allow' | 'deny' )
B<QUALIFIERS> = [ 'priority' '=' <INTEGER> ] [ 'audit' ] [ I<ACCESS TYPE> ]
@ -1880,6 +1892,13 @@ example) is collapsed.
=back
=head3 Boolean Variables
In addition to the set variables AppArmor supports boolean
variables. These begin with a B<$> and can only be used in conditional
expressions. Boolean variables provide a convenient way to
enable/disable policy rules that have been wrapped in the proper if
condition.
=head2 Alias rules
@ -2203,6 +2222,107 @@ An example AppArmor profile:
}
}
=head2 Conditional rules
AppArmor provides a mechanism to conditionally enable and disable
rules in a profile. Rules that are to be conditionally used can be
wrapped in an I<if> condition block with the condition expression
being controlled by setting variable values.
The condition expression can be composed of variables, boolean
variables, a B<defined> check, the comparison operators B<in>, B<==>,
B<!=>, B<>>, B<>=>, B<<>, and B<<=>, open '(' and close ')' parentheses,
and the boolean operators B<and>, B<or>, and B<not>. Boolean operators
are evaluated left to write with priority in order of the
following list.
=over 4
=item B<in> - tests whether the left text or variable is a subset of
the text or variable on the right. The subset check is done by a full
match, therefore partial string matches would not evaluate to true.
=item B<==> - tests whether both text or variable evaluate to the same
value.
=item B<!=> - tests whether both text or variable evaluate to
different values.
=item B<>> - tests whether the left text or variable is greater than
the right text or variable.
Comparisons between integers and set variables will fail to compile
unless the variable contents are exactly one value, an integer. Other
comparison will be strictly lexicographical.
=item B<>=> - tests whether the left text or variable is greater than
or equal to the right text or variable.
Comparisons between integers and set variables will fail to compile
unless the variable contents are exactly one value, an integer. Other
comparison will be strictly lexicographical.
=item B<<> - tests whether the left text or variable is lesser than
the right text or variable.
Comparisons between integers and set variables will fail to compile
unless the variable contents are exactly one value, an integer. Other
comparison will be strictly lexicographical.
=item B<<=> - tests whether the left text or variable is lesser than
or equal to the right text or variable.
Comparisons between integers and set variables will fail to compile
unless the variable contents are exactly one value, an integer. Other
comparison will be strictly lexicographical.
=item B<defined> - tests whether the following variable or boolean
variable has been defined/created.
=item B<parentheses> - open '(' and close ')' paretheses are used to
group operations by priority.
=item B<not> - boolean B<not> operator, negates the value of the
following expression.
=item B<and> - boolean B<and> operator, both expressions being
combined with B<and> must be true for the result to be true.
=item B<or> - boolean B<or> operator, either one or both expressions
being combined with B<or> can be true for the result to be true.
=back
An example of a profile conditional
if ${distro_mods} and defined @{HOME} {
/@{HOME}/.foo_file rw,
} else {
/home/*/.foo_file rw,
}
Since lexicographical comparisons using the B<>>, B<>=>, B<<>, B<<=>
operators could lead to mistakes when comparing integers, comparisons
between variables and integers will fail to compile unless the
variable contains an integer.
Eg.
@{BAR} = /home/user/ /home/user2/
@{BAR} += /home/user3/
@{TEST_VERSION} = 2
@{BAZ} = 10
/usr/bin/foo {
if /home/user/ in @{BAR} {
/** r,
}
if @{TEST_VERSION} >= @{BAZ} {
/** w,
} else if 10 > @{TEST_VERSION} {
/** rw,
}
if @{BAZ} <= 10 {
/** rw,
}
}
=head1 FILES
=over 4

View File

@ -16,6 +16,8 @@
* Ltd.
*/
#include <algorithm>
#include "cond_expr.h"
#include "parser.h"
#include "symtab.h"
@ -25,17 +27,16 @@ cond_expr::cond_expr(bool result):
{
}
cond_expr::cond_expr(const char *var, bool defined)
cond_expr::cond_expr(const char *var, cond_op op)
{
variable *ref;
if (!defined) {
if (op == BOOLEAN_OP) {
ref = symtab::get_boolean_var(var);
if (!ref) {
/* FIXME check for set var */
yyerror(_("Unset boolean variable %s used in if-expression"), var);
}
result = ref->boolean;
} else {
} else if (op == DEFINED_OP) {
ref = symtab::get_set_var(var);
if (!ref) {
result = false;
@ -43,5 +44,106 @@ cond_expr::cond_expr(const char *var, bool defined)
PDEBUG("Matched: defined set expr %s value %s\n", var, ref->expanded.begin()->c_str());
result = true;
}
} else
PERROR("Invalid operation for if-expression");
}
/* variables passed in conditionals can be variables or values.
if the string passed has the formatting of a variable (@{}), then
we should look for it in the symtab. if it's present in the symtab,
expand its values and return the expanded set. if it's not present
in the symtab, we should error out. if the string passed does not
have the formatting of a variable, we should treat it as if it was
a value. add it to a set and return it so comparisons can be made.
*/
std::set<std::string> cond_expr::get_set(const char *var)
{
char *var_name = variable::process_var(var);
if (!var_name) {
/* not a variable */
return {var};
}
variable *ref = symtab::lookup_existing_symbol(var_name);
free(var_name);
if (!ref) {
yyerror(_("Error retrieving variable %s"), var);
}
if (ref->expand_variable() != 0) {
/* expand_variable prints error messages already, so
* exit quietly here */
exit(1);
}
return ref->expanded;
}
template <typename T>
void cond_expr::compare(cond_op op, T lhs, T rhs)
{
switch (op) {
case GT_OP:
result = lhs > rhs;
break;
case GE_OP:
result = lhs >= rhs;
break;
case LT_OP:
result = lhs < rhs;
break;
case LE_OP:
result = lhs <= rhs;
break;
default:
PDEBUG("Invalid op\n");
}
}
bool nullstr(char *p)
{
return p && !(*p);
}
long str_set_to_long(std::set<std::string> &src, char **endptr)
{
long converted_src = 0;
errno = 0;
if (src.size() == 1 && !src.begin()->empty())
converted_src = strtol(src.begin()->c_str(), endptr, 0);
if (errno == ERANGE)
yyerror(_("Value out of valid range\n"));
return converted_src;
}
cond_expr::cond_expr(const char *lhv, cond_op op, const char *rhv)
{
std::set<std::string> lhs = get_set(lhv);
std::set<std::string> rhs = get_set(rhv);
char *p_lhs = NULL, *p_rhs = NULL;
long converted_lhs = 0, converted_rhs = 0;
if (op == IN_OP) {
/* if lhs is a subset of rhs */
result = std::includes(rhs.begin(), rhs.end(),
lhs.begin(), lhs.end());
return;
} else if (op == EQ_OP) {
result = lhs == rhs;
return;
} else if (op == NE_OP) {
result = lhs != rhs;
return;
}
converted_lhs = str_set_to_long(lhs, &p_lhs);
converted_rhs = str_set_to_long(rhs, &p_rhs);
if (!nullstr(p_lhs) && !nullstr(p_rhs)) {
/* sets */
compare(op, lhs, rhs);
} else if (nullstr(p_lhs) && nullstr(p_rhs)) {
/* numbers */
compare(op, converted_lhs, converted_rhs);
} else {
yyerror(_("Can only compare numbers with numbers\n"));
}
}

View File

@ -19,12 +19,31 @@
#ifndef __AA_COND_EXPR_H
#define __AA_COND_EXPR_H
#include <set>
#include <string>
typedef enum {
EQ_OP,
NE_OP,
IN_OP,
GT_OP,
GE_OP,
LT_OP,
LE_OP,
BOOLEAN_OP,
DEFINED_OP,
} cond_op;
class cond_expr {
private:
bool result;
public:
cond_expr(bool result);
cond_expr(const char *var, bool defined);
cond_expr(const char *var, cond_op op);
cond_expr(const char *var, cond_op op, const char *cond_id);
std::set<std::string> get_set(const char *var);
template <typename T>
void compare(cond_op op, T lhs, T rhs);
virtual ~cond_expr()
{
};

View File

@ -299,7 +299,11 @@ ADD_ASSIGN \+=
ARROW ->
LT_EQUAL <=
LT <
GT_EQUAL >=
GT >
EQ_OP ==
NE_OP !=
IN_OP in
/* IF adding new state please update state_names table and default rule (just
* above the state_names table) at the eof.
@ -336,6 +340,8 @@ GT >
%x MQUEUE_MODE
%x IOURING_MODE
%x INTEGER_MODE
%x EXPR_MODE
%%
%{
@ -347,7 +353,7 @@ GT >
}
%}
<INITIAL,SUB_ID_WS,INCLUDE,INCLUDE_EXISTS,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,ASSIGN_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,RLIMIT_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE,INTEGER_MODE>{
<INITIAL,SUB_ID_WS,INCLUDE,INCLUDE_EXISTS,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,ASSIGN_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,RLIMIT_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE,INTEGER_MODE,EXPR_MODE>{
{WS}+ { DUMP_PREPROCESS; /* Ignoring whitespace */ }
}
@ -647,6 +653,41 @@ GT >
}
}
<INITIAL,EXPR_MODE>{
{SET_VARIABLE} {
yylval.set_var = strdup(yytext);
RETURN_TOKEN(TOK_SET_VAR);
}
{BOOL_VARIABLE} {
yylval.bool_var = strdup(yytext);
RETURN_TOKEN(TOK_BOOL_VAR);
}
}
<EXPR_MODE>{
{OPEN_BRACE} { POP_AND_RETURN(TOK_OPEN); }
{OPEN_PAREN} { RETURN_TOKEN(TOK_OPENPAREN); }
{CLOSE_PAREN} { RETURN_TOKEN(TOK_CLOSEPAREN); }
and { RETURN_TOKEN(TOK_AND); }
or { RETURN_TOKEN(TOK_OR); }
not { RETURN_TOKEN(TOK_NOT); }
defined { RETURN_TOKEN(TOK_DEFINED); }
{EQ_OP} { RETURN_TOKEN(TOK_EQ_OP); }
{NE_OP} { RETURN_TOKEN(TOK_NE_OP); }
{IN_OP} { RETURN_TOKEN(TOK_IN_OP); }
{GT} { RETURN_TOKEN(TOK_GT); }
{GT_EQUAL} { RETURN_TOKEN(TOK_GE); }
{LT} { RETURN_TOKEN(TOK_LT); }
{LT_EQUAL} { RETURN_TOKEN(TOK_LE); }
({IDS_NOEQ}|{LABEL}|{QUOTED_ID}) {
yylval.id = processid(yytext, yyleng);
RETURN_TOKEN(TOK_ID);
}
}
#include{WS}+if{WS}+exists/{WS}.*\r?\n {
/* Don't use PUSH() macro here as we don't want #include echoed out.
* It needs to be handled specially
@ -694,16 +735,6 @@ all/({WS}|[^[:alnum:]_]) {
{ADD_ASSIGN} { PUSH_AND_RETURN(ASSIGN_MODE, TOK_ADD_ASSIGN); }
{SET_VARIABLE} {
yylval.set_var = strdup(yytext);
RETURN_TOKEN(TOK_SET_VAR);
}
{BOOL_VARIABLE} {
yylval.bool_var = strdup(yytext);
RETURN_TOKEN(TOK_BOOL_VAR);
}
{OPEN_BRACE} { RETURN_TOKEN(TOK_OPEN); }
{CLOSE_BRACE} { RETURN_TOKEN(TOK_CLOSE); }
@ -772,6 +803,9 @@ all/({WS}|[^[:alnum:]_]) {
case TOK_MQUEUE:
state = MQUEUE_MODE;
break;
case TOK_IF:
state = EXPR_MODE;
break;
default: /* nothing */
break;
}
@ -786,14 +820,14 @@ all/({WS}|[^[:alnum:]_]) {
}
}
<INITIAL,SUB_ID_WS,INCLUDE,INCLUDE_EXISTS,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,RLIMIT_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE>{
<INITIAL,SUB_ID_WS,INCLUDE,INCLUDE_EXISTS,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,RLIMIT_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE,EXPR_MODE>{
\r?\n {
DUMP_PREPROCESS;
current_lineno++;
}
}
<INITIAL,SUB_ID,SUB_ID_WS,SUB_VALUE,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,ASSIGN_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,RLIMIT_MODE,INCLUDE,INCLUDE_EXISTS,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE>{
<INITIAL,SUB_ID,SUB_ID_WS,SUB_VALUE,LIST_VAL_MODE,EXTCOND_MODE,LIST_COND_VAL,LIST_COND_PAREN_VAL,LIST_COND_MODE,EXTCONDLIST_MODE,ASSIGN_MODE,NETWORK_MODE,CHANGE_PROFILE_MODE,MOUNT_MODE,DBUS_MODE,SIGNAL_MODE,PTRACE_MODE,UNIX_MODE,RLIMIT_MODE,INCLUDE,INCLUDE_EXISTS,ABI_MODE,USERNS_MODE,MQUEUE_MODE,IOURING_MODE,EXPR_MODE>{
(.|\n) {
DUMP_PREPROCESS;
/* Something we didn't expect */
@ -832,4 +866,5 @@ unordered_map<int, string> state_names = {
STATE_TABLE_ENT(MQUEUE_MODE),
STATE_TABLE_ENT(IOURING_MODE),
STATE_TABLE_ENT(INTEGER_MODE),
STATE_TABLE_ENT(EXPR_MODE),
};

View File

@ -134,6 +134,8 @@ static const unordered_map<string, int> keyword_table = {
{"sqpoll", TOK_SQPOLL},
{"all", TOK_ALL},
{"priority", TOK_PRIORITY},
{"and", TOK_AND},
{"or", TOK_OR},
};
/* glibc maps bsd ofile to nofile but musl does not. */

View File

@ -152,6 +152,14 @@ static void abi_features(char *filename, bool search);
%token TOK_SQPOLL
%token TOK_ALL
%token TOK_PRIORITY
%token TOK_AND
%token TOK_OR
%token TOK_EQ_OP
%token TOK_NE_OP
%token TOK_IN_OP
%token TOK_GT
%token TOK_GE
%token TOK_LT
/* rlimits */
%token TOK_RLIMIT
@ -270,6 +278,9 @@ static void abi_features(char *filename, bool search);
%type <var_val> TOK_VALUE
%type <val_list> valuelist
%type <cond> expr
%type <cond> term
%type <cond> notfactor
%type <cond> factor
%type <id> id_or_var
%type <id> opt_id_or_var
%type <boolean> opt_subset_flag
@ -942,37 +953,134 @@ cond_rule: TOK_IF expr block TOK_ELSE cond_rule
$$ = ret;
}
expr: TOK_NOT expr
expr: expr TOK_OR term
{
cond_expr *conds = new cond_expr($1->eval() || $3->eval());
delete $1;
delete $3;
$$ = conds;
}
| term
{
$$ = $1;
}
term: term TOK_AND notfactor
{
cond_expr *conds = new cond_expr($1->eval() && $3->eval());
delete $1;
delete $3;
$$ = conds;
}
| notfactor
{
$$ = $1;
}
notfactor: TOK_NOT notfactor
{
cond_expr *conds = new cond_expr(!$2->eval());
delete $2;
$$ = conds;
}
expr: TOK_BOOL_VAR
| factor
{
cond_expr *conds = new cond_expr($1, false);
$$ = $1;
}
factor: TOK_BOOL_VAR
{
cond_expr *conds = new cond_expr($1, BOOLEAN_OP);
PDEBUG("Matched: boolean expr %s value: %d\n", $1, conds->eval());
$$ = conds;
free($1);
}
expr: TOK_DEFINED TOK_SET_VAR
factor: TOK_DEFINED TOK_SET_VAR
{
cond_expr *conds = new cond_expr($2, true);
cond_expr *conds = new cond_expr($2, DEFINED_OP);
PDEBUG("Matched: defined set expr %s value %d\n", $2, conds->eval());
$$ = conds;
free($2);
}
expr: TOK_DEFINED TOK_BOOL_VAR
factor: TOK_DEFINED TOK_BOOL_VAR
{
cond_expr *conds = new cond_expr($2, false);
cond_expr *conds = new cond_expr($2, DEFINED_OP);
PDEBUG("Matched: defined set expr %s value %d\n", $2, conds->eval());
$$ = conds;
free($2);
}
factor: TOK_OPENPAREN expr TOK_CLOSEPAREN
{
$$ = $2;
}
factor: id_or_var TOK_EQ_OP id_or_var
{
cond_expr *conds = new cond_expr($1, EQ_OP, $3);
PDEBUG("Matched: equal set expr %s == %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_NE_OP id_or_var
{
cond_expr *conds = new cond_expr($1, NE_OP, $3);
PDEBUG("Matched: not equal set expr %s != %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_IN_OP id_or_var
{
cond_expr *conds = new cond_expr($1, IN_OP, $3);
PDEBUG("Matched: in set expr %s in %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_GT id_or_var
{
cond_expr *conds = new cond_expr($1, GT_OP, $3);
PDEBUG("Matched: greater set expr %s > %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_GE id_or_var
{
cond_expr *conds = new cond_expr($1, GE_OP, $3);
PDEBUG("Matched: greater or equal set expr %s >= %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_LT id_or_var
{
cond_expr *conds = new cond_expr($1, LT_OP, $3);
PDEBUG("Matched: less set expr %s < %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
factor: id_or_var TOK_LE id_or_var
{
cond_expr *conds = new cond_expr($1, LE_OP, $3);
PDEBUG("Matched: less or equal set expr %s <= %s value %d\n", $1, $3, conds->eval());
$$ = conds;
free($1);
free($3);
}
id_or_var: TOK_ID { $$ = $1; }
id_or_var: TOK_SET_VAR { $$ = $1; };

View File

@ -17,10 +17,10 @@ endif
all: tests
.PHONY: tests error_output gen_dbus gen_xtrans parser_sanity caching minimize equality dirtest valgrind
.PHONY: tests error_output gen_dbus gen_xtrans parser_sanity caching minimize equality dirtest valgrind gen_conditionals gen_tests
tests: error_output caching minimize equality dirtest parser_sanity
GEN_TRANS_DIRS=simple_tests/generated_x/ simple_tests/generated_perms_leading/ simple_tests/generated_perms_safe/ simple_tests/generated_dbus
GEN_TRANS_DIRS=simple_tests/generated_x/ simple_tests/generated_perms_leading/ simple_tests/generated_perms_safe/ simple_tests/generated_dbus simple_tests/generated_conditional/
gen_xtrans: $(GEN_TRANS_DIRS)
./gen-xtrans.py
@ -31,10 +31,15 @@ $(GEN_TRANS_DIRS):
gen_dbus: $(GEN_TRANS_DIRS)
./gen-dbus.py
gen_conditionals: $(GEN_TRANS_DIRS)
./gen-conditionals.py
gen_tests: gen_xtrans gen_dbus gen_conditionals
error_output: $(PARSER)
LANG=C ./errors.py -p "$(PARSER)" $(PYTEST_ARG)
parser_sanity: $(PARSER) gen_xtrans gen_dbus
parser_sanity: $(PARSER) gen_tests
$(Q)LANG=C APPARMOR_PARSER="$(PARSER)" ${PROVE} ${PROVE_ARG} ${TESTS}
# use this target for faster manual testing if you don't want/need to test all the profiles generated by gen-*.py
@ -54,7 +59,7 @@ equality: $(PARSER)
dirtest: $(PARSER)
LANG=C APPARMOR_PARSER="$(PARSER) $(PARSER_ARGS)" ./dirtest.sh
valgrind: $(PARSER) gen_xtrans gen_dbus
valgrind: $(PARSER) gen_tests
LANG=C ./valgrind_simple.py -p "$(PARSER) $(PARSER_ARGS)" -v simple_tests
$(PARSER):

75
parser/tst/gen-conditionals.py Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/python3
#
# Copyright (c) 2025 Canonical, Ltd. (All rights reserved)
#
# 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.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Canonical Ltd.
#
from testlib import write_file
def gen_file(test, xres, op, lhs, rhs, add_else):
global count
content = ''
content += '#\n'
content += '#=DESCRIPTION {}\n'.format(test)
content += '#=EXRESULT {}\n'.format(xres)
content += '#\n'
if lhs['def'] and lhs != rhs:
content += '{} = {}\n'.format(lhs['varname'], lhs['def'])
if rhs['def']:
content += '{} = {}\n'.format(rhs['varname'], rhs['def'])
content += '/usr/bin/foo {\n'
content += ' if {} {} {} {{\n'.format(lhs['varname'], op, rhs['varname'])
content += ' /bin/true rix,\n'
content += ' }'
if add_else:
content += ' else {\n'
content += ' mount,\n'
content += ' }\n'
else:
content += '\n'
content += '}\n'
write_file('simple_tests/generated_conditional', '{}{}-{}.sd'.format(test, '-else' if add_else else '', count), content)
count += 1
ops = {'==': 'equals', '!=': 'notequals', 'in': 'in', '>': 'greater', '>=': 'greaterequals', '<': 'lesser', '<=': 'lesserequals'}
test_vars = [
{'varname': '@{VAR_EMPTY}', 'def': '""', 'desc': 'empty', 'number': False}, # empty var
{'varname': '@{VAR_ONE_STRING}', 'def': '/path/foo/', 'desc': 'var_one_string', 'number': False}, # one string in var
{'varname': '@{VAR_ONE_NUMBER}', 'def': '10', 'desc': 'var_one_number', 'number': True}, # one number in var
{'varname': '@{VAR_MULT_STRING}', 'def': '/path/foo/ /path/bar/', 'desc': 'var_mult_string', 'number': False}, # multiple strings in var
{'varname': '@{VAR_MULT_NUMBER}', 'def': '10 2 3.1', 'desc': 'var_mult_number', 'number': False}, # multiple numbers in var
{'varname': '@{VAR_MIXED}', 'def': '3 /foo 1 /bar/ 10 /path/foo/', 'desc': 'var_mixed', 'number': False}, # mixed var contents
{'varname': '10', 'def': '', 'desc': 'number1', 'number': True}, # number directly
{'varname': '9', 'def': '', 'desc': 'number2', 'number': True}, # number directly
{'varname': '/path/foo/', 'def': '', 'desc': 'string1', 'number': False}, # string directly
{'varname': '/path/baz/', 'def': '', 'desc': 'string2', 'number': False}, # string directly
]
def gen_files():
for op in ops:
for lhs in test_vars:
for rhs in test_vars:
for add_else in [True, False]:
test_description = lhs['desc'] + '-' + ops[op] + '-' + rhs['desc']
xres = 'PASS' if lhs['number'] == rhs['number'] or op in ['in', '==', '!='] else 'FAIL'
gen_file(test_description, xres, op, lhs, rhs, add_else)
count = 0
gen_files()
print('Generated {} conditional tests'.format(count))

View File

@ -0,0 +1,12 @@
#=DESCRIPTION @FOO is not a boolean variable, but $FOO is defined
#=EXRESULT FAIL
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not @FOO {
/bin/true rix,
}
}

View File

@ -0,0 +1,10 @@
#=DESCRIPTION trying to use undefined boolean
#=EXRESULT FAIL
$FOO=true
/bin/true {
if $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,9 @@
#=DESCRIPTION unfinished else
#=EXRESULT FAIL
$BAR=true
/bin/true {
if $BAR {
/bin/true rix,
} else {

View File

@ -0,0 +1,19 @@
#=DESCRIPTION duplicated hats inside a conditional
#=EXRESULT FAIL
${FOO} = true
/bin/true {
^dupehat {
/bin/false rix,
}
if not ${FOO} {
/bin/true rm,
} else {
^dupehat {
capability dac_override,
}
}
}

View File

@ -0,0 +1,11 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
/bin/true {
if $FOO and $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,11 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
/bin/true {
if $FOO or $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,11 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
/bin/true {
if ( $FOO and $BAR ) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if $FOO and $BAR and $THREE {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if ($FOO and $BAR) and $THREE {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if $FOO and ($BAR and $THREE) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if $FOO and (($BAR and $THREE)) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if ($FOO and (($BAR and $THREE))) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if ($FOO) and ($BAR or $THREE) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if ($FOO and $BAR) or $THREE {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if (($FOO and $BAR) or $THREE) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if (($FOO and $BAR) or ($THREE)) {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not $FOO and $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not $FOO and not $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not not $FOO and not $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if defined $FOO and $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if defined $FOO and defined $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not defined $FOO and defined $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not defined $FOO and not defined $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,12 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
$BAR=true
$THREE=true
/bin/true {
if not defined @FOO or not defined $BAR {
/bin/true rix,
}
}

View File

@ -0,0 +1,11 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
$FOO=true
@{BAR}=10
/bin/true {
if $FOO and @{BAR} >= 10 {
/bin/true rix,
}
}

View File

@ -0,0 +1,8 @@
#=DESCRIPTION conditional within profile
#=EXRESULT PASS
/bin/true {
if defined $UNKNOWN {
/bin/true rix,
}
}

View File

@ -66,8 +66,6 @@ char *variable::process_var(const char *var)
orig++;
len--;
} else {
PERROR("ASSERT: Found var '%s' without variable prefix\n",
var);
return NULL;
}

View File

@ -42,6 +42,10 @@ profile curl /usr/bin/curl {
network inet6 stream,
network inet6 dgram,
# Allow access to the snap socket until we can revisit it with delegation
# or profile refactoring
file rw @{run}/snapd.socket,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/curl>
}

View File

@ -284,7 +284,7 @@ suites:
summary: Unit tests for the Python utilities.
prepare: |
# Generate apparmor profiles that the tests rely on.
make -C "$SPREAD_PATH"/parser/tst gen_xtrans gen_dbus
make -C "$SPREAD_PATH"/parser/tst gen_tests
# Spread does not support programmatically generated test variants.
# Ensure that the list baked into utils/test/task.yaml contains all
# the files matching utils/test/test-*.py

View File

@ -35,13 +35,14 @@ from apparmor.common import (
from apparmor.profile_list import ProfileList, preamble_ruletypes
from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, ruletypes
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_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL_START,
RE_PROFILE_CONDITIONAL_ELSE, RE_PROFILE_END,
RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_METADATA_LOGPROF_SUGGEST,
RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include)
from apparmor.rule.abi import AbiRule
from apparmor.rule.file import FileRule
from apparmor.rule.include import IncludeRule
from apparmor.rule.conditional import ConditionalBlock
from apparmor.logparser import ReadLog
from apparmor.translations import init_translation
@ -1783,28 +1784,57 @@ def attach_profile_data(profiles, profile_data):
profiles[p] = deepcopy(profile_data[p])
def parse_profile_data(data, file, do_include, in_preamble):
def parse_conditional(conditional_block, profile_data, data, lineno, file, do_include, in_preamble, profname, profile, debug_lineno):
cond_storage = ProfileStorage(profile_data[profname]['info']['profile'],
profile_data[profname]['info']['hat'],
profile_data[profname]['info']['calledby'] + ' in cond')
ret_lineno, ret_profile_data, end_of_block = parse_block(data[lineno + 1:], file, do_include, in_preamble, profname, profile, cond_storage, debug_lineno, in_if=True)
conditional_block.store_profile_data(ret_profile_data)
lineno = lineno + ret_lineno
if conditional_block.result:
for cond_profname in ret_profile_data:
if ret_profile_data[cond_profname]['in_cond']:
if profile_data.get(cond_profname, False):
cond_prof_info = ret_profile_data[cond_profname].data['info']
cond_prof_name = combine_name(cond_prof_info['profile'], cond_prof_info['hat'])
raise AppArmorException(
'Profile %(profile)s defined twice in %(file)s, last found in line %(line)s'
% {'file': file, 'line': debug_lineno + 1, 'profile': cond_prof_name})
profile_data[cond_profname] = ret_profile_data[cond_profname]
if not end_of_block:
i = 1
if lineno + i < len(data):
next_line = data[lineno + i].strip()
matches = RE_PROFILE_CONDITIONAL_ELSE.search(next_line)
if matches and matches.group('close'):
data[lineno + i] = data[lineno + i].replace('}', '', 1)
else:
profile_data[profname]['cond_block'].add(conditional_block)
conditional_block = None
return lineno, conditional_block
def parse_block(data, file, do_include, in_preamble, profname, profile, prof_storage, src_lineno, in_if=False):
profile_data = {}
profile = None
hat = None
profname = None
in_contained_hat = None
parsed_profiles = []
initial_comment = ''
lastline = None
conditional_block = None
active_profiles.init_file(file)
if do_include:
profile = file
hat = None
profname = combine_profname((profile, hat))
profile_data[profname] = ProfileStorage(profile, hat, 'parse_profile_data() do_include')
if do_include or profname and profname not in profile_data:
profile_data[profname] = prof_storage
profile_data[profname]['filename'] = file
for lineno, line in enumerate(data):
line = line.strip()
lineno = 0
while lineno < len(data):
line = data[lineno].strip()
debug_lineno = src_lineno + lineno
if not line:
lineno += 1
continue
# we're dealing with a multiline statement
if lastline:
@ -1812,11 +1842,13 @@ def parse_profile_data(data, file, do_include, in_preamble):
lastline = None
# is line handled by a *Rule class?
(rule_name, rule_obj) = match_line_against_rule_classes(line, profile, file, lineno, in_preamble)
(rule_name, rule_obj) = match_line_against_rule_classes(line, profile, file, debug_lineno, in_preamble)
if rule_name:
if in_preamble:
active_profiles.add_rule(file, rule_name, rule_obj)
else:
if profname not in profile_data:
profile_data[profname] = prof_storage
profile_data[profname][rule_name].add(rule_obj)
if rule_name == 'inc_ie':
@ -1838,16 +1870,16 @@ def parse_profile_data(data, file, do_include, in_preamble):
in_preamble = False
(profile, hat, prof_storage) = ProfileStorage.parse(line, file, lineno, profile, hat)
(profile, hat, prof_storage) = ProfileStorage.parse(line, file, debug_lineno, profile, hat)
if profile == hat:
hat = None
profname = combine_profname((profile, hat))
if profile_data.get(profname, False):
print(file)
raise AppArmorException(
'Profile %(profile)s defined twice in %(file)s, last found in line %(line)s'
% {'file': file, 'line': lineno + 1, 'profile': combine_name(profile, hat)})
% {'file': file, 'line': debug_lineno + 1, 'profile': combine_name(profile, hat)})
profile_data[profname] = prof_storage
@ -1857,12 +1889,11 @@ def parse_profile_data(data, file, do_include, in_preamble):
initial_comment = ''
elif RE_PROFILE_END.search(line):
# If profile ends and we're not in one
if not profile:
raise AppArmorException(
_('Syntax Error: Unexpected End of Profile reached in file: %(file)s line: %(line)s')
% {'file': file, 'line': lineno + 1})
ret_lineno, ret_profile_data, end_of_block = parse_block(data[lineno + 1:], file, do_include, in_preamble, profname, profile, prof_storage, debug_lineno + 1, in_if)
profile_data[profname]['in_cond'] = in_if
lineno = lineno + ret_lineno
profile_data = {**profile_data, **ret_profile_data}
if in_contained_hat:
hat = None
@ -1876,32 +1907,49 @@ def parse_profile_data(data, file, do_include, in_preamble):
initial_comment = ''
elif RE_PROFILE_CONDITIONAL.search(line):
# Conditional Boolean
pass
elif RE_PROFILE_END.search(line):
# If profile ends and we're not in one
if not profile:
raise AppArmorException(
_('Syntax Error: Unexpected End of Profile reached in file: %(file)s line: %(line)s')
% {'file': file, 'line': debug_lineno + 1})
elif RE_PROFILE_CONDITIONAL_VARIABLE.search(line):
# Conditional Variable defines
pass
return lineno + 1, profile_data, True
elif RE_PROFILE_CONDITIONAL_BOOLEAN.search(line):
# Conditional Boolean defined
pass
elif RE_PROFILE_CONDITIONAL_START.search(line):
conditional_block = ConditionalBlock(line, active_profiles.files[file])
lineno, conditional_block = parse_conditional(conditional_block, profile_data, data, lineno, file, do_include, in_preamble, profname, profile, debug_lineno + 1)
elif RE_PROFILE_CONDITIONAL_ELSE.search(line):
if not in_if and conditional_block is None:
raise AppArmorException(_('Syntax Error: Unexpected else without previous if in file: %(file)s line: %(line)s')
% {'file': file, 'line': debug_lineno + 1})
matches = RE_PROFILE_CONDITIONAL_ELSE.search(line)
if matches.group('close'):
return lineno, profile_data, False # not returning next line on purpose
if conditional_block is None:
raise AppArmorException(_('Syntax Error: Unexpected else found without previous if in file: %(file)s line: %(line)s')
% {'file': file, 'line': debug_lineno + 1})
conditional_block.add_conditional(line, active_profiles.files[file])
lineno, conditional_block = parse_conditional(conditional_block, profile_data, data, lineno, file, do_include, in_preamble, profname, profile, debug_lineno + 1)
elif RE_PROFILE_CHANGE_HAT.search(line):
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
if not profile:
raise AppArmorException(_('Syntax Error: Unexpected change hat declaration found in file: %(file)s line: %(line)s')
% {'file': file, 'line': lineno + 1})
% {'file': file, 'line': debug_lineno + 1})
aaui.UI_Important(_('Ignoring no longer supported change hat declaration "^%(hat)s," found in file: %(file)s line: %(line)s')
% {'hat': matches[0], 'file': file, 'line': lineno + 1})
% {'hat': matches[0], 'file': file, 'line': debug_lineno + 1})
elif line.startswith('#'):
# Handle initial comments
if not profile:
if line.startswith('# Last Modified:'):
lineno += 1
continue
else:
initial_comment = initial_comment + line + '\n'
@ -1925,14 +1973,16 @@ def parse_profile_data(data, file, do_include, in_preamble):
else:
raise AppArmorException(
_('Syntax Error: Unknown line found in file %(file)s line %(lineno)s:\n %(line)s')
% {'file': file, 'lineno': lineno + 1, 'line': line})
% {'file': file, 'lineno': debug_lineno, 'line': line})
lineno += 1
if lastline:
# lastline gets merged into line (and reset to None) when reading the next line.
# If it isn't empty, this means there's something unparsable at the end of the profile
raise AppArmorException(
_('Syntax Error: Unknown line found in file %(file)s line %(lineno)s:\n %(line)s')
% {'file': file, 'lineno': lineno + 1, 'line': lastline})
% {'file': file, 'lineno': debug_lineno + 1, 'line': lastline})
# Below is not required I'd say
if not do_include:
@ -1952,6 +2002,30 @@ def parse_profile_data(data, file, do_include, in_preamble):
_("Syntax Error: Missing '}' or ','. Reached end of file %(file)s while inside profile %(profile)s")
% {'file': file, 'profile': profile})
return lineno, profile_data, False
def parse_profile_data(data, file, do_include, in_preamble):
profile_data = {}
profile = None
hat = None
profname = None
prof_storage = None
active_profiles.init_file(file)
if do_include:
profile = file
hat = None
profname = combine_profname((profile, hat))
prof_storage = ProfileStorage(profile, hat, 'parse_profile_data() do_include')
_lineno, profile_data, end_of_block = parse_block(data, file, do_include, in_preamble, profname, profile, prof_storage, 0)
for prof in list(profile_data): # get the keys
if profile_data[prof]['in_cond']:
profile_data.pop(prof)
return profile_data

View File

@ -35,6 +35,7 @@ from apparmor.rule.io_uring import IOUringRule, IOUringRuleset
from apparmor.rule.mount import MountRule, MountRuleset
from apparmor.rule.pivot_root import PivotRootRule, PivotRootRuleset
from apparmor.rule.unix import UnixRule, UnixRuleset
from apparmor.rule.conditional import ConditionalBlock, ConditionalBlockset
from apparmor.translations import init_translation
@ -58,6 +59,7 @@ ruletypes = {
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
'pivot_root': {'rule': PivotRootRule, 'ruleset': PivotRootRuleset},
'unix': {'rule': UnixRule, 'ruleset': UnixRuleset},
'cond_block': {'rule': ConditionalBlock, 'ruleset': ConditionalBlockset},
}
@ -88,6 +90,7 @@ class ProfileStorage:
data['profile_keyword'] = False
data['is_hat'] = False # profile or hat?
data['hat_keyword'] = False # True for 'hat foo', False for '^foo'
data['in_cond'] = False # used for profiles created inside conditionals
self.data = data
@ -178,6 +181,7 @@ class ProfileStorage:
'inc_ie',
'rlimit',
'capability',
'cond_block',
'network',
'dbus',
'mount',

View File

@ -28,7 +28,7 @@ RE_COMMA_EOL = r'\s*,' + RE_EOL # optional whitespace, comma + RE_EOL
RE_PROFILE_NAME = r'(?P<%s>(\S+|"[^"]+"))' # string without spaces, or quoted string. %s is the match group name
RE_PATH = r'/\S*|"/[^"]*"' # filename (starting with '/') without spaces, or quoted filename.
RE_VAR = r'@{[^}\s]+}'
RE_VAR = r'@{?[^}\s]+}?'
RE_DICT_ENTRY = r'\s*(?P<key>[^,\s=]+)(?:=(?P<value>[^,\s=]+))?\s*'
RE_PROFILE_PATH = '(?P<%s>(' + RE_PATH + '))' # quoted or unquoted filename. %s is the match group name
RE_PROFILE_PATH_OR_VAR = '(?P<%s>(' + RE_PATH + '|' + RE_VAR + r'\S*|"' + RE_VAR + '[^"]*"))' # quoted or unquoted filename or variable. %s is the match group name
@ -37,6 +37,10 @@ RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)*)\)\s*)?'
RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?'
RE_VARIABLE = re.compile(RE_VAR)
RE_ID = r'(?P<id%(label)s>[^,!#\s=@$()"]+|"(\w|\s)*")'
RE_VARIABLES = r'(?P<var%(label)s>(?P<var_type%(label)s>@|\$)\{?(?P<varname%(label)s>\w+)\}?)'
RE_ID_OR_VAR = r'(' + RE_VARIABLES + r'|' + RE_ID + r')'
RE_ALL_VARIABLES = re.compile(RE_VARIABLES % {'label': ''})
RE_PROFILE_END = re.compile(r'^\s*\}' + RE_EOL)
RE_PROFILE_ALL = re.compile(RE_PRIORITY_AUDIT_DENY + r'all' + RE_COMMA_EOL)
@ -45,9 +49,21 @@ RE_PROFILE_ALIAS = re.compile(r'^\s*alias\s+(?P<orig_path>"??.+?"??)\s+->\s*(?P<
RE_PROFILE_RLIMIT = re.compile(r'^\s*set\s+rlimit\s+(?P<rlimit>[a-z]+)\s*<=\s*(?P<value>[^ ]+(\s+[a-zA-Z]+)?)' + RE_COMMA_EOL)
RE_PROFILE_BOOLEAN = re.compile(r'^\s*(?P<varname>\$\{?\w*\}?)\s*=\s*(?P<value>true|false)\s*,?' + RE_EOL, flags=re.IGNORECASE)
RE_PROFILE_VARIABLE = re.compile(r'^\s*(?P<varname>@\{?\w+\}?)\s*(?P<mode>\+?=)\s*(?P<values>@*.+?)' + RE_EOL)
RE_PROFILE_CONDITIONAL = re.compile(r'^\s*if\s+(not\s+)?(\$\{?\w*\}?)\s*\{' + RE_EOL)
RE_PROFILE_CONDITIONAL_VARIABLE = re.compile(r'^\s*if\s+(not\s+)?defined\s+(@\{?\w+\}?)\s*\{\s*(#.*)?$')
RE_PROFILE_CONDITIONAL_BOOLEAN = re.compile(r'^\s*if\s+(not\s+)?defined\s+(\$\{?\w+\}?)\s*\{\s*(#.*)?$')
RE_BOOLEAN_OP = r'(?P<boolean_op%(term)s>(?P<boolean_not%(term)s>(not\s+)*)(?P<defined%(term)s>defined\s+)?' + RE_VARIABLES % {'label': '%(term)s'} + r')'
RE_COMPARE_OP_QUOTED = r'(?P<compare_op%(term)s>(?P<compare_not%(term)s>(not\s+)*)(?P<left%(term)s>"?' + RE_ID_OR_VAR % {'label': '_left%(term)s'} + r'"?)\s+(?P<op%(term)s>==|!=|in|>|>=|<|<=)\s+(?P<right%(term)s>"?' + RE_ID_OR_VAR % {'label': '_right%(term)s'} + r'"?))' # used only by transform_cond
RE_COMPARE_OP = RE_COMPARE_OP_QUOTED.replace('"?', '')
RE_FACTOR = r'(?P<open_paren%(term)s>\()?(' + RE_COMPARE_OP + r'|' + RE_BOOLEAN_OP + r')(?P<close_paren%(term)s>\))?'
RE_TERM = r'(?P<open_paren%(expr)s>\()*\s*((?P<one%(expr)s>' + RE_FACTOR % {'term': '_1%(expr)s'} + r')\s+(?P<cond_op%(expr)s>and|or)\s+(?P<two%(expr)s>' + RE_FACTOR % {'term': '_2%(expr)s'} + ')|' + RE_FACTOR % {'term': '_0%(expr)s'} + r')\s*(?P<close_paren%(expr)s>\))*'
RE_CONDITION = r'(?P<expr>(?P<open_paren>\()?(?P<first>' + RE_TERM % {'expr': '_first'} + r')(\s+(?P<cond_op>and|or)\s+(?P<second>' + RE_TERM % {'expr': '_second'} + r'))*(?P<close_paren>\))?)'
RE_PROFILE_CONDITIONAL = r'\s*if\s+' + RE_CONDITION + r'\s*\{'
RE_PROFILE_CONDITIONAL_START = re.compile(r'^' + RE_PROFILE_CONDITIONAL + RE_EOL)
RE_PROFILE_CONDITIONAL_ELSE = re.compile(r'^\s*(?P<close>\})?\s*else((?P<if>\s+' + RE_PROFILE_CONDITIONAL + r')|(\s*\{))' + RE_EOL)
RE_PROFILE_NETWORK = re.compile(RE_PRIORITY_AUDIT_DENY + r'network(?P<details>\s+.*)?' + RE_COMMA_EOL)
RE_PROFILE_CHANGE_HAT = re.compile(r'^\s*\^("??.+?"??)' + RE_COMMA_EOL)
RE_PROFILE_HAT_DEF = re.compile(r'^(?P<leadingspace>\s*)(?P<hat_keyword>\^|hat\s+)(?P<hat>"??[^)]+?"??)' + RE_FLAGS + r'\s*\{' + RE_EOL)

View File

@ -51,7 +51,7 @@ class BooleanRule(BaseRule):
raise AppArmorException('Passed invalid value to %s: %s' % (self.__class__.__name__, value))
self.varname = varname
self.value = value
self.value = value == 'true'
@classmethod
def _create_instance(cls, raw_rule, matches):
@ -70,7 +70,7 @@ class BooleanRule(BaseRule):
space = ' ' * depth
return '%s%s = %s' % (space, self.varname, self.value)
return '%s%s = %s' % (space, self.varname, str(self.value).lower())
def _is_covered_localvars(self, other_rule):
"""check if other_rule is covered by this rule object"""
@ -78,7 +78,10 @@ class BooleanRule(BaseRule):
if self.varname != other_rule.varname:
return False
if not self._is_covered_list(self.value, None, set(other_rule.value), None, 'value'):
if type(self.value) is not bool or type(other_rule.value) is not bool:
raise AppArmorBug(_('Invalid value for boolean variable'))
if self.value != other_rule.value:
return False
# still here? -> then it is covered

View File

@ -0,0 +1,534 @@
#
# ----------------------------------------------------------------------
# Copyright (C) 2025 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 ast
import re
from apparmor.common import AppArmorException, AppArmorBug
from apparmor.regex import strip_quotes, RE_PROFILE_CONDITIONAL_START, RE_PROFILE_CONDITIONAL_ELSE, RE_ALL_VARIABLES, RE_BOOLEAN_OP, RE_ID_OR_VAR, RE_CONDITION, RE_COMPARE_OP_QUOTED
from apparmor.rule import BaseRule, BaseRuleset, parse_comment, quote_if_needed
from apparmor.translations import init_translation
_ = init_translation()
class ConditionalBlock(BaseRule):
"""Class to handle and store a conditional block containing if,
and optionally, else ifs and else"""
_match_re = None
result = False
def __init__(self, raw_rule, prof_storage, audit=False, deny=False, allow_keyword=False,
comment='', log_event=None, priority=None):
super().__init__(audit=audit, deny=deny, allow_keyword=allow_keyword,
comment=comment, log_event=log_event, priority=priority)
# conditional blocks don't support priority, allow keyword, audit or deny - yet
self.ensure_modifiers_not_supported()
self.cond_list = []
conditional = ConditionalStart.create_instance(raw_rule)
self.result = conditional.evaluate(prof_storage)
self.cond_list.append(conditional)
def store_profile_data(self, profile_data):
self.cond_list[-1].profile_data = profile_data
def add_conditional(self, raw_rule, prof_storage):
conditional = ConditionalElse.create_instance(raw_rule)
result = conditional.evaluate(prof_storage)
self.cond_list.append(conditional)
if self.result:
self.result = False
else:
self.result = result
def get_clean(self, depth=0):
clean = ''
for cond in self.cond_list:
if clean:
clean += ' '
clean += cond.get_clean(depth)
return clean
@classmethod
def _create_instance(cls, raw_rule, matches):
raise NotImplementedError("'%s' is not supposed to be called directly" % (str(cls)))
def _is_covered_localvars(self, other_rule):
"""check if other_rule is covered by this rule object"""
if len(self.cond_list) != len(other_rule.cond_list):
return False
else:
for idx in range(len(self.cond_list)):
if not self.cond_list[idx].is_covered(other_rule.cond_list[idx]):
return False
return True
def _is_equal_localvars(self, other_rule, strict):
"""compare if rule-specific conditionals are equal"""
if len(self.cond_list) != len(other_rule.cond_list):
return False
else:
for idx in range(len(self.cond_list)):
if not self.cond_list[idx].is_equal(other_rule.cond_list[idx]):
return False
return True
def _logprof_header_localvars(self):
conditions = []
for cond in self.cond_list:
conditions.append(cond.logprof_header())
return _('ConditionalBlock'), (conditions)
class ConditionalRule(BaseRule):
"""Class to handle and store a single conditional rule"""
IF = 1
ELSEIF = 2
ELSE = 3
rule_name = 'conditional'
_match_re = re.compile(RE_CONDITION)
def __init__(self, condition, ast_tree,
audit=False, deny=False, allow_keyword=False,
comment='', log_event=None, priority=None):
super().__init__(audit=audit, deny=deny, allow_keyword=allow_keyword,
comment=comment, log_event=log_event, priority=priority)
# conditionals don't support priority, allow keyword, audit or deny
self.ensure_modifiers_not_supported()
if condition not in [self.IF, self.ELSEIF, self.ELSE]:
raise AppArmorBug('Passed invalid condition to %s: %s' % (self.__class__.__name__, condition))
if ast_tree is not None and not isinstance(ast_tree, AppArmorAst):
raise AppArmorBug('Passed invalid AST tree type to %s: %s' % (self.__class__.__name__, type(ast_tree)))
self.condition = condition
self.ast_tree = ast_tree
@classmethod
def _create_instance(cls, raw_rule, matches):
"""parse raw_rule and return instance of this class"""
if cls == ConditionalStart:
conditional = ConditionalRule.IF
else:
if matches.group('if'):
conditional = ConditionalRule.ELSEIF
else:
conditional = ConditionalRule.ELSE
comment = parse_comment(matches)
expr = cls._match_re.search(raw_rule)
ast_tree = None
if conditional != ConditionalRule.ELSE:
ast_tree = AppArmorAst(expr.group('expr'))
return cls(conditional, ast_tree, comment)
def evaluate(self, prof_storage):
# else should always evaluate to true, since that's the
# default if all previous "ifs" evaluated to false
if self.condition == ConditionalRule.ELSE:
return True
return self.ast_tree.evaluate(prof_storage)
def get_clean(self, depth=0):
"""return rule (in clean/default formatting)"""
space = ' ' * depth
leading_space = ''
if self.condition == ConditionalRule.IF:
conditional = 'if '
leading_space = space
elif self.condition == ConditionalRule.ELSEIF:
conditional = 'else if '
elif self.condition == ConditionalRule.ELSE:
conditional = 'else '
else:
raise AppArmorBug('Invalid condition type in %s' % (self.__class__.__name__))
expr = ''
if self.ast_tree:
expr = self.ast_tree.get_clean()
data = []
data.append('%s%s%s{' % (leading_space, conditional, expr))
for profname in self.profile_data:
if self.profile_data[profname]['in_cond']:
from apparmor.aa import write_piece
data.extend(write_piece(self.profile_data, depth + 1, profname, profname))
else:
data += self.profile_data[profname].get_rules_clean(depth + 1)
data.append('%s}' % space)
return '\n'.join(data)
def _is_covered_localvars(self, other_rule):
"""check if other_rule is covered by this rule object"""
# conditional is only covered if equal
if self.is_equal(other_rule):
return True
return False
def _is_equal_localvars(self, rule_obj, strict):
"""compare if rule-specific conditionals are equal"""
if self.condition != rule_obj.condition:
return False
if self.profile_data != rule_obj.profile_data:
return False
return self.ast_tree.is_equal(rule_obj.ast_tree)
def _logprof_header_localvars(self):
return _('Conditional'), self.get_clean()
class ConditionalStart(ConditionalRule):
"""Class to handle and store a single conditional rule"""
_match_re = RE_PROFILE_CONDITIONAL_START
class ConditionalElse(ConditionalRule):
"""Class to handle and store a single conditional rule"""
_match_re = RE_PROFILE_CONDITIONAL_ELSE
class ConditionalBlockset(BaseRuleset):
"""Class to handle and store a collection of conditional rule blocks"""
class AppArmorAst():
astcomp_to_string = {
ast.Eq: '==',
ast.NotEq: '!=',
ast.Lt: '<',
ast.LtE: '<=',
ast.Gt: '>',
ast.GtE: '>=',
ast.In: 'in',
}
def __init__(self, expr):
self.tree = ast.parse(self.transform_cond(expr))
def get_clean(self):
noop, expr = self.get_clean_tree(self.tree.body[0])
expr += ' '
return expr
def get_clean_tree(self, node):
if isinstance(node, ast.Expr):
node = node.value
if isinstance(node, ast.BoolOp):
op = ''
clean = ''
if isinstance(node.op, ast.And):
op = 'and'
else:
op = 'or'
for value in node.values:
if clean:
clean += ' ' + op + ' '
ret_op, ret_clean = self.get_clean_tree(value)
if ret_op == 'or' and op == 'and':
ret_clean = '(' + ret_clean + ')'
clean += ret_clean
return op, clean
elif isinstance(node, ast.UnaryOp):
if not isinstance(node.op, ast.Not):
raise AppArmorBug('Invalid unary operation in %s' % (self.__class__.__name__))
op, child = self.get_clean_tree(node.operand)
if op == 'not': # remove canceling nots
return 'val', '%s' % (child[len('not '):])
return 'not', 'not %s' % (child)
elif isinstance(node, ast.Constant):
val = quote_if_needed(strip_quotes(node.value)) # strip first because it might it can be quoted but not need it
term = Term.create_instance(val)
return 'val', str(term)
elif isinstance(node, ast.Name):
return 'func', node.id
elif isinstance(node, ast.Call):
noop, name = self.get_clean_tree(node.func)
if name != 'defined':
raise AppArmorBug('Invalid function name in %s' % (self.__class__.__name__))
noop, var = self.get_clean_tree(node.args[0])
return 'defined', '%s %s' % (name, var)
elif isinstance(node, ast.Compare):
noop, left = self.get_clean_tree(node.left)
noop, right = self.get_clean_tree(node.comparators[0])
op = self.astcomp_to_string[type(node.ops[0])]
return 'cmp', '%s %s %s' % (left, op, right)
else:
raise AppArmorBug('Unsupported node type in %s' % (self.__class__.__name__))
def evaluate(self, prof_storage):
noop, result = self.evaluate_tree(self.tree.body[0], prof_storage, True)
return result
def evaluate_tree(self, node, prof_storage, resolve=False):
result = None
if isinstance(node, ast.Expr):
node = node.value
if isinstance(node, ast.BoolOp):
op = node.op
for value in node.values:
ret_op, ret_result = self.evaluate_tree(value, prof_storage, True)
if result is None:
result = ret_result
else:
if isinstance(op, ast.And):
result = result and ret_result
else:
result = result or ret_result
return op, result
elif isinstance(node, ast.UnaryOp):
if not isinstance(node.op, ast.Not):
raise AppArmorBug('Invalid unary operation in %s' % (self.__class__.__name__))
result = not self.evaluate_tree(node.operand, prof_storage, True)
return node.op, result
elif isinstance(node, ast.Constant):
val = quote_if_needed(strip_quotes(node.value)) # strip first because it might it can be quoted but not need it
term = Term.create_instance(val)
if resolve:
cond = BooleanCondition('', term)
result = cond.evaluate(prof_storage)
return term, result
elif isinstance(node, ast.Name):
return node.id, result
elif isinstance(node, ast.Call):
func, noop = self.evaluate_tree(node.func, prof_storage)
if func != 'defined':
raise AppArmorBug('Invalid function name in %s' % (self.__class__.__name__))
# there should be only one arg
variable, noop = self.evaluate_tree(node.args[0], prof_storage)
cond = BooleanCondition(func, variable)
return 'defined', cond.evaluate(prof_storage)
elif isinstance(node, ast.Compare):
left, noop = self.evaluate_tree(node.left, prof_storage)
right, noop = self.evaluate_tree(node.comparators[0], prof_storage)
op = self.astcomp_to_string[type(node.ops[0])]
cond = CompareCondition(left, op, right)
return 'cmp', cond.evaluate(prof_storage)
else:
raise AppArmorBug('Unsupported node type in %s' % (self.__class__.__name__))
def compare_ast(self, node1, node2):
if type(node1) is not type(node2):
return False
if isinstance(node1, ast.AST):
for k, v in vars(node1).items():
if k in ('lineno', 'col_offset', 'ctx', 'end_lineno', 'end_col_offset'):
continue
if not self.compare_ast(v, getattr(node2, k)):
return False
return True
elif isinstance(node1, list):
if len(node1) != len(node2):
return False
for i in range(len(node1)):
if not self.compare_ast(node1[i], node2[i]):
return False
return True
else:
return node1 == node2
def is_equal(self, other_tree):
return self.compare_ast(self.tree, other_tree.tree)
def transform_cond(self, text):
"""Used to transform policy conditional into Python format, so ast can be used"""
def boolean_op(match):
not_op = match.group('boolean_not')
defined = match.group('defined')
var = match.group('var')
var = '"%s"' % (var)
if defined:
var = 'defined(%s)' % (var)
return '%s%s' % (not_op, var)
def compare_op(match):
not_op = match.group('compare_not')
left = match.group('left')
op = match.group('op')
right = match.group('right')
if match.group('id_left') and not (left.startswith('"') or left.endswith('"')):
left = '"%s"' % (left)
if match.group('id_right') and not (right.startswith('"') or right.endswith('"')):
right = '"%s"' % (right)
return '%s%s %s %s' % (not_op, left, op, right)
replaced = re.sub(RE_BOOLEAN_OP % {'term': ''}, boolean_op, text)
replaced = re.sub(RE_COMPARE_OP_QUOTED % {'term': ''}, compare_op, replaced)
return replaced
class Term():
match_re = re.compile(RE_ID_OR_VAR % {'label': ''})
@classmethod
def create_instance(cls, raw_term):
"""parse raw_term and return instance of this class"""
matches = cls.match_re.search(raw_term)
if not matches:
raise AppArmorBug('Unable to parse term in %s' % (cls.__class__.__name__))
if matches.group('id'):
return Id(matches.group('id'))
else:
var_type = matches.group('var_type')
varname = matches.group('varname')
var = matches.group('var')
return Variable(var_type, varname, var)
class Variable(Term):
def __init__(self, var_type, varname, var):
self.varname = varname
self.var = var
if var_type == '$':
self.var_type = 'boolean'
else:
self.var_type = 'variable'
def get_variable_rule(self, prof_storage):
for rule in prof_storage[self.var_type].rules:
filtered = RE_ALL_VARIABLES.search(rule.varname)
if self.varname == filtered.group('varname'):
return rule
return None
def get_set(self, prof_storage):
variable_rule = self.get_variable_rule(prof_storage)
if variable_rule is None:
raise AppArmorException(_('Error retrieving variable %(var)s') % {'var': self.var})
return variable_rule.values
def __repr__(self):
return self.var
class Id(Term):
def __init__(self, value):
self.value = value
def get_set(self, prof_storage):
return {self.value}
def __repr__(self):
return self.value
class BooleanCondition():
def __init__(self, defined: str, variable: Term):
if not isinstance(defined, str):
raise AppArmorBug('Passed invalid defined value to %s: %s' % (self.__class__.__name__, defined))
if not isinstance(variable, Term):
raise AppArmorBug('Passed invalid variable type to %s: %s' % (self.__class__.__name__, type(variable)))
self.defined = defined
self.variable = variable
def evaluate(self, prof_storage):
matched = self.variable.get_variable_rule(prof_storage)
if not self.defined: # boolean op
if self.variable.var_type == 'boolean':
if matched:
return matched.value
else:
raise AppArmorException(_('Cannot find previous declaration of %(var)s') % {'var': self.variable})
else:
raise AppArmorException(_('Unexpected variable in boolean operation: %(var)s') % {'var': self.variable})
else:
return bool(matched)
class CompareCondition():
valid_ops = ['==', '!=', 'in', '>', '>=', '<', '<=']
def __init__(self, left_term: Term, op: str, right_term: Term):
if op not in self.valid_ops:
raise AppArmorBug('Passed invalid op value to %s: %s' % (self.__class__.__name__, op))
if not isinstance(left_term, Term):
raise AppArmorBug('Passed invalid left term type to %s: %s' % (self.__class__.__name__, type(left_term)))
if not isinstance(right_term, Term):
raise AppArmorBug('Passed invalid right term type to %s: %s' % (self.__class__.__name__, type(right_term)))
self.left_term = left_term
self.op = op
self.right_term = right_term
def compare(self, op, lhs, rhs):
if type(lhs) is not type(rhs):
raise AppArmorBug('Trying to compare elements of different types in %s' % (self.__class__.__name__))
if (op == '>'):
return lhs > rhs
elif (op == '>='):
return lhs >= rhs
elif (op == '<'):
return lhs < rhs
elif (op == '<='):
return lhs <= rhs
else:
raise AppArmorBug('Invalid op in %s: %s' % (self.__class__.__name__, op))
def evaluate(self, prof_storage):
lhs = self.left_term.get_set(prof_storage)
rhs = self.right_term.get_set(prof_storage)
if not isinstance(lhs, set) or not isinstance(rhs, set):
raise AppArmorBug('Passed invalid type for condition term in %s' % (self.__class__.__name__))
converted_lhs = None
converted_rhs = None
if self.op == 'in':
return lhs.issubset(rhs)
elif self.op == '==':
return lhs == rhs
elif self.op == '!=':
return lhs != rhs
try:
if len(lhs) == 1:
converted_lhs = int(next(iter(lhs)))
except ValueError:
pass
try:
if len(rhs) == 1:
converted_rhs = int(next(iter(rhs)))
except ValueError:
pass
if converted_lhs is None and converted_rhs is None: # sets
return self.compare(self.op, lhs, rhs)
elif converted_lhs is not None and converted_rhs is not None: # numbers
return self.compare(self.op, converted_lhs, converted_rhs)
else:
raise AppArmorException(_('Can only compare numbers with numbers'))

View File

@ -40,10 +40,10 @@ class VariableRule(BaseRule):
if not isinstance(varname, str):
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 varname.startswith('@'):
raise AppArmorException("Passed invalid varname to %s (doesn't start with '@'): %s" % (self.__class__.__name__, varname))
if (varname.startswith('@{') and not varname.endswith('}')) or (not varname.startswith('@{') and varname.endswith('}')):
raise AppArmorException("Passed invalid varname to %s (mismatched braces): %s" % (self.__class__.__name__, varname))
if not isinstance(mode, str):
raise AppArmorBug('Passed unknown type for variable assignment mode to %s: %s' % (self.__class__.__name__, mode))

View File

@ -107,3 +107,20 @@ profile foo//bar {
/usr/bin/a/simple/cleanprof/test/profile//child {
/home/test-child r,
}
profile uniquename /attach {
if not not
not not
not not defined $foo {
/bin/true rix,
} else if x in @xy {
if @{asdf} > " a test" {
/greaterthan r,
} else {
file,
}
/bin/false rix,
} else {
/dev/null r,
}
}

View File

@ -107,3 +107,20 @@ profile foo//bar {
/usr/bin/a/simple/cleanprof/test/profile//child {
/home/test-child r,
}
profile uniquename /attach {
if not not
not not
not not defined $foo {
/bin/true rix,
} else if x in @xy {
if @{asdf} > " a test" {
/greaterthan r,
} else {
file,
}
/bin/false rix,
} else {
/dev/null r,
}
}

View File

@ -86,3 +86,24 @@ profile foo//bar {
/home/namedchild r,
}
profile uniquename /attach {
if defined $foo {
/bin/true rix,
} else if x in @xy {
if @{asdf} > " a test" {
/greaterthan r,
} else {
file,
}
/bin/false rix,
} else {
/dev/null r,
}
}

View File

@ -44,12 +44,12 @@ class BooleanTest(AATest):
class BooleanTestParse(BooleanTest):
tests = (
# rawrule comment varname value
('$foo=true', exp('', '$foo', 'true')),
('$foo = false', exp('', '$foo', 'false')),
('$foo=TrUe', exp('', '$foo', 'true')),
('$foo = FaLsE', exp('', '$foo', 'false')),
(' $foo = true ', exp('', '$foo', 'true')),
(' $foo = true # comment', exp(' # comment', '$foo', 'true')),
('$foo=true', exp('', '$foo', True)),
('$foo = false', exp('', '$foo', False)),
('$foo=TrUe', exp('', '$foo', True)),
('$foo = FaLsE', exp('', '$foo', False)),
(' $foo = true ', exp('', '$foo', True)),
(' $foo = true # comment', exp(' # comment', '$foo', True)),
)
def _run_test(self, rawrule, expected):

View File

@ -0,0 +1,402 @@
#!/usr/bin/python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 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 apparmor.common import AppArmorException, AppArmorBug, combine_profname
from apparmor.profile_storage import ProfileStorage
from apparmor.profile_list import ProfileList
from apparmor.rule.file import FileRule
from apparmor.rule.variable import VariableRule
from apparmor.rule.boolean import BooleanRule
from apparmor.rule.conditional import ConditionalBlock, AppArmorAst, Term, CompareCondition, ConditionalRule, BooleanCondition
from common_test import AATest, setup_all_loops
class TestConditional(AATest):
""" Base class to test conditionals, profile_data does not contain any rules """
filename = 'somefile'
condition_contents = '\n'
tests = (
# ConditionalBlock clean rule
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if defined $FOO {', '} else if not defined $BAR {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
)
@classmethod
def setUpClass(cls):
super(TestConditional, cls).setUpClass()
cls.active_profiles = ProfileList()
cls.profile_data = {}
cls.condition_contents = '\n'
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR2} = "test1 test2"'))
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR3} = 10'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
def _run_test(self, params, expected):
conditional_block = None
for condition in params:
if condition == params[0]:
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
else:
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
self.assertEqual(conditional_block.get_clean(), expected)
class TestConditionalComplex(TestConditional):
""" Class to test complex conditionals (and, ors, comparisons and boolean operations), profile_data contains one file rule """
# directly related to how setUpClass sets up profile_data
condition_contents = '\n /bin/false rix,\n\n'
tests = (
# ConditionalBlock clean rule
(['if $FOO and ($BAR) {', '} else if $BAR {', '} else {'], 'if $FOO and $BAR {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if $FOO and ($BAR or defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO and ($BAR or defined @{VAR1}) {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if $FOO or ($BAR or defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO or $BAR or defined @{VAR1} {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if $FOO or ($BAR and defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO or $BAR and defined @{VAR1} {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not $FOO {', '} else if not not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if ((test in @{VAR1})) and defined $FOO {'], 'if test in @{VAR1} and defined $FOO {' + condition_contents + '}'),
(['if test3 in @{VAR2} {'], 'if test3 in @{VAR2} {' + condition_contents + '}'),
(['if 9 < @{VAR3} {'], 'if 9 < @{VAR3} {' + condition_contents + '}'),
(['if test in "test2 test" {'], 'if test in "test2 test" {' + condition_contents + '}'),
)
@classmethod
def setUpClass(cls):
super(TestConditionalComplex, cls).setUpClass()
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
class TestConditionalSimple(TestConditional):
""" Class to test simple conditionals (boolean and defined operations), profile_data contains one file rule """
# directly related to how setUpClass sets up profile_data
condition_contents = '\n /bin/false rix,\n\n'
tests = (
# ConditionalBlock clean rule
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if defined $FOO {', '} else if not defined ${BAR} {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined ${BAR} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
)
@classmethod
def setUpClass(cls):
super(TestConditionalSimple, cls).setUpClass()
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
class TestConditionalSubProfile(TestConditional):
""" Class to test simple conditionals, profile_data contains hat definition """
# directly related to how setUpClass sets up profile_data
condition_contents = '\n /** w,\n /bin/false rix,\n\n ^bar {\n /bin/true rix,\n\n }\n'
tests = (
# ConditionalBlock clean rule
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
(['if defined $FOO {', '} else if not defined ${BAR} {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined ${BAR} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
)
@classmethod
def setUpClass(cls):
super(TestConditionalSubProfile, cls).setUpClass()
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
profname = combine_profname((profile, hat))
cls.profile_data[profname] = prof_storage
cls.profile_data[profname]['in_cond'] = True
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
class TestConditionalLogprof(TestConditional):
""" Class to test output of Logprof """
# directly related to how setUpClass sets up profile_data
condition_contents = '\n /** w,\n /bin/false rix,\n\n ^bar {\n /bin/true rix,\n\n }\n'
tests = (
(['if $FOO {', '} else if $BAR {', '} else {'],
['ConditionalBlock', [['Conditional', 'if $FOO {' + condition_contents + '}'],
['Conditional', 'else if $BAR {' + condition_contents + '}'],
['Conditional', 'else {' + condition_contents + '}']]]),
(['if not $FOO {', '} else if not $BAR {', '} else {'],
['ConditionalBlock', [['Conditional', 'if not $FOO {' + condition_contents + '}'],
['Conditional', 'else if not $BAR {' + condition_contents + '}'],
['Conditional', 'else {' + condition_contents + '}']]]),
(['if defined $FOO {', '} else if not defined $BAR {', '} else {'],
['ConditionalBlock', [['Conditional', 'if defined $FOO {' + condition_contents + '}'],
['Conditional', 'else if not defined $BAR {' + condition_contents + '}'],
['Conditional', 'else {' + condition_contents + '}']]]),
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'],
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}'],
['Conditional', 'else if defined @{VAR1} {' + condition_contents + '}'],
['Conditional', 'else {' + condition_contents + '}']]]),
(['if not defined @{VAR2} {', '} else {'],
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}'],
['Conditional', 'else {' + condition_contents + '}']]]),
(['if not defined @{VAR2} {'],
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}']]]),
)
@classmethod
def setUpClass(cls):
super(TestConditionalLogprof, cls).setUpClass()
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
profname = combine_profname((profile, hat))
cls.profile_data[profname] = prof_storage
cls.profile_data[profname]['in_cond'] = True
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
def _run_test(self, params, expected):
conditional_block = None
for condition in params:
if condition == params[0]:
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
else:
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
self.assertEqual(conditional_block.logprof_header(), expected)
class TestConditionalEquality(AATest):
""" Class to test equality tests """
filename = 'somefile'
profile_data = {}
tests = (
# should always be different
# first condition second condition
((['if not $FOO {'], profile_data), (['if $FOO {'], profile_data)),
((['if defined $FOO {'], profile_data), (['if $FOO {'], profile_data)),
((['if $BAR {'], profile_data), (['if $FOO {'], profile_data)),
((['if $FOO {'], None), (['if $FOO {'], profile_data)),
((['if $FOO {', '} else if $BAR {'], profile_data), (['if $FOO {', '} else {'], profile_data)),
((['if $FOO and $BAR {'], profile_data), (['if $FOO and $BAR and defined $BAZ{'], profile_data)),
)
@classmethod
def setUpClass(cls):
super(TestConditionalEquality, cls).setUpClass()
cls.active_profiles = ProfileList()
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
profname = combine_profname((profile, hat))
cls.profile_data[profname] = prof_storage
cls.profile_data[profname]['in_cond'] = True
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
def init_cond(self, conds, profile_data):
block = None
for cond in conds:
if cond == conds[0]:
block = ConditionalBlock(cond, self.active_profiles.files[self.filename])
block.store_profile_data(profile_data)
else:
block.add_conditional(cond, self.active_profiles.files[self.filename])
block.store_profile_data(profile_data)
return block
def _run_test(self, cond1, cond2):
conditional_block1 = self.init_cond(cond1[0], cond1[1])
conditional_block2 = self.init_cond(cond2[0], cond2[1])
self.assertFalse(conditional_block1.is_equal(conditional_block2))
self.assertFalse(conditional_block1.is_covered(conditional_block2))
def test_is_equal_condition(self):
conditional_block1 = self.init_cond(['if $FOO {', '} else if $BAR {', '} else {'], self.profile_data)
conditional_block2 = self.init_cond(['if $FOO {', '} else {'], self.profile_data)
self.assertFalse(conditional_block1.is_equal(conditional_block2))
self.assertFalse(conditional_block1.is_covered(conditional_block2))
def test_is_equal(self):
conditional_block1 = ConditionalBlock('if not defined $FOO {', self.active_profiles.files[self.filename])
conditional_block1.store_profile_data(self.profile_data)
self.assertTrue(conditional_block1.is_equal(conditional_block1))
self.assertTrue(conditional_block1.is_covered(conditional_block1))
class TestConditionalException(AATest):
filename = 'somefile'
profile_data = {}
tests = (
(['if @{VAR1} {', '} else if $BAR {', '} else {'], AppArmorException), # set variable used in boolean comparison
(['if $FOO {', '} else if @{VAR} {', '} else {'], AppArmorException), # undefined variable in else if
(['if ${BAZ} {', '} else {'], AppArmorException), # undefined boolean variable with else
(['if ${VAR} {'], AppArmorException), # undefined boolean variable
(['if foo in @{UNDERFINED} {'], AppArmorException), # undefined set variable
)
@classmethod
def setUpClass(cls):
super(TestConditionalException, cls).setUpClass()
cls.active_profiles = ProfileList()
cls.condition_contents = '\n'
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
cls.profile_data[profile] = prof_storage
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
def _run_test(self, params, expected):
conditional_block = None
with self.assertRaises(expected):
for condition in params:
if condition == params[0]:
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
else:
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
conditional_block.store_profile_data(self.profile_data)
def test_create_instance_block1(self):
with self.assertRaises(NotImplementedError):
ConditionalBlock.create_instance('test')
def test_create_instance_block2(self):
with self.assertRaises(NotImplementedError):
ConditionalBlock._create_instance('test', None)
class TestAppArmorAstException(AATest):
tests = (
('foo = "1"', AppArmorBug), # AppArmorAst only supports the actual condition
('~ $FOO', AppArmorBug), # Invalid unary operator
('func(${BAZ})', AppArmorBug), # Invalid function name
)
def _run_test(self, params, expected):
ast = AppArmorAst(params)
with self.assertRaises(expected):
ast.get_clean()
with self.assertRaises(expected):
ast.evaluate(None)
class TestMiscClassException(AATest):
def test_invalid_op_1(self):
term = Term.create_instance('10')
op = 'is'
with self.assertRaises(AppArmorBug):
CompareCondition(term, op, term)
def test_invalid_op_2(self):
term = Term.create_instance('10')
cond = CompareCondition(term, 'in', term)
cond.op = 'is'
with self.assertRaises(AppArmorBug):
cond.evaluate(None)
def test_invalid_term(self):
with self.assertRaises(AppArmorBug):
Term.create_instance('#')
def test_invalid_conditional_condition(self):
with self.assertRaises(AppArmorBug):
ConditionalRule('invalid', AppArmorAst('defined $BAR'))
def test_invalid_conditional_ast_tree(self):
with self.assertRaises(AppArmorBug):
ConditionalRule(ConditionalRule.IF, 'defined $BAR')
def test_invalid_conditional_clean(self):
cond = ConditionalRule(ConditionalRule.IF, AppArmorAst('defined $BAR'))
cond.condition = 'invalid'
with self.assertRaises(AppArmorBug):
cond.get_clean()
def test_invalid_boolean_defined(self):
with self.assertRaises(AppArmorBug):
BooleanCondition(None, Term.create_instance('10'))
def test_invalid_boolean_varible(self):
with self.assertRaises(AppArmorBug):
BooleanCondition('defined', '10')
def test_invalid_compare_left(self):
with self.assertRaises(AppArmorBug):
CompareCondition(None, '==', Term.create_instance('10'))
def test_invalid_compare_right(self):
with self.assertRaises(AppArmorBug):
CompareCondition(Term.create_instance('10'), '==', None)
def test_invalid_compare_op(self):
with self.assertRaises(AppArmorBug):
CompareCondition(Term.create_instance('10'), '~', Term.create_instance('10'))
def test_invalid_compare(self):
term = Term.create_instance('10')
cond = CompareCondition(term, '==', term)
with self.assertRaises(AppArmorBug):
cond.compare(cond.op, term.get_set(None), 10)
def test_invalid_compare_evaluate(self):
term = InvalidTerm(10)
cond = CompareCondition(term, '==', term)
with self.assertRaises(AppArmorBug):
cond.evaluate(None)
class InvalidTerm(Term):
def __init__(self, value):
self.value = value
def get_set(self, prof_storage):
return self.value # not a set
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=1)

View File

@ -340,27 +340,6 @@ syntax_failure = (
# missing profile keywords
'profile/re_named_ok2.sd',
# Syntax Errors caused by boolean conditions (parse_profile_data() gets confused by the closing '}')
'conditional/defined_1.sd',
'conditional/defined_2.sd',
'conditional/else_1.sd',
'conditional/else_2.sd',
'conditional/else_3.sd',
'conditional/else_if_1.sd',
'conditional/else_if_2.sd',
'conditional/else_if_3.sd',
'conditional/else_if_5.sd',
'conditional/ok_1.sd',
'conditional/ok_2.sd',
'conditional/ok_3.sd',
'conditional/ok_4.sd',
'conditional/ok_5.sd',
'conditional/ok_6.sd',
'conditional/ok_7.sd',
'conditional/ok_8.sd',
'conditional/ok_9.sd',
'conditional/stress_1.sd',
# unexpected uppercase vs. lowercase in *x rules
'file/ok_5.sd', # Invalid mode UX
'file/ok_2.sd', # Invalid mode RWM