mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-09-02 15:25:27 +00:00
Merge add improved comparisons in policy conditionals
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1723 Merged-by: Georgia Garcia <georgia.garcia@canonical.com>
This commit is contained in:
@@ -103,8 +103,7 @@ test-utils:
|
|||||||
- printf '\e[0K%s:%s:%s\r\e[0K\n' section_end "$(date +%s)" install_extra_deps
|
- printf '\e[0K%s:%s:%s\r\e[0K\n' section_end "$(date +%s)" install_extra_deps
|
||||||
|
|
||||||
# See apparmor/apparmor#221
|
# See apparmor/apparmor#221
|
||||||
- make -C parser/tst gen_dbus
|
- make -C parser/tst gen_tests
|
||||||
- make -C parser/tst gen_xtrans
|
|
||||||
- make -C utils check
|
- make -C utils check
|
||||||
- make -C utils/test coverage-regression
|
- make -C utils/test coverage-regression
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@@ -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> )*
|
B<PREAMBLE> = ( I<COMMENT> | I<VARIABLE ASSIGNMENT> | I<ALIAS RULE> | I<INCLUDE> | I<ABI> )*
|
||||||
Variable assignment and alias rules must come before the profile.
|
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<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<ALIAS RULE> = 'alias' I<ABS PATH> '-E<gt>' I<REWRITTEN ABS PATH> ','
|
||||||
|
|
||||||
B<INCLUDE> = ( '#include' | 'include' ) [ 'if exists' ] ( I<ABS PATH> | I<MAGIC 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<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> )* '}'
|
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<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<ACCESS TYPE> = ( 'allow' | 'deny' )
|
||||||
|
|
||||||
B<QUALIFIERS> = [ 'priority' '=' <INTEGER> ] [ 'audit' ] [ I<ACCESS TYPE> ]
|
B<QUALIFIERS> = [ 'priority' '=' <INTEGER> ] [ 'audit' ] [ I<ACCESS TYPE> ]
|
||||||
@@ -1880,6 +1892,13 @@ example) is collapsed.
|
|||||||
|
|
||||||
=back
|
=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
|
=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
|
=head1 FILES
|
||||||
|
|
||||||
=over 4
|
=over 4
|
||||||
|
@@ -16,6 +16,8 @@
|
|||||||
* Ltd.
|
* Ltd.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "cond_expr.h"
|
#include "cond_expr.h"
|
||||||
#include "parser.h"
|
#include "parser.h"
|
||||||
#include "symtab.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;
|
variable *ref;
|
||||||
if (!defined) {
|
if (op == BOOLEAN_OP) {
|
||||||
ref = symtab::get_boolean_var(var);
|
ref = symtab::get_boolean_var(var);
|
||||||
if (!ref) {
|
if (!ref) {
|
||||||
/* FIXME check for set var */
|
|
||||||
yyerror(_("Unset boolean variable %s used in if-expression"), var);
|
yyerror(_("Unset boolean variable %s used in if-expression"), var);
|
||||||
}
|
}
|
||||||
result = ref->boolean;
|
result = ref->boolean;
|
||||||
} else {
|
} else if (op == DEFINED_OP) {
|
||||||
ref = symtab::get_set_var(var);
|
ref = symtab::get_set_var(var);
|
||||||
if (!ref) {
|
if (!ref) {
|
||||||
result = false;
|
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());
|
PDEBUG("Matched: defined set expr %s value %s\n", var, ref->expanded.begin()->c_str());
|
||||||
result = true;
|
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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,12 +19,31 @@
|
|||||||
#ifndef __AA_COND_EXPR_H
|
#ifndef __AA_COND_EXPR_H
|
||||||
#define __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 {
|
class cond_expr {
|
||||||
private:
|
private:
|
||||||
bool result;
|
bool result;
|
||||||
public:
|
public:
|
||||||
cond_expr(bool result);
|
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()
|
virtual ~cond_expr()
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
@@ -299,7 +299,11 @@ ADD_ASSIGN \+=
|
|||||||
ARROW ->
|
ARROW ->
|
||||||
LT_EQUAL <=
|
LT_EQUAL <=
|
||||||
LT <
|
LT <
|
||||||
|
GT_EQUAL >=
|
||||||
GT >
|
GT >
|
||||||
|
EQ_OP ==
|
||||||
|
NE_OP !=
|
||||||
|
IN_OP in
|
||||||
|
|
||||||
/* IF adding new state please update state_names table and default rule (just
|
/* IF adding new state please update state_names table and default rule (just
|
||||||
* above the state_names table) at the eof.
|
* above the state_names table) at the eof.
|
||||||
@@ -336,6 +340,8 @@ GT >
|
|||||||
%x MQUEUE_MODE
|
%x MQUEUE_MODE
|
||||||
%x IOURING_MODE
|
%x IOURING_MODE
|
||||||
%x INTEGER_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 */ }
|
{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 {
|
#include{WS}+if{WS}+exists/{WS}.*\r?\n {
|
||||||
/* Don't use PUSH() macro here as we don't want #include echoed out.
|
/* Don't use PUSH() macro here as we don't want #include echoed out.
|
||||||
* It needs to be handled specially
|
* It needs to be handled specially
|
||||||
@@ -694,16 +735,6 @@ all/({WS}|[^[:alnum:]_]) {
|
|||||||
|
|
||||||
{ADD_ASSIGN} { PUSH_AND_RETURN(ASSIGN_MODE, TOK_ADD_ASSIGN); }
|
{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); }
|
{OPEN_BRACE} { RETURN_TOKEN(TOK_OPEN); }
|
||||||
|
|
||||||
{CLOSE_BRACE} { RETURN_TOKEN(TOK_CLOSE); }
|
{CLOSE_BRACE} { RETURN_TOKEN(TOK_CLOSE); }
|
||||||
@@ -772,6 +803,9 @@ all/({WS}|[^[:alnum:]_]) {
|
|||||||
case TOK_MQUEUE:
|
case TOK_MQUEUE:
|
||||||
state = MQUEUE_MODE;
|
state = MQUEUE_MODE;
|
||||||
break;
|
break;
|
||||||
|
case TOK_IF:
|
||||||
|
state = EXPR_MODE;
|
||||||
|
break;
|
||||||
default: /* nothing */
|
default: /* nothing */
|
||||||
break;
|
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 {
|
\r?\n {
|
||||||
DUMP_PREPROCESS;
|
DUMP_PREPROCESS;
|
||||||
current_lineno++;
|
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) {
|
(.|\n) {
|
||||||
DUMP_PREPROCESS;
|
DUMP_PREPROCESS;
|
||||||
/* Something we didn't expect */
|
/* Something we didn't expect */
|
||||||
@@ -832,4 +866,5 @@ unordered_map<int, string> state_names = {
|
|||||||
STATE_TABLE_ENT(MQUEUE_MODE),
|
STATE_TABLE_ENT(MQUEUE_MODE),
|
||||||
STATE_TABLE_ENT(IOURING_MODE),
|
STATE_TABLE_ENT(IOURING_MODE),
|
||||||
STATE_TABLE_ENT(INTEGER_MODE),
|
STATE_TABLE_ENT(INTEGER_MODE),
|
||||||
|
STATE_TABLE_ENT(EXPR_MODE),
|
||||||
};
|
};
|
||||||
|
@@ -134,6 +134,8 @@ static const unordered_map<string, int> keyword_table = {
|
|||||||
{"sqpoll", TOK_SQPOLL},
|
{"sqpoll", TOK_SQPOLL},
|
||||||
{"all", TOK_ALL},
|
{"all", TOK_ALL},
|
||||||
{"priority", TOK_PRIORITY},
|
{"priority", TOK_PRIORITY},
|
||||||
|
{"and", TOK_AND},
|
||||||
|
{"or", TOK_OR},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* glibc maps bsd ofile to nofile but musl does not. */
|
/* glibc maps bsd ofile to nofile but musl does not. */
|
||||||
|
@@ -152,6 +152,14 @@ static void abi_features(char *filename, bool search);
|
|||||||
%token TOK_SQPOLL
|
%token TOK_SQPOLL
|
||||||
%token TOK_ALL
|
%token TOK_ALL
|
||||||
%token TOK_PRIORITY
|
%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 */
|
/* rlimits */
|
||||||
%token TOK_RLIMIT
|
%token TOK_RLIMIT
|
||||||
@@ -270,6 +278,9 @@ static void abi_features(char *filename, bool search);
|
|||||||
%type <var_val> TOK_VALUE
|
%type <var_val> TOK_VALUE
|
||||||
%type <val_list> valuelist
|
%type <val_list> valuelist
|
||||||
%type <cond> expr
|
%type <cond> expr
|
||||||
|
%type <cond> term
|
||||||
|
%type <cond> notfactor
|
||||||
|
%type <cond> factor
|
||||||
%type <id> id_or_var
|
%type <id> id_or_var
|
||||||
%type <id> opt_id_or_var
|
%type <id> opt_id_or_var
|
||||||
%type <boolean> opt_subset_flag
|
%type <boolean> opt_subset_flag
|
||||||
@@ -942,37 +953,134 @@ cond_rule: TOK_IF expr block TOK_ELSE cond_rule
|
|||||||
$$ = ret;
|
$$ = 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());
|
cond_expr *conds = new cond_expr(!$2->eval());
|
||||||
delete $2;
|
delete $2;
|
||||||
$$ = conds;
|
$$ = conds;
|
||||||
}
|
}
|
||||||
|
| factor
|
||||||
expr: TOK_BOOL_VAR
|
|
||||||
{
|
{
|
||||||
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());
|
PDEBUG("Matched: boolean expr %s value: %d\n", $1, conds->eval());
|
||||||
$$ = conds;
|
$$ = conds;
|
||||||
free($1);
|
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());
|
PDEBUG("Matched: defined set expr %s value %d\n", $2, conds->eval());
|
||||||
$$ = conds;
|
$$ = conds;
|
||||||
free($2);
|
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());
|
PDEBUG("Matched: defined set expr %s value %d\n", $2, conds->eval());
|
||||||
$$ = conds;
|
$$ = conds;
|
||||||
free($2);
|
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_ID { $$ = $1; }
|
||||||
id_or_var: TOK_SET_VAR { $$ = $1; };
|
id_or_var: TOK_SET_VAR { $$ = $1; };
|
||||||
|
|
||||||
|
@@ -17,10 +17,10 @@ endif
|
|||||||
|
|
||||||
all: tests
|
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
|
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: $(GEN_TRANS_DIRS)
|
||||||
./gen-xtrans.py
|
./gen-xtrans.py
|
||||||
@@ -31,10 +31,15 @@ $(GEN_TRANS_DIRS):
|
|||||||
gen_dbus: $(GEN_TRANS_DIRS)
|
gen_dbus: $(GEN_TRANS_DIRS)
|
||||||
./gen-dbus.py
|
./gen-dbus.py
|
||||||
|
|
||||||
|
gen_conditionals: $(GEN_TRANS_DIRS)
|
||||||
|
./gen-conditionals.py
|
||||||
|
|
||||||
|
gen_tests: gen_xtrans gen_dbus gen_conditionals
|
||||||
|
|
||||||
error_output: $(PARSER)
|
error_output: $(PARSER)
|
||||||
LANG=C ./errors.py -p "$(PARSER)" $(PYTEST_ARG)
|
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}
|
$(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
|
# 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)
|
dirtest: $(PARSER)
|
||||||
LANG=C APPARMOR_PARSER="$(PARSER) $(PARSER_ARGS)" ./dirtest.sh
|
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
|
LANG=C ./valgrind_simple.py -p "$(PARSER) $(PARSER_ARGS)" -v simple_tests
|
||||||
|
|
||||||
$(PARSER):
|
$(PARSER):
|
||||||
|
75
parser/tst/gen-conditionals.py
Executable file
75
parser/tst/gen-conditionals.py
Executable 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))
|
12
parser/tst/simple_tests/conditional/bad_4.sd
Normal file
12
parser/tst/simple_tests/conditional/bad_4.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
10
parser/tst/simple_tests/conditional/bad_5.sd
Normal file
10
parser/tst/simple_tests/conditional/bad_5.sd
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#=DESCRIPTION trying to use undefined boolean
|
||||||
|
#=EXRESULT FAIL
|
||||||
|
|
||||||
|
$FOO=true
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if $BAR {
|
||||||
|
/bin/true rix,
|
||||||
|
}
|
||||||
|
}
|
9
parser/tst/simple_tests/conditional/bad_6.sd
Normal file
9
parser/tst/simple_tests/conditional/bad_6.sd
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#=DESCRIPTION unfinished else
|
||||||
|
#=EXRESULT FAIL
|
||||||
|
|
||||||
|
$BAR=true
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if $BAR {
|
||||||
|
/bin/true rix,
|
||||||
|
} else {
|
19
parser/tst/simple_tests/conditional/bad_dup_hats_3.sd
Normal file
19
parser/tst/simple_tests/conditional/bad_dup_hats_3.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
parser/tst/simple_tests/conditional/ok_10.sd
Normal file
11
parser/tst/simple_tests/conditional/ok_10.sd
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#=DESCRIPTION conditional within profile
|
||||||
|
#=EXRESULT PASS
|
||||||
|
|
||||||
|
$FOO=true
|
||||||
|
$BAR=true
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if $FOO and $BAR {
|
||||||
|
/bin/true rix,
|
||||||
|
}
|
||||||
|
}
|
11
parser/tst/simple_tests/conditional/ok_11.sd
Normal file
11
parser/tst/simple_tests/conditional/ok_11.sd
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#=DESCRIPTION conditional within profile
|
||||||
|
#=EXRESULT PASS
|
||||||
|
|
||||||
|
$FOO=true
|
||||||
|
$BAR=true
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if $FOO or $BAR {
|
||||||
|
/bin/true rix,
|
||||||
|
}
|
||||||
|
}
|
11
parser/tst/simple_tests/conditional/ok_12.sd
Normal file
11
parser/tst/simple_tests/conditional/ok_12.sd
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#=DESCRIPTION conditional within profile
|
||||||
|
#=EXRESULT PASS
|
||||||
|
|
||||||
|
$FOO=true
|
||||||
|
$BAR=true
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if ( $FOO and $BAR ) {
|
||||||
|
/bin/true rix,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_13.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_13.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_14.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_14.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_15.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_15.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_16.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_16.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_17.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_17.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_18.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_18.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_19.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_19.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_20.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_20.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_21.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_21.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_22.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_22.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_23.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_23.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_24.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_24.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_25.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_25.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_26.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_26.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_27.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_27.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_28.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_28.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
12
parser/tst/simple_tests/conditional/ok_29.sd
Normal file
12
parser/tst/simple_tests/conditional/ok_29.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
11
parser/tst/simple_tests/conditional/ok_30.sd
Normal file
11
parser/tst/simple_tests/conditional/ok_30.sd
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
8
parser/tst/simple_tests/conditional/ok_31.sd
Normal file
8
parser/tst/simple_tests/conditional/ok_31.sd
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#=DESCRIPTION conditional within profile
|
||||||
|
#=EXRESULT PASS
|
||||||
|
|
||||||
|
/bin/true {
|
||||||
|
if defined $UNKNOWN {
|
||||||
|
/bin/true rix,
|
||||||
|
}
|
||||||
|
}
|
@@ -66,8 +66,6 @@ char *variable::process_var(const char *var)
|
|||||||
orig++;
|
orig++;
|
||||||
len--;
|
len--;
|
||||||
} else {
|
} else {
|
||||||
PERROR("ASSERT: Found var '%s' without variable prefix\n",
|
|
||||||
var);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -284,7 +284,7 @@ suites:
|
|||||||
summary: Unit tests for the Python utilities.
|
summary: Unit tests for the Python utilities.
|
||||||
prepare: |
|
prepare: |
|
||||||
# Generate apparmor profiles that the tests rely on.
|
# 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.
|
# Spread does not support programmatically generated test variants.
|
||||||
# Ensure that the list baked into utils/test/task.yaml contains all
|
# Ensure that the list baked into utils/test/task.yaml contains all
|
||||||
# the files matching utils/test/test-*.py
|
# the files matching utils/test/test-*.py
|
||||||
|
@@ -35,13 +35,14 @@ from apparmor.common import (
|
|||||||
from apparmor.profile_list import ProfileList, preamble_ruletypes
|
from apparmor.profile_list import ProfileList, preamble_ruletypes
|
||||||
from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, ruletypes
|
from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, ruletypes
|
||||||
from apparmor.regex import (
|
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_START,
|
||||||
RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END,
|
RE_PROFILE_CONDITIONAL_ELSE, RE_PROFILE_END,
|
||||||
RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_METADATA_LOGPROF_SUGGEST,
|
RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_METADATA_LOGPROF_SUGGEST,
|
||||||
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.file import FileRule
|
from apparmor.rule.file import FileRule
|
||||||
from apparmor.rule.include import IncludeRule
|
from apparmor.rule.include import IncludeRule
|
||||||
|
from apparmor.rule.conditional import ConditionalBlock
|
||||||
from apparmor.logparser import ReadLog
|
from apparmor.logparser import ReadLog
|
||||||
from apparmor.translations import init_translation
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
@@ -1783,28 +1784,57 @@ def attach_profile_data(profiles, profile_data):
|
|||||||
profiles[p] = deepcopy(profile_data[p])
|
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_data = {}
|
||||||
profile = None
|
|
||||||
hat = None
|
hat = None
|
||||||
profname = None
|
|
||||||
in_contained_hat = None
|
in_contained_hat = None
|
||||||
parsed_profiles = []
|
parsed_profiles = []
|
||||||
initial_comment = ''
|
initial_comment = ''
|
||||||
lastline = None
|
lastline = None
|
||||||
|
conditional_block = None
|
||||||
|
|
||||||
active_profiles.init_file(file)
|
if do_include or profname and profname not in profile_data:
|
||||||
|
profile_data[profname] = prof_storage
|
||||||
if do_include:
|
|
||||||
profile = file
|
|
||||||
hat = None
|
|
||||||
profname = combine_profname((profile, hat))
|
|
||||||
profile_data[profname] = ProfileStorage(profile, hat, 'parse_profile_data() do_include')
|
|
||||||
profile_data[profname]['filename'] = file
|
profile_data[profname]['filename'] = file
|
||||||
|
lineno = 0
|
||||||
for lineno, line in enumerate(data):
|
while lineno < len(data):
|
||||||
line = line.strip()
|
line = data[lineno].strip()
|
||||||
|
debug_lineno = src_lineno + lineno
|
||||||
if not line:
|
if not line:
|
||||||
|
lineno += 1
|
||||||
continue
|
continue
|
||||||
# we're dealing with a multiline statement
|
# we're dealing with a multiline statement
|
||||||
if lastline:
|
if lastline:
|
||||||
@@ -1812,11 +1842,13 @@ def parse_profile_data(data, file, do_include, in_preamble):
|
|||||||
lastline = None
|
lastline = None
|
||||||
|
|
||||||
# is line handled by a *Rule class?
|
# 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 rule_name:
|
||||||
if in_preamble:
|
if in_preamble:
|
||||||
active_profiles.add_rule(file, rule_name, rule_obj)
|
active_profiles.add_rule(file, rule_name, rule_obj)
|
||||||
else:
|
else:
|
||||||
|
if profname not in profile_data:
|
||||||
|
profile_data[profname] = prof_storage
|
||||||
profile_data[profname][rule_name].add(rule_obj)
|
profile_data[profname][rule_name].add(rule_obj)
|
||||||
|
|
||||||
if rule_name == 'inc_ie':
|
if rule_name == 'inc_ie':
|
||||||
@@ -1838,16 +1870,16 @@ def parse_profile_data(data, file, do_include, in_preamble):
|
|||||||
|
|
||||||
in_preamble = False
|
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:
|
if profile == hat:
|
||||||
hat = None
|
hat = None
|
||||||
profname = combine_profname((profile, hat))
|
profname = combine_profname((profile, hat))
|
||||||
|
|
||||||
if profile_data.get(profname, False):
|
if profile_data.get(profname, False):
|
||||||
|
print(file)
|
||||||
raise AppArmorException(
|
raise AppArmorException(
|
||||||
'Profile %(profile)s defined twice in %(file)s, last found in line %(line)s'
|
'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
|
profile_data[profname] = prof_storage
|
||||||
|
|
||||||
@@ -1857,12 +1889,11 @@ def parse_profile_data(data, file, do_include, in_preamble):
|
|||||||
|
|
||||||
initial_comment = ''
|
initial_comment = ''
|
||||||
|
|
||||||
elif RE_PROFILE_END.search(line):
|
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)
|
||||||
# If profile ends and we're not in one
|
profile_data[profname]['in_cond'] = in_if
|
||||||
if not profile:
|
lineno = lineno + ret_lineno
|
||||||
raise AppArmorException(
|
|
||||||
_('Syntax Error: Unexpected End of Profile reached in file: %(file)s line: %(line)s')
|
profile_data = {**profile_data, **ret_profile_data}
|
||||||
% {'file': file, 'line': lineno + 1})
|
|
||||||
|
|
||||||
if in_contained_hat:
|
if in_contained_hat:
|
||||||
hat = None
|
hat = None
|
||||||
@@ -1876,32 +1907,49 @@ def parse_profile_data(data, file, do_include, in_preamble):
|
|||||||
|
|
||||||
initial_comment = ''
|
initial_comment = ''
|
||||||
|
|
||||||
elif RE_PROFILE_CONDITIONAL.search(line):
|
elif RE_PROFILE_END.search(line):
|
||||||
# Conditional Boolean
|
# If profile ends and we're not in one
|
||||||
pass
|
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):
|
return lineno + 1, profile_data, True
|
||||||
# Conditional Variable defines
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif RE_PROFILE_CONDITIONAL_BOOLEAN.search(line):
|
elif RE_PROFILE_CONDITIONAL_START.search(line):
|
||||||
# Conditional Boolean defined
|
conditional_block = ConditionalBlock(line, active_profiles.files[file])
|
||||||
pass
|
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):
|
elif RE_PROFILE_CHANGE_HAT.search(line):
|
||||||
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
|
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
|
||||||
|
|
||||||
if not profile:
|
if not profile:
|
||||||
raise AppArmorException(_('Syntax Error: Unexpected change hat declaration found in file: %(file)s line: %(line)s')
|
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')
|
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('#'):
|
elif line.startswith('#'):
|
||||||
# Handle initial comments
|
# Handle initial comments
|
||||||
if not profile:
|
if not profile:
|
||||||
if line.startswith('# Last Modified:'):
|
if line.startswith('# Last Modified:'):
|
||||||
|
lineno += 1
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
initial_comment = initial_comment + line + '\n'
|
initial_comment = initial_comment + line + '\n'
|
||||||
@@ -1925,14 +1973,16 @@ def parse_profile_data(data, file, do_include, in_preamble):
|
|||||||
else:
|
else:
|
||||||
raise AppArmorException(
|
raise AppArmorException(
|
||||||
_('Syntax Error: Unknown line found in file %(file)s line %(lineno)s:\n %(line)s')
|
_('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:
|
if lastline:
|
||||||
# lastline gets merged into line (and reset to None) when reading the next line.
|
# 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
|
# If it isn't empty, this means there's something unparsable at the end of the profile
|
||||||
raise AppArmorException(
|
raise AppArmorException(
|
||||||
_('Syntax Error: Unknown line found in file %(file)s line %(lineno)s:\n %(line)s')
|
_('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
|
# Below is not required I'd say
|
||||||
if not do_include:
|
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")
|
_("Syntax Error: Missing '}' or ','. Reached end of file %(file)s while inside profile %(profile)s")
|
||||||
% {'file': file, 'profile': profile})
|
% {'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
|
return profile_data
|
||||||
|
|
||||||
|
|
||||||
|
@@ -35,6 +35,7 @@ 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.pivot_root import PivotRootRule, PivotRootRuleset
|
from apparmor.rule.pivot_root import PivotRootRule, PivotRootRuleset
|
||||||
from apparmor.rule.unix import UnixRule, UnixRuleset
|
from apparmor.rule.unix import UnixRule, UnixRuleset
|
||||||
|
from apparmor.rule.conditional import ConditionalBlock, ConditionalBlockset
|
||||||
|
|
||||||
from apparmor.translations import init_translation
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ ruletypes = {
|
|||||||
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
|
'mount': {'rule': MountRule, 'ruleset': MountRuleset},
|
||||||
'pivot_root': {'rule': PivotRootRule, 'ruleset': PivotRootRuleset},
|
'pivot_root': {'rule': PivotRootRule, 'ruleset': PivotRootRuleset},
|
||||||
'unix': {'rule': UnixRule, 'ruleset': UnixRuleset},
|
'unix': {'rule': UnixRule, 'ruleset': UnixRuleset},
|
||||||
|
'cond_block': {'rule': ConditionalBlock, 'ruleset': ConditionalBlockset},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ class ProfileStorage:
|
|||||||
data['profile_keyword'] = False
|
data['profile_keyword'] = False
|
||||||
data['is_hat'] = False # profile or hat?
|
data['is_hat'] = False # profile or hat?
|
||||||
data['hat_keyword'] = False # True for 'hat foo', False for '^foo'
|
data['hat_keyword'] = False # True for 'hat foo', False for '^foo'
|
||||||
|
data['in_cond'] = False # used for profiles created inside conditionals
|
||||||
|
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
@@ -178,6 +181,7 @@ class ProfileStorage:
|
|||||||
'inc_ie',
|
'inc_ie',
|
||||||
'rlimit',
|
'rlimit',
|
||||||
'capability',
|
'capability',
|
||||||
|
'cond_block',
|
||||||
'network',
|
'network',
|
||||||
'dbus',
|
'dbus',
|
||||||
'mount',
|
'mount',
|
||||||
|
@@ -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_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_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_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 = '(?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
|
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_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?'
|
||||||
|
|
||||||
RE_VARIABLE = re.compile(RE_VAR)
|
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_END = re.compile(r'^\s*\}' + RE_EOL)
|
||||||
RE_PROFILE_ALL = re.compile(RE_PRIORITY_AUDIT_DENY + r'all' + RE_COMMA_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_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_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_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_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_PROFILE_CONDITIONAL_BOOLEAN = re.compile(r'^\s*if\s+(not\s+)?defined\s+(\$\{?\w+\}?)\s*\{\s*(#.*)?$')
|
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_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_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)
|
RE_PROFILE_HAT_DEF = re.compile(r'^(?P<leadingspace>\s*)(?P<hat_keyword>\^|hat\s+)(?P<hat>"??[^)]+?"??)' + RE_FLAGS + r'\s*\{' + RE_EOL)
|
||||||
|
@@ -51,7 +51,7 @@ class BooleanRule(BaseRule):
|
|||||||
raise AppArmorException('Passed invalid value to %s: %s' % (self.__class__.__name__, value))
|
raise AppArmorException('Passed invalid value to %s: %s' % (self.__class__.__name__, value))
|
||||||
|
|
||||||
self.varname = varname
|
self.varname = varname
|
||||||
self.value = value
|
self.value = value == 'true'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_instance(cls, raw_rule, matches):
|
def _create_instance(cls, raw_rule, matches):
|
||||||
@@ -70,7 +70,7 @@ class BooleanRule(BaseRule):
|
|||||||
|
|
||||||
space = ' ' * depth
|
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):
|
def _is_covered_localvars(self, other_rule):
|
||||||
"""check if other_rule is covered by this rule object"""
|
"""check if other_rule is covered by this rule object"""
|
||||||
@@ -78,7 +78,10 @@ class BooleanRule(BaseRule):
|
|||||||
if self.varname != other_rule.varname:
|
if self.varname != other_rule.varname:
|
||||||
return False
|
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
|
return False
|
||||||
|
|
||||||
# still here? -> then it is covered
|
# still here? -> then it is covered
|
||||||
|
534
utils/apparmor/rule/conditional.py
Normal file
534
utils/apparmor/rule/conditional.py
Normal 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'))
|
@@ -40,10 +40,10 @@ class VariableRule(BaseRule):
|
|||||||
|
|
||||||
if not isinstance(varname, str):
|
if not isinstance(varname, str):
|
||||||
raise AppArmorBug('Passed unknown type for varname to %s: %s' % (self.__class__.__name__, varname))
|
raise AppArmorBug('Passed unknown type for varname to %s: %s' % (self.__class__.__name__, varname))
|
||||||
if not varname.startswith('@{'):
|
if not varname.startswith('@'):
|
||||||
raise AppArmorException("Passed invalid varname to %s (doesn't start with '@{'): %s" % (self.__class__.__name__, varname))
|
raise AppArmorException("Passed invalid varname to %s (doesn't start with '@'): %s" % (self.__class__.__name__, varname))
|
||||||
if not varname.endswith('}'):
|
if (varname.startswith('@{') and not varname.endswith('}')) or (not varname.startswith('@{') and varname.endswith('}')):
|
||||||
raise AppArmorException("Passed invalid varname to %s (doesn't end with '}'): %s" % (self.__class__.__name__, varname))
|
raise AppArmorException("Passed invalid varname to %s (mismatched braces): %s" % (self.__class__.__name__, varname))
|
||||||
|
|
||||||
if not isinstance(mode, str):
|
if not isinstance(mode, str):
|
||||||
raise AppArmorBug('Passed unknown type for variable assignment mode to %s: %s' % (self.__class__.__name__, mode))
|
raise AppArmorBug('Passed unknown type for variable assignment mode to %s: %s' % (self.__class__.__name__, mode))
|
||||||
|
@@ -107,3 +107,20 @@ profile foo//bar {
|
|||||||
/usr/bin/a/simple/cleanprof/test/profile//child {
|
/usr/bin/a/simple/cleanprof/test/profile//child {
|
||||||
/home/test-child r,
|
/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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -107,3 +107,20 @@ profile foo//bar {
|
|||||||
/usr/bin/a/simple/cleanprof/test/profile//child {
|
/usr/bin/a/simple/cleanprof/test/profile//child {
|
||||||
/home/test-child r,
|
/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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -86,3 +86,24 @@ profile foo//bar {
|
|||||||
/home/namedchild r,
|
/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,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -44,12 +44,12 @@ class BooleanTest(AATest):
|
|||||||
class BooleanTestParse(BooleanTest):
|
class BooleanTestParse(BooleanTest):
|
||||||
tests = (
|
tests = (
|
||||||
# rawrule comment varname value
|
# rawrule comment varname value
|
||||||
('$foo=true', exp('', '$foo', 'true')),
|
('$foo=true', exp('', '$foo', True)),
|
||||||
('$foo = false', exp('', '$foo', 'false')),
|
('$foo = false', exp('', '$foo', False)),
|
||||||
('$foo=TrUe', exp('', '$foo', 'true')),
|
('$foo=TrUe', exp('', '$foo', True)),
|
||||||
('$foo = FaLsE', exp('', '$foo', 'false')),
|
('$foo = FaLsE', exp('', '$foo', False)),
|
||||||
(' $foo = true ', exp('', '$foo', 'true')),
|
(' $foo = true ', exp('', '$foo', True)),
|
||||||
(' $foo = true # comment', exp(' # comment', '$foo', 'true')),
|
(' $foo = true # comment', exp(' # comment', '$foo', True)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _run_test(self, rawrule, expected):
|
def _run_test(self, rawrule, expected):
|
||||||
|
402
utils/test/test-conditional.py
Normal file
402
utils/test/test-conditional.py
Normal 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)
|
@@ -340,27 +340,6 @@ syntax_failure = (
|
|||||||
# missing profile keywords
|
# missing profile keywords
|
||||||
'profile/re_named_ok2.sd',
|
'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
|
# unexpected uppercase vs. lowercase in *x rules
|
||||||
'file/ok_5.sd', # Invalid mode UX
|
'file/ok_5.sd', # Invalid mode UX
|
||||||
'file/ok_2.sd', # Invalid mode RWM
|
'file/ok_2.sd', # Invalid mode RWM
|
||||||
|
Reference in New Issue
Block a user