mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-22 01:57:43 +00:00
Compare commits
71 Commits
f73ff065a8
...
4a46925e9b
Author | SHA1 | Date | |
---|---|---|---|
|
4a46925e9b | ||
|
e7daccedc6 | ||
|
468f0096ee | ||
|
d993dfbb02 | ||
|
ba336533ac | ||
|
0d34f12d7e | ||
|
ebba635fa9 | ||
|
e477ccacfa | ||
|
9c5064529a | ||
|
862d8ec9fc | ||
|
fbd266c63f | ||
|
fcbf8e34ec | ||
|
01ab33202a | ||
|
24216d79e9 | ||
|
bef673f3c6 | ||
|
8210308508 | ||
|
a8875460ed | ||
|
eae49bf8de | ||
|
144d782ae8 | ||
|
df1a4c8782 | ||
|
4c30a0ac65 | ||
|
60ca491f21 | ||
|
43fa5f88a7 | ||
|
bb03d9ee08 | ||
|
d9866f0a24 | ||
|
fedcab2ad0 | ||
|
b6caed3b57 | ||
|
ae70dc38f8 | ||
|
51bdbec119 | ||
|
b8dee97ed3 | ||
|
8b2e2c3358 | ||
|
3faddfcf46 | ||
|
05458768cf | ||
|
cb0d66d55a | ||
|
0de9678d4f | ||
|
617d3021e8 | ||
|
63b46dd3d7 | ||
|
67382dcf15 | ||
|
d61295a249 | ||
|
a2f2ca6119 | ||
|
61e09c6ffa | ||
|
45a7cc1ed0 | ||
|
dc78be4db6 | ||
|
ea97cbedef | ||
|
514bf114b2 | ||
|
0430080a16 | ||
|
0f36070a54 | ||
|
392849e518 | ||
|
e8cd6e704a | ||
|
95d7f37520 | ||
|
c54c4a7e01 | ||
|
375470144f | ||
|
73bcf488b2 | ||
|
117df51e4a | ||
|
37185f50a4 | ||
|
b40ac50f49 | ||
|
87e0151c7c | ||
|
b9ed931c90 | ||
|
63ce02c01d | ||
|
e82ee9f4f4 | ||
|
9ac6047f6c | ||
|
73f4f650e7 | ||
|
12e3557896 | ||
|
d8c57da6ba | ||
|
4de3b64e52 | ||
|
71a71e0fa7 | ||
|
d3a49ff566 | ||
|
3e7ddc1ce5 | ||
|
2448655188 | ||
|
f1773f4083 | ||
|
766cd2d8a5 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -255,6 +255,7 @@ tests/regression/apparmor/introspect
|
||||
tests/regression/apparmor/io_uring
|
||||
tests/regression/apparmor/link
|
||||
tests/regression/apparmor/link_subset
|
||||
tests/regression/apparmor/linkat_tmpfile
|
||||
tests/regression/apparmor/mkdir
|
||||
tests/regression/apparmor/mmap
|
||||
tests/regression/apparmor/mount
|
||||
|
3
Makefile
3
Makefile
@ -54,9 +54,6 @@ snapshot: clean
|
||||
.PHONY: coverity
|
||||
coverity: snapshot
|
||||
cd $(SNAPSHOT_NAME)/libraries/libapparmor && ./configure --with-python
|
||||
$(foreach dir, libraries/libapparmor utils, \
|
||||
cov-build --dir $(COVERITY_DIR) --no-command --fs-capture-search $(SNAPSHOT_NAME)/$(dir); \
|
||||
mv $(COVERITY_DIR)/build-log.txt $(COVERITY_DIR)/build-log-python-$(subst /,.,$(dir)).txt ;)
|
||||
cov-build --dir $(COVERITY_DIR) -- sh -c \
|
||||
"$(foreach dir, $(filter-out utils profiles tests, $(DIRS)), \
|
||||
$(MAKE) -j $$(nproc) -C $(SNAPSHOT_NAME)/$(dir);) "
|
||||
|
@ -1 +1 @@
|
||||
4.1.0~beta1
|
||||
5.0.0~alpha1
|
||||
|
@ -1,2 +1,4 @@
|
||||
profile unconfined {
|
||||
change_profile -> system_tor,
|
||||
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ bool aare_rules::append_rule(const char *rule, bool oob, bool with_perm,
|
||||
CHFA *aare_rules::create_chfa(int *min_match_len,
|
||||
vector <aa_perms> &perms_table,
|
||||
optflags const &opts, bool filedfa,
|
||||
bool extended_perms, bool prompt)
|
||||
bool extended_perms)
|
||||
{
|
||||
/* finish constructing the expr tree from the different permission
|
||||
* set nodes */
|
||||
@ -315,7 +315,7 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
|
||||
//cerr << "Checking extended perms " << extended_perms << "\n";
|
||||
if (extended_perms) {
|
||||
//cerr << "creating permstable\n";
|
||||
dfa.compute_perms_table(perms_table, prompt);
|
||||
dfa.compute_perms_table(perms_table);
|
||||
// TODO: move perms table to a class
|
||||
if (opts.dump & DUMP_DFA_TRANS_TABLE && perms_table.size()) {
|
||||
cerr << "Perms Table size: " << perms_table.size() << "\n";
|
||||
@ -329,7 +329,7 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
|
||||
cerr << "\n";
|
||||
}
|
||||
}
|
||||
chfa = new CHFA(dfa, eq, opts, extended_perms, prompt);
|
||||
chfa = new CHFA(dfa, eq, opts, extended_perms);
|
||||
if (opts.dump & DUMP_DFA_TRANS_TABLE)
|
||||
chfa->dump(cerr);
|
||||
if (opts.dump & DUMP_DFA_COMPTRESSED_STATES)
|
||||
@ -350,15 +350,14 @@ CHFA *aare_rules::create_chfa(int *min_match_len,
|
||||
void *aare_rules::create_dfablob(size_t *size, int *min_match_len,
|
||||
vector <aa_perms> &perms_table,
|
||||
optflags const &opts, bool filedfa,
|
||||
bool extended_perms, bool prompt)
|
||||
bool extended_perms)
|
||||
{
|
||||
char *buffer = NULL;
|
||||
stringstream stream;
|
||||
|
||||
try {
|
||||
CHFA *chfa = create_chfa(min_match_len, perms_table,
|
||||
opts, filedfa, extended_perms,
|
||||
prompt);
|
||||
opts, filedfa, extended_perms);
|
||||
if (!chfa) {
|
||||
*size = 0;
|
||||
return NULL;
|
||||
@ -383,82 +382,3 @@ void *aare_rules::create_dfablob(size_t *size, int *min_match_len,
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
/* create a dfa from the ruleset
|
||||
* returns: buffer contain dfa tables, @size set to the size of the tables
|
||||
* else NULL on failure, @min_match_len set to the shortest string
|
||||
* that can match the dfa for determining xmatch priority.
|
||||
*/
|
||||
void *aare_rules::create_welded_dfablob(aare_rules *file_rules,
|
||||
size_t *size, int *min_match_len,
|
||||
size_t *new_start,
|
||||
vector <aa_perms> &perms_table,
|
||||
optflags const &opts,
|
||||
bool extended_perms, bool prompt)
|
||||
{
|
||||
int file_min_len;
|
||||
vector <aa_perms> file_perms;
|
||||
CHFA *file_chfa;
|
||||
try {
|
||||
file_chfa = file_rules->create_chfa(&file_min_len,
|
||||
file_perms, opts,
|
||||
true, extended_perms, prompt);
|
||||
if (!file_chfa) {
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
catch(int error) {
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CHFA *policy_chfa;
|
||||
try {
|
||||
policy_chfa = create_chfa(min_match_len,
|
||||
perms_table, opts,
|
||||
false, extended_perms, prompt);
|
||||
if (!policy_chfa) {
|
||||
delete file_chfa;
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
catch(int error) {
|
||||
delete file_chfa;
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
stringstream stream;
|
||||
try {
|
||||
policy_chfa->weld_file_to_policy(*file_chfa, *new_start,
|
||||
extended_perms, prompt,
|
||||
perms_table, file_perms);
|
||||
policy_chfa->flex_table(stream, opts);
|
||||
}
|
||||
catch(int error) {
|
||||
delete (file_chfa);
|
||||
delete (policy_chfa);
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
delete file_chfa;
|
||||
delete policy_chfa;
|
||||
|
||||
/* write blob to buffer */
|
||||
stringbuf *buf = stream.rdbuf();
|
||||
|
||||
buf->pubseekpos(0);
|
||||
*size = buf->in_avail();
|
||||
if (file_min_len < *min_match_len)
|
||||
*min_match_len = file_min_len;
|
||||
|
||||
char *buffer = (char *)malloc(*size);
|
||||
if (!buffer)
|
||||
return NULL;
|
||||
buf->sgetn(buffer, *size);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
@ -123,17 +123,11 @@ class aare_rules {
|
||||
CHFA *create_chfa(int *min_match_len,
|
||||
std::vector <aa_perms> &perms_table,
|
||||
optflags const &opts, bool filedfa,
|
||||
bool extended_perms, bool prompt);
|
||||
bool extended_perms);
|
||||
void *create_dfablob(size_t *size, int *min_match_len,
|
||||
std::vector <aa_perms> &perms_table,
|
||||
optflags const &opts,
|
||||
bool filedfa, bool extended_perms, bool prompt);
|
||||
void *create_welded_dfablob(aare_rules *file_rules,
|
||||
size_t *size, int *min_match_len,
|
||||
size_t *new_start,
|
||||
std::vector <aa_perms> &perms_table,
|
||||
optflags const &opts,
|
||||
bool extended_perms, bool prompt);
|
||||
bool filedfa, bool extended_perms);
|
||||
};
|
||||
|
||||
#endif /* __LIBAA_RE_RULES_H */
|
||||
|
@ -59,7 +59,7 @@ void CHFA::init_free_list(vector<pair<size_t, size_t> > &free_list,
|
||||
* permtable index flag
|
||||
*/
|
||||
CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
|
||||
bool permindex, bool prompt): eq(eq)
|
||||
bool permindex): eq(eq)
|
||||
{
|
||||
if (opts.dump & DUMP_DFA_TRANS_PROGRESS)
|
||||
fprintf(stderr, "Compressing HFA:\r");
|
||||
@ -118,12 +118,10 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
|
||||
accept2.resize(max(dfa.states.size(), (size_t) 2));
|
||||
dfa.nonmatching->map_perms_to_accept(accept[0],
|
||||
accept2[0],
|
||||
accept3,
|
||||
prompt);
|
||||
accept3);
|
||||
dfa.start->map_perms_to_accept(accept[1],
|
||||
accept2[1],
|
||||
accept3,
|
||||
prompt);
|
||||
accept3);
|
||||
}
|
||||
next_check.resize(max(optimal, (size_t) dfa.max_range));
|
||||
free_list.resize(next_check.size());
|
||||
@ -147,8 +145,7 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
|
||||
else
|
||||
(*i)->map_perms_to_accept(accept[num.size()],
|
||||
accept2[num.size()],
|
||||
accept3,
|
||||
prompt);
|
||||
accept3);
|
||||
num.insert(make_pair(*i, num.size()));
|
||||
}
|
||||
if (opts.dump & (DUMP_DFA_TRANS_PROGRESS)) {
|
||||
@ -170,8 +167,7 @@ CHFA::CHFA(DFA &dfa, map<transchar, transchar> &eq, optflags const &opts,
|
||||
else
|
||||
i->second->map_perms_to_accept(accept[num.size()],
|
||||
accept2[num.size()],
|
||||
accept3,
|
||||
prompt);
|
||||
accept3);
|
||||
num.insert(make_pair(i->second, num.size()));
|
||||
}
|
||||
if (opts.dump & (DUMP_DFA_TRANS_PROGRESS)) {
|
||||
@ -519,116 +515,3 @@ void CHFA::flex_table(ostream &os, optflags const &opts) {
|
||||
flex_table_serialize<uint16_t>(*this, os, (1 << 16) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @file_chfa: chfa to add on to the policy chfa
|
||||
* @new_start: new start state for where the @file_dfa is in the new chfa
|
||||
*
|
||||
* Make a new chfa that is a combination of policy and file chfas. It
|
||||
* assumes policy is built with AA_CLASS_FILE support transition. The
|
||||
* resultant chfa will have file states and indexes offset except for
|
||||
* start and null states.
|
||||
*
|
||||
* NOTE:
|
||||
* - modifies chfa
|
||||
* requires:
|
||||
* - no ec
|
||||
* - policy chfa has transitions state[start].next[AA_CLASS_FILE]
|
||||
* - policy perms table is build if using permstable
|
||||
|
||||
*/
|
||||
void CHFA::weld_file_to_policy(CHFA &file_chfa, size_t &new_start,
|
||||
bool accept_idx, bool prompt,
|
||||
vector <aa_perms> &policy_perms,
|
||||
vector <aa_perms> &file_perms)
|
||||
{
|
||||
// doesn't support remapping eq classes yet
|
||||
if (eq.size() > 0 || file_chfa.eq.size() > 0)
|
||||
throw 1;
|
||||
|
||||
size_t old_base_size = default_base.size();
|
||||
size_t old_next_size = next_check.size();
|
||||
|
||||
const State *nonmatching = default_base[0].first;
|
||||
//const State *start = default_base[1].first;
|
||||
const State *file_nonmatching = file_chfa.default_base[0].first;
|
||||
|
||||
// renumber states from file_dfa by appending to policy dfa
|
||||
num.insert(make_pair(file_nonmatching, 0)); // remap to policy nonmatching
|
||||
for (map<const State *, size_t>::iterator i = file_chfa.num.begin(); i != file_chfa.num.end() ; i++) {
|
||||
if (i->first == file_nonmatching)
|
||||
continue;
|
||||
num.insert(make_pair(i->first, i->second + old_base_size));
|
||||
}
|
||||
|
||||
// handle default and base table expansion, and setup renumbering
|
||||
// while we remap file_nonmatch within the table, we still keep its
|
||||
// slot.
|
||||
bool first = true;
|
||||
for (DefaultBase::iterator i = file_chfa.default_base.begin(); i != file_chfa.default_base.end(); i++) {
|
||||
const State *def;
|
||||
size_t base;
|
||||
if (first) {
|
||||
first = false;
|
||||
// remap file_nonmatch to nonmatch
|
||||
def = nonmatching;
|
||||
base = 0;
|
||||
} else {
|
||||
def = i->first;
|
||||
base = i->second + old_next_size;
|
||||
}
|
||||
default_base.push_back(make_pair(def, base));
|
||||
}
|
||||
|
||||
// mapping for these are handled by num[]
|
||||
for (NextCheck::iterator i = file_chfa.next_check.begin(); i != file_chfa.next_check.end(); i++) {
|
||||
next_check.push_back(*i);
|
||||
}
|
||||
|
||||
// append file perms to policy perms, and rework permsidx if needed
|
||||
if (accept_idx) {
|
||||
// policy idx double
|
||||
// file + doubled offset
|
||||
// Requires: policy perms table, so we can double and
|
||||
// update indexes
|
||||
// * file perm idx to start on even idx
|
||||
// * policy perms table size to double and entries
|
||||
// to repeat
|
||||
assert(accept.size() == old_base_size);
|
||||
accept.resize(accept.size() + file_chfa.accept.size());
|
||||
assert(policy_perms.size() < std::numeric_limits<ssize_t>::max());
|
||||
ssize_t size = (ssize_t) policy_perms.size();
|
||||
policy_perms.resize(size*2 + file_perms.size());
|
||||
// shift and double the policy perms
|
||||
for (ssize_t i = size - 1; i >= 0; i--) {
|
||||
policy_perms[i*2] = policy_perms[i];
|
||||
policy_perms[i*2 + 1] = policy_perms[i];
|
||||
}
|
||||
// update policy accept idx for the new shifted perms table
|
||||
for (size_t i = 0; i < old_base_size; i++) {
|
||||
accept[i] = accept[i]*2;
|
||||
}
|
||||
// copy over file perms
|
||||
for (size_t i = 0; i < file_perms.size(); i++) {
|
||||
policy_perms[size*2 + i] = file_perms[i];
|
||||
}
|
||||
// shift file accept indexs
|
||||
for (size_t i = 0; i < file_chfa.accept.size(); i++) {
|
||||
accept[old_base_size + i] = file_chfa.accept[i] + size*2;
|
||||
}
|
||||
} else {
|
||||
// perms are stored in accept just append the perms
|
||||
size_t size = accept.size();
|
||||
accept.resize(size + file_chfa.accept.size());
|
||||
accept2.resize(size + file_chfa.accept.size());
|
||||
for (size_t i = 0; i < file_chfa.accept.size(); i++) {
|
||||
accept[size + i] = file_chfa.accept[i];
|
||||
accept2[size + i] = file_chfa.accept2[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Rework transition state[start].next[AA_CLASS_FILE]
|
||||
next_check[default_base[1].second + AA_CLASS_FILE].first = file_chfa.start;
|
||||
|
||||
new_start = num[file_chfa.start];
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class CHFA {
|
||||
public:
|
||||
CHFA(void);
|
||||
CHFA(DFA &dfa, std::map<transchar, transchar> &eq, optflags const &opts,
|
||||
bool permindex, bool prompt);
|
||||
bool permindex);
|
||||
void dump(ostream & os);
|
||||
void flex_table(ostream &os, optflags const &opts);
|
||||
void init_free_list(std::vector<std::pair<size_t, size_t> > &free_list,
|
||||
@ -48,10 +48,6 @@ class CHFA {
|
||||
StateTrans &cases);
|
||||
void insert_state(std::vector<std::pair<size_t, size_t> > &free_list,
|
||||
State *state, DFA &dfa);
|
||||
void weld_file_to_policy(CHFA &file_chfa, size_t &new_start,
|
||||
bool accept_idx, bool prompt,
|
||||
std::vector <aa_perms> &policy_perms,
|
||||
std::vector <aa_perms> &file_perms);
|
||||
|
||||
// private:
|
||||
// sigh templates suck, friend declaration does not work so for now
|
||||
|
@ -334,7 +334,16 @@ State *DFA::add_new_state(optflags const &opts, NodeSet *anodes,
|
||||
|
||||
ProtoState proto;
|
||||
proto.init(nnodev, anodev);
|
||||
State *state = new State(opts, node_map.size(), proto, other, filedfa);
|
||||
State *state;
|
||||
try {
|
||||
state = new State(opts, node_map.size(), proto, other, filedfa);
|
||||
} catch(int error) {
|
||||
/* this function is called in the DFA object creation,
|
||||
* and the exception prevents the destructor from
|
||||
* being called, so call the helper here */
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
pair<NodeMap::iterator,bool> x = node_map.insert(proto, state);
|
||||
if (x.second == false) {
|
||||
delete state;
|
||||
@ -392,7 +401,17 @@ void DFA::update_state_transitions(optflags const &opts, State *state)
|
||||
*/
|
||||
for (Cases::iterator j = cases.begin(); j != cases.end(); j++) {
|
||||
State *target;
|
||||
target = add_new_state(opts, j->second, nonmatching);
|
||||
try {
|
||||
target = add_new_state(opts, j->second, nonmatching);
|
||||
} catch (int error) {
|
||||
/* when add_new_state fails, there could still
|
||||
* be NodeSets in the rest of cases, so clean
|
||||
* them up before re-throwing the exception */
|
||||
for (Cases::iterator k = ++j; k != cases.end(); k++) {
|
||||
delete k->second;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
/* Don't insert transition that the otherwise transition
|
||||
* already covers
|
||||
@ -522,11 +541,7 @@ DFA::DFA(Node *root, optflags const &opts, bool buildfiledfa): root(root), filed
|
||||
|
||||
DFA::~DFA()
|
||||
{
|
||||
anodes_cache.clear();
|
||||
nnodes_cache.clear();
|
||||
|
||||
for (Partition::iterator i = states.begin(); i != states.end(); i++)
|
||||
delete *i;
|
||||
cleanup();
|
||||
}
|
||||
|
||||
State *DFA::match_len(State *state, const char *str, size_t len)
|
||||
@ -1367,13 +1382,12 @@ void DFA::apply_equivalence_classes(map<transchar, transchar> &eq)
|
||||
}
|
||||
|
||||
void DFA::compute_perms_table_ent(State *state, size_t pos,
|
||||
vector <aa_perms> &perms_table,
|
||||
bool prompt)
|
||||
vector <aa_perms> &perms_table)
|
||||
{
|
||||
uint32_t accept1, accept2, accept3;
|
||||
|
||||
// until front end doesn't map the way it does
|
||||
state->map_perms_to_accept(accept1, accept2, accept3, prompt);
|
||||
state->map_perms_to_accept(accept1, accept2, accept3);
|
||||
if (filedfa) {
|
||||
state->idx = pos * 2;
|
||||
perms_table[pos*2] = compute_fperms_user(accept1, accept2, accept3);
|
||||
@ -1384,7 +1398,7 @@ void DFA::compute_perms_table_ent(State *state, size_t pos,
|
||||
}
|
||||
}
|
||||
|
||||
void DFA::compute_perms_table(vector <aa_perms> &perms_table, bool prompt)
|
||||
void DFA::compute_perms_table(vector <aa_perms> &perms_table)
|
||||
{
|
||||
size_t mult = filedfa ? 2 : 1;
|
||||
size_t pos = 2;
|
||||
@ -1393,13 +1407,13 @@ void DFA::compute_perms_table(vector <aa_perms> &perms_table, bool prompt)
|
||||
perms_table.resize(states.size() * mult);
|
||||
|
||||
// nonmatching and start need to be 0 and 1 so handle outside of loop
|
||||
compute_perms_table_ent(nonmatching, 0, perms_table, prompt);
|
||||
compute_perms_table_ent(start, 1, perms_table, prompt);
|
||||
compute_perms_table_ent(nonmatching, 0, perms_table);
|
||||
compute_perms_table_ent(start, 1, perms_table);
|
||||
|
||||
for (Partition::iterator i = states.begin(); i != states.end(); i++) {
|
||||
if (*i == nonmatching || *i == start)
|
||||
continue;
|
||||
compute_perms_table_ent(*i, pos, perms_table, prompt);
|
||||
compute_perms_table_ent(*i, pos, perms_table);
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
@ -289,13 +289,10 @@ public:
|
||||
|
||||
int apply_and_clear_deny(void) { return perms.apply_and_clear_deny(); }
|
||||
void map_perms_to_accept(perm32_t &accept1, perm32_t &accept2,
|
||||
perm32_t &accept3, bool prompt)
|
||||
perm32_t &accept3)
|
||||
{
|
||||
accept1 = perms.allow;
|
||||
if (prompt && prompt_compat_mode == PROMPT_COMPAT_DEV)
|
||||
accept2 = PACK_AUDIT_CTL(perms.prompt, perms.quiet);
|
||||
else
|
||||
accept2 = PACK_AUDIT_CTL(perms.audit, perms.quiet);
|
||||
accept2 = PACK_AUDIT_CTL(perms.audit, perms.quiet);
|
||||
accept3 = perms.prompt;
|
||||
}
|
||||
|
||||
@ -371,6 +368,15 @@ class DFA {
|
||||
NodeMap node_map;
|
||||
std::list<State *> work_queue;
|
||||
|
||||
void cleanup(void) {
|
||||
anodes_cache.clear();
|
||||
nnodes_cache.clear();
|
||||
|
||||
for (Partition::iterator i = states.begin(); i != states.end(); i++) {
|
||||
delete *i;
|
||||
}
|
||||
states.clear();
|
||||
}
|
||||
public:
|
||||
DFA(Node *root, optflags const &flags, bool filedfa);
|
||||
virtual ~DFA();
|
||||
@ -399,10 +405,8 @@ public:
|
||||
void apply_equivalence_classes(std::map<transchar, transchar> &eq);
|
||||
|
||||
void compute_perms_table_ent(State *state, size_t pos,
|
||||
std::vector <aa_perms> &perms_table,
|
||||
bool prompt);
|
||||
void compute_perms_table(std::vector <aa_perms> &perms_table,
|
||||
bool prompt);
|
||||
std::vector <aa_perms> &perms_table);
|
||||
void compute_perms_table(std::vector <aa_perms> &perms_table);
|
||||
|
||||
unsigned int diffcount;
|
||||
int oob_range;
|
||||
|
@ -133,8 +133,7 @@ struct aa_perms compute_fperms_user(uint32_t accept1, uint32_t accept2,
|
||||
perms.prompt = map_old_perms(dfa_user_allow(accept3));
|
||||
perms.audit = map_old_perms(dfa_user_audit(accept1, accept2));
|
||||
perms.quiet = map_old_perms(dfa_user_quiet(accept1, accept2));
|
||||
if (prompt_compat_mode != PROMPT_COMPAT_PERMSV1)
|
||||
perms.xindex = dfa_user_xindex(accept1);
|
||||
perms.xindex = dfa_user_xindex(accept1);
|
||||
|
||||
compute_fperms_allow(&perms, accept1);
|
||||
perms.prompt &= ~(perms.allow | perms.deny);
|
||||
@ -150,8 +149,7 @@ struct aa_perms compute_fperms_other(uint32_t accept1, uint32_t accept2,
|
||||
perms.prompt = map_old_perms(dfa_other_allow(accept3));
|
||||
perms.audit = map_old_perms(dfa_other_audit(accept1, accept2));
|
||||
perms.quiet = map_old_perms(dfa_other_quiet(accept1, accept2));
|
||||
if (prompt_compat_mode != PROMPT_COMPAT_PERMSV1)
|
||||
perms.xindex = dfa_other_xindex(accept1);
|
||||
perms.xindex = dfa_other_xindex(accept1);
|
||||
|
||||
compute_fperms_allow(&perms, accept1);
|
||||
perms.prompt &= ~(perms.allow | perms.deny);
|
||||
@ -165,12 +163,6 @@ static uint32_t map_other(uint32_t x)
|
||||
((x & 0x60) << 19); /* SETOPT/GETOPT */
|
||||
}
|
||||
|
||||
static uint32_t map_xbits(uint32_t x)
|
||||
{
|
||||
return ((x & 0x1) << 7) |
|
||||
((x & 0x7e) << 9);
|
||||
}
|
||||
|
||||
struct aa_perms compute_perms_entry(uint32_t accept1, uint32_t accept2,
|
||||
uint32_t accept3)
|
||||
// don't need to worry about version internally within the parser
|
||||
|
@ -570,6 +570,8 @@ ostream &mnt_rule::dump(ostream &os)
|
||||
{
|
||||
prefix_rule_t::dump(os);
|
||||
|
||||
std::ios::fmtflags fmt(os.flags());
|
||||
|
||||
if (perms & AA_MAY_MOUNT)
|
||||
os << "mount";
|
||||
else if (perms & AA_MAY_UMOUNT)
|
||||
@ -603,6 +605,7 @@ ostream &mnt_rule::dump(ostream &os)
|
||||
os << " " << "(0x" << hex << perms << "/0x" << (audit != AUDIT_UNSPECIFIED ? perms : 0) << ")";
|
||||
os << ",\n";
|
||||
|
||||
os.flags(fmt);
|
||||
return os;
|
||||
}
|
||||
|
||||
@ -1163,21 +1166,7 @@ fail:
|
||||
void mnt_rule::post_parse_profile(Profile &prof)
|
||||
{
|
||||
if (trans) {
|
||||
perm32_t perms = 0;
|
||||
int n = add_entry_to_x_table(&prof, trans);
|
||||
if (!n) {
|
||||
PERROR("Profile %s has too many specified profile transitions.\n", prof.name);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (perms & AA_USER_EXEC)
|
||||
perms |= SHIFT_PERMS(n << 10, AA_USER_SHIFT);
|
||||
if (perms & AA_OTHER_EXEC)
|
||||
perms |= SHIFT_PERMS(n << 10, AA_OTHER_SHIFT);
|
||||
perms = ((perms & ~AA_ALL_EXEC_MODIFIERS) |
|
||||
(perms & AA_ALL_EXEC_MODIFIERS));
|
||||
|
||||
trans = NULL;
|
||||
/* TODO: pivot_root profile transition */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,8 +133,10 @@ static void process_entries(const void *nodep, VISIT value, int level unused)
|
||||
if (entry->link_name &&
|
||||
strncmp((*t)->from, entry->link_name, len) == 0) {
|
||||
char *n = do_alias(*t, entry->link_name);
|
||||
if (!n)
|
||||
if (!n) {
|
||||
free_cod_entries(dup);
|
||||
return;
|
||||
}
|
||||
if (!dup)
|
||||
dup = copy_cod_entry(entry);
|
||||
free(dup->link_name);
|
||||
|
@ -185,19 +185,9 @@ bool prompt_compat_mode_supported(int mode)
|
||||
if (mode == PROMPT_COMPAT_PERMSV2 &&
|
||||
(kernel_supports_permstable32 && !kernel_supports_permstable32_v1))
|
||||
return true;
|
||||
/*
|
||||
else if (mode == PROMPT_COMPAT_DEV &&
|
||||
kernel_supports_promptdev)
|
||||
return true;
|
||||
*/
|
||||
else if (mode == PROMPT_COMPAT_FLAG &&
|
||||
kernel_supports_permstable32)
|
||||
return true;
|
||||
/*
|
||||
else if (mode == PROMPT_COMPAT_PERMSV1 &&
|
||||
(kernel_supports_permstable32_v1))
|
||||
return true;
|
||||
*/
|
||||
else if (mode == PROMPT_COMPAT_IGNORE)
|
||||
return true;
|
||||
|
||||
@ -208,12 +198,8 @@ int default_prompt_compat_mode()
|
||||
{
|
||||
if (prompt_compat_mode_supported(PROMPT_COMPAT_PERMSV2))
|
||||
return PROMPT_COMPAT_PERMSV2;
|
||||
if (prompt_compat_mode_supported(PROMPT_COMPAT_DEV))
|
||||
return PROMPT_COMPAT_DEV;
|
||||
if (prompt_compat_mode_supported(PROMPT_COMPAT_FLAG))
|
||||
return PROMPT_COMPAT_FLAG;
|
||||
if (prompt_compat_mode_supported(PROMPT_COMPAT_PERMSV1))
|
||||
return PROMPT_COMPAT_PERMSV1;
|
||||
if (prompt_compat_mode_supported(PROMPT_COMPAT_IGNORE))
|
||||
return PROMPT_COMPAT_IGNORE;
|
||||
return PROMPT_COMPAT_IGNORE;
|
||||
@ -231,12 +217,6 @@ void print_prompt_compat_mode(FILE *f)
|
||||
case PROMPT_COMPAT_PERMSV2:
|
||||
fprintf(f, "permsv2");
|
||||
break;
|
||||
case PROMPT_COMPAT_PERMSV1:
|
||||
fprintf(f, "permsv1");
|
||||
break;
|
||||
case PROMPT_COMPAT_DEV:
|
||||
fprintf(stderr, "dev");
|
||||
break;
|
||||
default:
|
||||
fprintf(f, "Unknown prompt compat mode '%d'", prompt_compat_mode);
|
||||
}
|
||||
|
@ -384,13 +384,11 @@ void sd_serialize_rlimits(std::ostringstream &buf, struct aa_rlimits *limits)
|
||||
sd_write_structend(buf);
|
||||
}
|
||||
|
||||
void sd_serialize_xtable(std::ostringstream &buf, char **table,
|
||||
size_t min_size)
|
||||
void sd_serialize_xtable(std::ostringstream &buf, char **table)
|
||||
{
|
||||
size_t count;
|
||||
size_t size;
|
||||
|
||||
if (!table[4] && min_size == 0)
|
||||
if (!table[4])
|
||||
return;
|
||||
sd_write_struct(buf, "xtable");
|
||||
count = 0;
|
||||
@ -399,9 +397,7 @@ void sd_serialize_xtable(std::ostringstream &buf, char **table,
|
||||
count++;
|
||||
}
|
||||
|
||||
size = max(min_size, count);
|
||||
|
||||
sd_write_array(buf, NULL, size);
|
||||
sd_write_array(buf, NULL, count);
|
||||
for (size_t i = 4; i < count + 4; i++) {
|
||||
size_t len = strlen(table[i]) + 1;
|
||||
|
||||
@ -414,13 +410,6 @@ void sd_serialize_xtable(std::ostringstream &buf, char **table,
|
||||
}
|
||||
sd_write_strn(buf, table[i], len, NULL);
|
||||
}
|
||||
if (min_size > count) {
|
||||
//fprintf(stderr, "Adding padding to xtable count %lu, min %lu\n", count, min_size);
|
||||
for (; count < min_size; count++) {
|
||||
/* fill with null strings */
|
||||
sd_write_strn(buf, "\000", 1, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
sd_write_arrayend(buf);
|
||||
sd_write_structend(buf);
|
||||
@ -554,38 +543,17 @@ void sd_serialize_profile(std::ostringstream &buf, Profile *profile,
|
||||
sd_serialize_dfa(buf, profile->policy.dfa, profile->policy.size,
|
||||
profile->policy.perms_table);
|
||||
if (kernel_supports_permstable32) {
|
||||
sd_serialize_xtable(buf, profile->exec_table,
|
||||
profile->uses_prompt_rules &&
|
||||
prompt_compat_mode == PROMPT_COMPAT_PERMSV1 ?
|
||||
profile->policy.perms_table.size() : 0);
|
||||
sd_serialize_xtable(buf, profile->exec_table);
|
||||
|
||||
}
|
||||
sd_write_structend(buf);
|
||||
}
|
||||
|
||||
/* either have a single dfa or lists of different entry types */
|
||||
if (profile->uses_prompt_rules && prompt_compat_mode == PROMPT_COMPAT_PERMSV1) {
|
||||
/* special compat mode to work around verification problem */
|
||||
sd_serialize_dfa(buf, profile->policy.dfa, profile->policy.size,
|
||||
profile->policy.perms_table);
|
||||
sd_write_name(buf, "dfa_start");
|
||||
sd_write_uint32(buf, profile->policy.file_start);
|
||||
if (profile->policy.dfa) {
|
||||
// fprintf(stderr, "profile %s: policy xtable\n", profile->name);
|
||||
// TODO: this is dummy exec make dependent on V1
|
||||
sd_serialize_xtable(buf, profile->exec_table,
|
||||
//permstable32_v1 workaround
|
||||
profile->policy.perms_table.size());
|
||||
}
|
||||
} else {
|
||||
sd_serialize_dfa(buf, profile->dfa.dfa, profile->dfa.size,
|
||||
profile->dfa.perms_table);
|
||||
if (profile->dfa.dfa) {
|
||||
// fprintf(stderr, "profile %s: dfa xtable\n", profile->name);
|
||||
sd_serialize_xtable(buf, profile->exec_table,
|
||||
//??? work around
|
||||
profile->dfa.perms_table.size());
|
||||
}
|
||||
sd_serialize_dfa(buf, profile->dfa.dfa, profile->dfa.size,
|
||||
profile->dfa.perms_table);
|
||||
if (profile->dfa.dfa) {
|
||||
// fprintf(stderr, "profile %s: dfa xtable\n", profile->name);
|
||||
sd_serialize_xtable(buf, profile->exec_table);
|
||||
}
|
||||
sd_write_structend(buf);
|
||||
}
|
||||
|
@ -797,12 +797,8 @@ static int process_arg(int c, char *optarg)
|
||||
case ARG_PROMPT_COMPAT:
|
||||
if (strcmp(optarg, "permsv2") == 0) {
|
||||
prompt_compat_mode = PROMPT_COMPAT_PERMSV2;
|
||||
} else if (strcmp(optarg, "permsv1") == 0) {
|
||||
prompt_compat_mode = PROMPT_COMPAT_PERMSV1;
|
||||
} else if (strcmp(optarg, "default") == 0) {
|
||||
prompt_compat_mode = default_prompt_compat_mode();
|
||||
} else if (strcmp(optarg, "dev") == 0) {
|
||||
prompt_compat_mode = PROMPT_COMPAT_DEV;
|
||||
} else if (strcmp(optarg, "ignore") == 0) {
|
||||
prompt_compat_mode = PROMPT_COMPAT_IGNORE;
|
||||
} else if (strcmp(optarg, "flag") == 0) {
|
||||
|
@ -244,10 +244,7 @@ int post_process_profile(Profile *profile, int debug_only)
|
||||
|
||||
error = post_process_policy_list(profile->hat_table, debug_only);
|
||||
|
||||
if (prompt_compat_mode == PROMPT_COMPAT_DEV && profile->uses_prompt_rules)
|
||||
profile->flags.flags |= FLAG_PROMPT_COMPAT;
|
||||
|
||||
else if (prompt_compat_mode == PROMPT_COMPAT_FLAG && profile->uses_prompt_rules)
|
||||
if (prompt_compat_mode == PROMPT_COMPAT_FLAG && profile->uses_prompt_rules)
|
||||
profile->flags.mode = MODE_PROMPT;
|
||||
|
||||
return error;
|
||||
|
@ -578,7 +578,7 @@ build:
|
||||
*
|
||||
* we don't need to build xmatch for permstable32, so don't
|
||||
*/
|
||||
prof->xmatch = rules->create_dfablob(&prof->xmatch_size, &prof->xmatch_len, prof->xmatch_perms_table, parseopts, false, false, false);
|
||||
prof->xmatch = rules->create_dfablob(&prof->xmatch_size, &prof->xmatch_len, prof->xmatch_perms_table, parseopts, false, false);
|
||||
delete rules;
|
||||
if (!prof->xmatch)
|
||||
return false;
|
||||
@ -785,28 +785,17 @@ int process_profile_regex(Profile *prof)
|
||||
/* under permstable32_v1 we weld file and policydb together, so
|
||||
* don't create the file blob here
|
||||
*/
|
||||
if (prof->dfa.rules->rule_count > 0 && prompt_compat_mode != PROMPT_COMPAT_PERMSV1) {
|
||||
if (prof->dfa.rules->rule_count > 0) {
|
||||
int xmatch_len = 0;
|
||||
//fprintf(stderr, "Creating file DFA %d\n", kernel_supports_permstable32);
|
||||
prof->dfa.dfa = prof->dfa.rules->create_dfablob(&prof->dfa.size,
|
||||
&xmatch_len, prof->dfa.perms_table,
|
||||
parseopts, true,
|
||||
kernel_supports_permstable32,
|
||||
prof->uses_prompt_rules);
|
||||
kernel_supports_permstable32);
|
||||
delete prof->dfa.rules;
|
||||
prof->dfa.rules = NULL;
|
||||
if (!prof->dfa.dfa)
|
||||
goto out;
|
||||
/*
|
||||
if (prof->dfa_size == 0) {
|
||||
PERROR(_("profile %s: has merged rules (%s) with "
|
||||
"multiple x modifiers\n"),
|
||||
prof->name, (char *) prof->dfa);
|
||||
free(prof->dfa);
|
||||
prof->dfa = NULL;
|
||||
goto out;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
error = 0;
|
||||
@ -1081,7 +1070,6 @@ static const char *mediates_ns = CLASS_STR(AA_CLASS_NS);
|
||||
static const char *mediates_posix_mqueue = CLASS_STR(AA_CLASS_POSIX_MQUEUE);
|
||||
static const char *mediates_sysv_mqueue = CLASS_STR(AA_CLASS_SYSV_MQUEUE);
|
||||
static const char *mediates_io_uring = CLASS_STR(AA_CLASS_IO_URING);
|
||||
static const char *deny_file = ".*";
|
||||
|
||||
/* Set the mediates priority to the maximum possible. This is to help
|
||||
* ensure that the mediates information is not wiped out by a rule
|
||||
@ -1164,44 +1152,13 @@ int process_profile_policydb(Profile *prof)
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (prompt_compat_mode == PROMPT_COMPAT_PERMSV1) {
|
||||
// MUST have file and policy
|
||||
// This requires file rule processing happen first
|
||||
if (!prof->dfa.rules->rule_count) {
|
||||
// add null dfa
|
||||
if (!prof->dfa.rules->add_rule(deny_file, 0, RULE_DENY, AA_MAY_READ, 0, parseopts))
|
||||
goto out;
|
||||
}
|
||||
if (!prof->policy.rules->rule_count) {
|
||||
if (!prof->policy.rules->add_rule(mediates_file, 0, RULE_DENY, AA_MAY_READ, 0, parseopts))
|
||||
goto out;
|
||||
}
|
||||
int xmatch_len = 0;
|
||||
prof->policy.dfa = prof->policy.rules->create_welded_dfablob(
|
||||
prof->dfa.rules,
|
||||
&prof->policy.size,
|
||||
&xmatch_len,
|
||||
&prof->policy.file_start,
|
||||
prof->policy.perms_table, parseopts,
|
||||
kernel_supports_permstable32_v1,
|
||||
prof->uses_prompt_rules);
|
||||
delete prof->policy.rules;
|
||||
delete prof->dfa.rules;
|
||||
prof->policy.rules = NULL;
|
||||
prof->dfa.rules = NULL;
|
||||
if (!prof->policy.dfa)
|
||||
goto out;
|
||||
} else if (prof->policy.rules->rule_count > 0 &&
|
||||
// yes not needed as covered above, just making sure
|
||||
// this doesn't get messed up in the future
|
||||
prompt_compat_mode != PROMPT_COMPAT_PERMSV1) {
|
||||
if (prof->policy.rules->rule_count > 0) {
|
||||
int xmatch_len = 0;
|
||||
prof->policy.dfa = prof->policy.rules->create_dfablob(&prof->policy.size,
|
||||
&xmatch_len,
|
||||
prof->policy.perms_table,
|
||||
parseopts, false,
|
||||
kernel_supports_permstable32,
|
||||
prof->uses_prompt_rules);
|
||||
kernel_supports_permstable32);
|
||||
delete prof->policy.rules;
|
||||
|
||||
prof->policy.rules = NULL;
|
||||
|
@ -188,24 +188,21 @@ cleanup:
|
||||
if (prof->attachment) {
|
||||
tmp = symtab::delete_var(PROFILE_EXEC_VAR);
|
||||
delete tmp;
|
||||
if (saved_exec_path) {
|
||||
if (saved_exec_path)
|
||||
symtab::add_var(*saved_exec_path);
|
||||
delete saved_exec_path;
|
||||
}
|
||||
}
|
||||
cleanup_attach:
|
||||
if (prof->attachment) {
|
||||
tmp = symtab::delete_var(PROFILE_ATTACH_VAR);
|
||||
delete tmp;
|
||||
if (saved_attach_path) {
|
||||
if (saved_attach_path)
|
||||
symtab::add_var(*saved_attach_path);
|
||||
delete saved_attach_path;
|
||||
}
|
||||
}
|
||||
cleanup_name:
|
||||
tmp = symtab::delete_var(PROFILE_NAME_VARIABLE);
|
||||
delete tmp;
|
||||
|
||||
delete saved_exec_path;
|
||||
delete saved_attach_path;
|
||||
out:
|
||||
return error;
|
||||
}
|
||||
|
@ -577,6 +577,7 @@ flags: opt_flags TOK_OPENPAREN flagvals TOK_CLOSEPAREN
|
||||
flagvals: flagvals flagval
|
||||
{
|
||||
$1.merge($2);
|
||||
$2.clear();
|
||||
$$ = $1;
|
||||
};
|
||||
|
||||
|
@ -78,6 +78,7 @@ void ProfileList::dump_profile_names(bool children)
|
||||
Profile::~Profile()
|
||||
{
|
||||
hat_table.clear();
|
||||
flags.clear();
|
||||
free_cod_entries(entries);
|
||||
free_cond_entry_list(xattrs);
|
||||
|
||||
@ -97,10 +98,6 @@ Profile::~Profile()
|
||||
free(name);
|
||||
if (attachment)
|
||||
free(attachment);
|
||||
if (flags.disconnected_path)
|
||||
free(flags.disconnected_path);
|
||||
if (flags.disconnected_ipc)
|
||||
free(flags.disconnected_ipc);
|
||||
if (ns)
|
||||
free(ns);
|
||||
for (int i = (AA_EXEC_LOCAL >> 10) + 1; i < AA_EXEC_COUNT; i++)
|
||||
|
@ -175,6 +175,12 @@ public:
|
||||
signal = 0;
|
||||
error = 0;
|
||||
}
|
||||
|
||||
void clear(void) {
|
||||
free(disconnected_path);
|
||||
free(disconnected_ipc);
|
||||
}
|
||||
|
||||
void init(const char *str)
|
||||
{
|
||||
init();
|
||||
@ -301,7 +307,7 @@ public:
|
||||
}
|
||||
// same ignore rhs.disconnect_path
|
||||
} else {
|
||||
disconnected_path = rhs.disconnected_path;
|
||||
disconnected_path = strdup(rhs.disconnected_path);
|
||||
}
|
||||
}
|
||||
if (rhs.disconnected_ipc) {
|
||||
@ -311,7 +317,7 @@ public:
|
||||
}
|
||||
// same so do nothing
|
||||
} else {
|
||||
disconnected_ipc = rhs.disconnected_ipc;
|
||||
disconnected_ipc = strdup(rhs.disconnected_ipc);
|
||||
}
|
||||
}
|
||||
if (rhs.signal) {
|
||||
|
@ -28,9 +28,7 @@
|
||||
#define PROMPT_COMPAT_UNKNOWN 0
|
||||
#define PROMPT_COMPAT_IGNORE 1
|
||||
#define PROMPT_COMPAT_PERMSV2 2
|
||||
#define PROMPT_COMPAT_DEV 3
|
||||
#define PROMPT_COMPAT_FLAG 4
|
||||
#define PROMPT_COMPAT_PERMSV1 5
|
||||
|
||||
|
||||
class Profile;
|
||||
@ -433,11 +431,14 @@ public:
|
||||
ostream &dump(ostream &os) override {
|
||||
class_rule_t::dump(os);
|
||||
|
||||
std::ios::fmtflags fmt(os.flags());
|
||||
|
||||
if (saved)
|
||||
os << "(0x" << std::hex << perms << "/orig " << saved << ") ";
|
||||
else
|
||||
os << "(0x" << std::hex << perms << ") ";
|
||||
|
||||
os.flags(fmt);
|
||||
return os;
|
||||
}
|
||||
|
||||
@ -462,7 +463,11 @@ public:
|
||||
ostream &dump(ostream &os) override {
|
||||
class_rule_t::dump(os);
|
||||
|
||||
std::ios::fmtflags fmt(os.flags());
|
||||
|
||||
os << "(0x" << std::hex << perms << ") ";
|
||||
|
||||
os.flags(fmt);
|
||||
return os;
|
||||
}
|
||||
|
||||
|
7
parser/tst/simple_tests/vars/vars_file_evaluation_17.sd
Normal file
7
parser/tst/simple_tests/vars/vars_file_evaluation_17.sd
Normal file
@ -0,0 +1,7 @@
|
||||
#=DESCRIPTION expansion of alternation after extra, unescaped @
|
||||
#=EXRESULT PASS
|
||||
@{uid} = {[0-9],[1-9][0-9]}
|
||||
|
||||
/usr/bin/foo {
|
||||
/sys/fs/cgroup/user.slice/user-@{uid}.slice/user@@{uid}.service/cpu.max r,
|
||||
}
|
@ -189,11 +189,23 @@ static void trim_trailing_slash(std::string& str)
|
||||
str.clear(); // str is all '/'
|
||||
}
|
||||
|
||||
int copy_value_to_name(const std::string& value, char **name)
|
||||
{
|
||||
free(*name);
|
||||
*name = strdup(value.c_str());
|
||||
if (!*name) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int variable::expand_by_alternation(char **name)
|
||||
{
|
||||
std::string expanded_name = "";
|
||||
bool filter_leading_slash = false;
|
||||
bool filter_trailing_slash = false;
|
||||
int ret = 0;
|
||||
|
||||
if (!name) {
|
||||
PERROR("ASSERT: name to be expanded cannot be NULL\n");
|
||||
@ -226,8 +238,6 @@ int variable::expand_by_alternation(char **name)
|
||||
return 1;
|
||||
}
|
||||
|
||||
free(*name);
|
||||
|
||||
size_t setsize = ref->expanded.size();
|
||||
auto i = ref->expanded.begin();
|
||||
|
||||
@ -252,15 +262,19 @@ int variable::expand_by_alternation(char **name)
|
||||
if (setsize > 1) {
|
||||
expanded_name += "}";
|
||||
}
|
||||
|
||||
expanded_name = prefix + expanded_name + suffix;
|
||||
*name = strdup(expanded_name.c_str());
|
||||
if (!*name) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
/* don't include prefix */
|
||||
expanded_name = expanded_name + suffix;
|
||||
ret = copy_value_to_name(expanded_name, name);
|
||||
if (ret)
|
||||
return ret;
|
||||
/* recursive until no variables are found in *name */
|
||||
return expand_by_alternation(name);
|
||||
ret = expand_by_alternation(name);
|
||||
if (ret == 0) {
|
||||
/* return prefix to name */
|
||||
expanded_name = prefix + std::string(*name);
|
||||
ret = copy_value_to_name(expanded_name, name);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int variable::expand_variable()
|
||||
@ -293,6 +307,7 @@ int variable::expand_variable()
|
||||
}
|
||||
name = variable::process_var(var.c_str());
|
||||
variable *ref = symtab::lookup_existing_symbol(name);
|
||||
free(name);
|
||||
if (!ref) {
|
||||
PERROR("Failed to find declaration for: %s\n", var.c_str());
|
||||
rc = 1;
|
||||
@ -322,7 +337,6 @@ int variable::expand_variable()
|
||||
}
|
||||
|
||||
out:
|
||||
free(name);
|
||||
expanding = false;
|
||||
return rc;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
abi <abi/4.0>,
|
||||
include <tunables/global>
|
||||
|
||||
profile QtWebEngineProcess /usr/lib/@{multiarch}/qt{5,6}/libexec/QtWebEngineProcess flags=(unconfined) {
|
||||
profile QtWebEngineProcess /usr/lib{,64,exec}/{,@{multiarch}/}qt{,5,6}/{,libexec/}QtWebEngineProcess flags=(unconfined) {
|
||||
userns,
|
||||
@{exec_path} mr,
|
||||
|
||||
|
@ -35,6 +35,10 @@
|
||||
owner @{HOME}/.gtkrc r,
|
||||
owner @{HOME}/.gtkrc-2.0 r,
|
||||
owner @{HOME}/.gtk-bookmarks r,
|
||||
|
||||
owner @{HOME}/.cache/gtk-4.0/ rw,
|
||||
owner @{HOME}/.cache/gtk-4.0/vulkan-pipeline-cache/{,*} rw,
|
||||
|
||||
owner @{HOME}/.config/gtkrc r,
|
||||
owner @{HOME}/.config/gtkrc-2.0 r,
|
||||
owner @{HOME}/.config/gtk-{3,4}.0/ rw,
|
||||
|
@ -11,12 +11,20 @@
|
||||
|
||||
abi <abi/4.0>,
|
||||
|
||||
# this abstract profile can be included by applications that are
|
||||
# dynamically linked to libnuma
|
||||
# This abstract profile can be included by applications that are
|
||||
# dynamically linked to libnuma.
|
||||
|
||||
# libnuma defines the function num_init() as the .init function
|
||||
# to be called by the runtime linker (ld) when libnuma is loaded
|
||||
# even if not any active usage of libnuma takes place
|
||||
|
||||
@{sys}/devices/system/cpu/node/ r,
|
||||
|
||||
# Actually using libnuma functionality will need a few more
|
||||
# sysfs entries to gather information about the system
|
||||
@{sys}/devices/system/cpu/ r,
|
||||
@{sys}/devices/system/node/node[0-9]*/meminfo r,
|
||||
@{sys}/devices/system/node/*/cpumap r,
|
||||
|
||||
# Include additions to the abstraction
|
||||
include if exists <abstractions/libnuma.d>
|
||||
|
@ -25,6 +25,7 @@
|
||||
@{run}/systemd/userdb/io.systemd.Home rw, # systemd-home dirs
|
||||
@{run}/systemd/userdb/io.systemd.NameServiceSwitch rw, # UNIX/glibc NSS
|
||||
@{run}/systemd/userdb/io.systemd.Machine rw, # systemd-machined
|
||||
@{run}/systemd/userdb/org.gnome.DisplayManager rw, # GDM implements a user database for its greeter
|
||||
|
||||
@{PROC}/sys/kernel/random/boot_id r,
|
||||
|
||||
|
@ -27,6 +27,9 @@ profile curl /usr/bin/curl {
|
||||
# (see --config, --cacert options)
|
||||
file r @{HOME}/**,
|
||||
|
||||
# allow reading data/config from tmp
|
||||
owner file r /tmp/**,
|
||||
|
||||
# allow writing output to $HOME, /tmp (see -o option)
|
||||
file w @{HOME}/**,
|
||||
file w /tmp/**,
|
||||
|
67
profiles/apparmor.d/nginx
Normal file
67
profiles/apparmor.d/nginx
Normal file
@ -0,0 +1,67 @@
|
||||
#------------------------------------------------------------------
|
||||
# Copyright (C) 2025 Canonical Ltd.
|
||||
#
|
||||
# Author: Maxime Bélair <maxime.belair@canonical.com>
|
||||
#
|
||||
# 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.
|
||||
#------------------------------------------------------------------
|
||||
# vim: ft=apparmor
|
||||
|
||||
abi <abi/4.0>,
|
||||
|
||||
include <tunables/global>
|
||||
|
||||
# Standard config for webservers. This assumes that the server uses one of these directories.
|
||||
# If it has been modified, change accordingly.
|
||||
# Web directory location is available at /etc/nginx/sites-available/default
|
||||
@{srv}=/var/www/html/** /srv/**
|
||||
|
||||
profile nginx /usr/{s,}bin/nginx {
|
||||
include <abstractions/base>
|
||||
include <abstractions/nameservice-strict>
|
||||
include <abstractions/openssl>
|
||||
include <abstractions/ssl_keys>
|
||||
|
||||
capability dac_override,
|
||||
capability dac_read_search,
|
||||
capability net_bind_service,
|
||||
capability setgid,
|
||||
capability setuid,
|
||||
|
||||
network inet stream,
|
||||
network inet6 stream,
|
||||
network inet dgram,
|
||||
network inet6 dgram,
|
||||
network netlink raw,
|
||||
|
||||
# Configuration
|
||||
file /etc/nginx/** r,
|
||||
|
||||
# Server directory
|
||||
file @{srv} r,
|
||||
|
||||
# Support for modules, perl and lua
|
||||
file /usr/share/nginx/** r,
|
||||
file /usr/share/perl/*/**.pm r,
|
||||
file /usr/share/lua/*/**.lua r,
|
||||
|
||||
|
||||
# Temporary files
|
||||
owner file /tmp/** rw,
|
||||
|
||||
# nginx libs
|
||||
owner file /var/lib/nginx/** rw,
|
||||
|
||||
# logs
|
||||
file /var/log/nginx/* w,
|
||||
|
||||
# Binaries
|
||||
file @{exec_path} mr,
|
||||
owner file /run/nginx.pid rw,
|
||||
|
||||
# Site-specific additions and overrides. See local/README for details.
|
||||
include if exists <local/nginx>
|
||||
}
|
||||
|
@ -18,9 +18,7 @@ profile plasmashell /usr/bin/plasmashell {
|
||||
ptrace,
|
||||
|
||||
# allow executing QtWebEngineProcess with full permissions including userns (using profile stacking to avoid no_new_privs issues)
|
||||
/usr/lib/x86_64-linux-gnu/qt[56]/libexec/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
|
||||
/usr/libexec/qt[56]/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
|
||||
/usr/lib/qt6/libexec/QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
|
||||
priority=1 /usr/lib{,64,exec}/{,@{multiarch}/}qt{,5,6}/{,libexec/}QtWebEngineProcess cx -> &plasmashell//QtWebEngineProcess,
|
||||
|
||||
# allow to execute all other programs under their own profile, or to run unconfined
|
||||
/** pux,
|
||||
|
@ -35,6 +35,7 @@ profile wg-quick /usr/bin/wg-quick flags=(attach_disconnected) {
|
||||
file mrix /usr/sbin/xtables-nft-multi,
|
||||
file mrix /usr/bin/resolvectl,
|
||||
file mrix /usr/sbin/resolvconf,
|
||||
file PUx /usr/bin/systemd-creds,
|
||||
|
||||
# dbus access
|
||||
file rw @{run}/dbus/system_bus_socket,
|
||||
|
@ -144,6 +144,7 @@ SRC=access.c \
|
||||
getcon_verify.c \
|
||||
link.c \
|
||||
link_subset.c \
|
||||
linkat_tmpfile.c \
|
||||
mmap.c \
|
||||
mkdir.c \
|
||||
mount.c \
|
||||
@ -311,6 +312,7 @@ TESTS=aa_exec \
|
||||
i18n \
|
||||
link \
|
||||
link_subset \
|
||||
linkat_tmpfile \
|
||||
mkdir \
|
||||
mmap \
|
||||
mount \
|
||||
|
@ -85,6 +85,7 @@ int test_with_old_style_mount() {
|
||||
perror("FAIL: could not open executable file in shadowed dir");
|
||||
close(shadowed_file_fd);
|
||||
close(shadowing_dirfd);
|
||||
close(shadowed_dirfd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -137,6 +138,11 @@ int test_with_old_style_mount() {
|
||||
|
||||
DEBUG_PRINTF("Read from disconnected file\n");
|
||||
char *file_contents_buf = calloc(shadowed_file_size+1, sizeof(char));
|
||||
if (file_contents_buf == NULL) {
|
||||
perror("FAIL: could not allocate memory to read file");
|
||||
rc |= 1;
|
||||
goto cleanup_mount;
|
||||
}
|
||||
if (read(shadowed_file_fd, file_contents_buf, shadowed_file_size) == -1) {
|
||||
perror("FAIL: could not read from file after mount");
|
||||
rc |= 1;
|
||||
@ -164,6 +170,7 @@ int test_with_old_style_mount() {
|
||||
rc |= 1;
|
||||
}
|
||||
cleanup_fds:
|
||||
close(shadowed_exec_fd);
|
||||
close(shadowed_file_fd);
|
||||
close(shadowing_dirfd);
|
||||
close(shadowed_exec_fd);
|
||||
|
29
tests/regression/apparmor/linkat_tmpfile.c
Normal file
29
tests/regression/apparmor/linkat_tmpfile.c
Normal file
@ -0,0 +1,29 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2 && argc != 3) {
|
||||
fprintf(stderr, "FAIL: Usage: linkat_tmpfile tmpdir [final_location]\n");
|
||||
return 1;
|
||||
}
|
||||
int tmpfile_fd = open(argv[1], O_TMPFILE | O_WRONLY, S_IRUSR | S_IWUSR);
|
||||
if (tmpfile_fd == -1) {
|
||||
perror("FAIL: could not open tmpfile");
|
||||
return 1;
|
||||
}
|
||||
if (argc == 3) {
|
||||
int linkat_result = linkat(tmpfile_fd, "", AT_FDCWD, argv[2], AT_EMPTY_PATH);
|
||||
if (linkat_result == -1) {
|
||||
perror("FAIL: could not link tmpfile into final location");
|
||||
close(tmpfile_fd);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
close(tmpfile_fd);
|
||||
fprintf(stderr, "PASS\n");
|
||||
return 0;
|
||||
}
|
52
tests/regression/apparmor/linkat_tmpfile.sh
Normal file
52
tests/regression/apparmor/linkat_tmpfile.sh
Normal file
@ -0,0 +1,52 @@
|
||||
#! /bin/bash
|
||||
# Copyright (C) 2025 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License as
|
||||
# published by the Free Software Foundation, version 2 of the
|
||||
# License.
|
||||
|
||||
#=NAME linkat
|
||||
#=DESCRIPTION
|
||||
# Verifies that file creation with O_TMPFILE and linkat(2) is mediated correctly
|
||||
#=END
|
||||
|
||||
pwd=`dirname $0`
|
||||
pwd=`cd $pwd ; /bin/pwd`
|
||||
|
||||
bin=$pwd
|
||||
|
||||
. "$bin/prologue.inc"
|
||||
|
||||
tmpdir_nested=$tmpdir/nested
|
||||
tmpdir_nested_file=$tmpdir_nested/file
|
||||
tmpfile=$tmpdir/file
|
||||
|
||||
mkdir $tmpdir_nested
|
||||
|
||||
genprofile cap:dac_read_search
|
||||
runchecktest "linkat O_TMPFILE noperms" fail $tmpdir_nested
|
||||
runchecktest "linkat O_TMPFILE noperms, link" fail $tmpdir_nested $tmpfile
|
||||
|
||||
# Denial log entry for tmpfile is /path/#[6digits]
|
||||
# Don't assume because O_TMPFILE fds should lack a name entirely
|
||||
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w"
|
||||
runchecktest "linkat O_TMPFILE tmpdir only" pass $tmpdir_nested
|
||||
runchecktest "linkat O_TMPFILE tmpdir only, link" fail $tmpdir_nested $tmpfile
|
||||
|
||||
genprofile cap:dac_read_search "${tmpfile}:w"
|
||||
runchecktest "linkat O_TMPFILE tmpfile only" fail $tmpdir_nested
|
||||
runchecktest "linkat O_TMPFILE tmpfile only, link" fail $tmpdir_nested $tmpfile
|
||||
|
||||
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w" "${tmpfile}:w"
|
||||
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (w)" pass $tmpdir_nested
|
||||
# Even if semantically a (w)rite it gets logged as the (l)ink that it actually is
|
||||
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (w), link" xpass $tmpdir_nested $tmpfile
|
||||
|
||||
genprofile cap:dac_read_search "${tmpdir_nested}/:w" "${tmpdir_nested}/*:w" "${tmpfile}:l"
|
||||
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (l)" pass $tmpdir_nested
|
||||
# Even if semantically a (w)rite we want to test backwards compatibility with (l)ink as it is currently seen
|
||||
runchecktest "linkat O_TMPFILE tmpdir and tmpfile (l), link" pass $tmpdir_nested $tmpfile
|
||||
|
||||
rm $tmpfile
|
||||
rmdir $tmpdir_nested
|
@ -36,6 +36,7 @@ environment:
|
||||
TEST/io_uring: 1
|
||||
TEST/link: 1
|
||||
TEST/link_subset: 1
|
||||
TEST/linkat_tmpfile: 1
|
||||
TEST/longpath: 1
|
||||
TEST/mkdir: 1
|
||||
TEST/mmap: 1
|
||||
|
122
utils/aa-notify
122
utils/aa-notify
@ -53,7 +53,7 @@ import apparmor.update_profile as update_profile
|
||||
import LibAppArmor # C-library to parse one log line
|
||||
from apparmor.common import DebugLogger, open_file_read
|
||||
from apparmor.fail import enable_aa_exception_handler
|
||||
from apparmor.notify import get_last_login_timestamp
|
||||
from apparmor.notify import get_last_login_timestamp, get_event_special_type, set_userns_special_profile
|
||||
from apparmor.translations import init_translation
|
||||
from apparmor.logparser import ReadLog
|
||||
from apparmor.gui import UsernsGUI, ErrorGUI, ShowMoreGUI, ShowMoreGUIAggregated, set_interface_theme, ProfileRules
|
||||
@ -66,6 +66,9 @@ import threading
|
||||
|
||||
gi.require_version('GLib', '2.0')
|
||||
|
||||
# setup module translations
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
def get_user_login():
|
||||
"""Portable function to get username.
|
||||
@ -449,22 +452,15 @@ def compile_filter_regex(filters):
|
||||
|
||||
|
||||
def can_allow_rule(ev, special_profiles):
|
||||
if customized_message['userns']['cond'](ev, special_profiles):
|
||||
ev_type = get_event_special_type(ev, special_profiles)
|
||||
if ev_type != 'normal':
|
||||
if ev['execpath'] is None:
|
||||
return False
|
||||
return not aa.get_profile_filename_from_profile_name(ev['comm'])
|
||||
else:
|
||||
return aa.get_profile_filename_from_profile_name(ev['profile']) is not None
|
||||
|
||||
|
||||
def is_special_profile_userns(ev, special_profiles):
|
||||
if not special_profiles or ev['profile'] not in special_profiles:
|
||||
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
|
||||
|
||||
if 'execpath' not in ev or not ev['execpath']:
|
||||
ev['execpath'] = aa.find_executable(ev['comm'])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_userns_profile(name, path, ans):
|
||||
update_profile_path = update_profile.__file__
|
||||
|
||||
@ -490,6 +486,8 @@ def create_userns_profile(name, path, ans):
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
|
||||
else:
|
||||
aa.update_profiles()
|
||||
|
||||
|
||||
def ask_for_user_ns_denied(path, name, interactive=True):
|
||||
@ -508,8 +506,6 @@ def can_leverage_userns_event(ev):
|
||||
if ev['execpath'] is None:
|
||||
return 'error_cannot_find_path'
|
||||
|
||||
aa.update_profiles()
|
||||
|
||||
if aa.get_profile_filename_from_profile_name(ev['comm']):
|
||||
return 'error_userns_profile_exists'
|
||||
return 'ok'
|
||||
@ -544,25 +540,35 @@ def get_more_info_about_event(rl, ev, special_profiles, profile_path, header='')
|
||||
if value:
|
||||
out += '\t{} = {}\n'.format(_(key), value)
|
||||
|
||||
out += _('\nThe software that declined this operation is {}\n').format(ev['profile'])
|
||||
if ev['aamode'] == 'REJECTING':
|
||||
out += _('\nThe profile that denied this operation is {}\n').format(ev['profile'])
|
||||
else:
|
||||
out += _('\nThe profile that triggered this alert is {}\n').format(ev['profile'])
|
||||
|
||||
rule = rl.create_rule_from_ev(ev)
|
||||
|
||||
if rule:
|
||||
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
|
||||
rule.exec_perms = 'Pix'
|
||||
aa.update_profiles()
|
||||
if customized_message['userns']['cond'](ev, special_profiles):
|
||||
out += _('You may allow it through a dedicated unconfined profile for {}.').format(ev['comm'])
|
||||
if get_event_special_type(ev, special_profiles) != 'normal':
|
||||
userns_event_usable = can_leverage_userns_event(ev)
|
||||
if userns_event_usable == 'error_cannot_find_path':
|
||||
raw_rule = _('# You may allow it through a dedicated unconfined profile for {0}. However, apparmor cannot find {0}. If you want to allow it, please create a profile for it manually.').format(ev['comm'])
|
||||
raw_rule = _('# You may allow it through a dedicated unconfined profile for {0}. If you want to allow it, please create a profile for it manually.').format(ev['comm'])
|
||||
elif userns_event_usable == 'error_userns_profile_exists':
|
||||
raw_rule = _('# You may allow it through a dedicated unconfined profile for {} ({}). However, a profile already exists with this name. If you want to allow it, please create a profile for it manually.').format(ev['comm'], ev['execpath'])
|
||||
elif userns_event_usable == 'ok':
|
||||
raw_rule = _('# You may allow it through a dedicated unconfined profile for {} ({})').format(ev['comm'], ev['execpath'])
|
||||
out += raw_rule[1:]
|
||||
else:
|
||||
raw_rule = rule.get_clean()
|
||||
# TODO: This is brittle. Priority>1 might be needed. Also do we need to make the message show that we force allow?
|
||||
if ev['profile'] in aa.active_profiles.profiles and aa.is_known_rule(aa.active_profiles.profiles[ev['profile']], rule.rule_name, rule):
|
||||
rule.priority = 1
|
||||
raw_rule = "priority=1 " + raw_rule
|
||||
if aa.is_known_rule(aa.active_profiles.profiles[ev['profile']], rule.rule_name, rule):
|
||||
# TODO: Handle this edge case more gracefully
|
||||
raw_rule = _('# aa-notify tried to add rule {}. However aa-notify is not allowed to override priority>0 rules. Please fix your profile manually.\n').format(raw_rule)
|
||||
|
||||
if profile_path:
|
||||
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
|
||||
out += raw_rule
|
||||
@ -570,7 +576,6 @@ def get_more_info_about_event(rl, ev, special_profiles, profile_path, header='')
|
||||
out += _('However {profile} is not in {profile_dir}\nIt is likely that the profile was not stored in {profile_dir} or was removed.\n').format(profile=ev['profile'], profile_dir=aa.profile_dir)
|
||||
else: # Should not happen
|
||||
out += _('ERROR: Could not create rule from event.')
|
||||
|
||||
return out, raw_rule
|
||||
|
||||
|
||||
@ -589,7 +594,6 @@ def cb_more_info(notification, action, _args):
|
||||
if ans == 'add_rule':
|
||||
add_to_profile(raw_rule, ev['profile'])
|
||||
elif ans in {'allow', 'deny'}:
|
||||
customized_message['userns']['cond'](ev, special_profiles)
|
||||
create_userns_profile(ev['comm'], ev['execpath'], ans)
|
||||
|
||||
|
||||
@ -608,12 +612,15 @@ def add_to_profile(rule, profile_name):
|
||||
return
|
||||
|
||||
update_profile_path = update_profile.__file__
|
||||
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', rule, profile_name]
|
||||
|
||||
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', args.local, rule, profile_name]
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||
ErrorGUI(_('Failed to add rule {rule} to {profile}\nError code = {retcode}').format(rule=rule, profile=profile_name, retcode=e.returncode), False).show()
|
||||
else:
|
||||
aa.update_profiles()
|
||||
|
||||
|
||||
def create_from_file(file_path):
|
||||
@ -624,6 +631,8 @@ def create_from_file(file_path):
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||
ErrorGUI(_('Failed to add some rules'), False).show()
|
||||
else:
|
||||
aa.update_profiles()
|
||||
|
||||
|
||||
def allow_rules(clean_rules, allow_all=False):
|
||||
@ -639,7 +648,7 @@ def allow_rules(clean_rules, allow_all=False):
|
||||
with open(tmp.name, mode='w') as f:
|
||||
|
||||
for profile_name, profile_rules in clean_rules.items():
|
||||
written += f.write(profile_rules.get_writable_rules(template_path))
|
||||
written += f.write(profile_rules.get_writable_rules(template_path, args.local))
|
||||
|
||||
if written > 0:
|
||||
create_from_file(tmp.name)
|
||||
@ -669,26 +678,29 @@ def cb_add_to_profile(notification, action, _args):
|
||||
ErrorGUI(_('ERROR: Could not create rule from event.'), False).show()
|
||||
return
|
||||
|
||||
aa.update_profiles()
|
||||
|
||||
if customized_message['userns']['cond'](ev, special_profiles):
|
||||
if get_event_special_type(ev, special_profiles) != 'normal':
|
||||
ask_for_user_ns_denied(ev['execpath'], ev['comm'], False)
|
||||
else:
|
||||
add_to_profile(rule.get_clean(), ev['profile'])
|
||||
|
||||
|
||||
customized_message = {
|
||||
'userns': {
|
||||
'cond': lambda ev, special_profiles: (ev['operation'] == 'userns_create' or ev['operation'] == 'capable') and is_special_profile_userns(ev, special_profiles),
|
||||
'msg': 'Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?'
|
||||
'userns_change_profile': {
|
||||
'msg': _('Application {0} is transited to special profile. Capabilities could be denied')
|
||||
},
|
||||
'userns_denied': {
|
||||
'msg': _('Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?')
|
||||
},
|
||||
'userns_capable': {
|
||||
'msg': _('Application {0} in special profile wanted to add a capability: ok?')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def customize_notification_message(ev, msg, special_profiles):
|
||||
if customized_message['userns']['cond'](ev, special_profiles):
|
||||
msg = _(customized_message['userns']['msg']).format(ev['comm'])
|
||||
|
||||
msg_type = get_event_special_type(ev, special_profiles)
|
||||
if msg_type in customized_message:
|
||||
msg = customized_message[msg_type]['msg'].format(ev['comm'])
|
||||
return msg
|
||||
|
||||
|
||||
@ -712,7 +724,6 @@ def aggregate_event(agg, ev, keys_to_aggregate):
|
||||
|
||||
def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles):
|
||||
notification = ''
|
||||
summary = ''
|
||||
more_info = ''
|
||||
clean_rules = dict()
|
||||
summary = _('Notifications were raised for profiles: {}\n').format(', '.join(list(agg.keys())))
|
||||
@ -740,10 +751,11 @@ def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles
|
||||
ev = data['events'][0]
|
||||
profile_name = ev['profile']
|
||||
profile_path = aa.get_profile_filename_from_profile_name(profile_name)
|
||||
is_userns_profile = customized_message['userns']['cond'](ev, special_profiles)
|
||||
|
||||
is_userns_profile = get_event_special_type(ev, special_profiles) != 'normal'
|
||||
if is_userns_profile:
|
||||
bin_name = ev['comm']
|
||||
if 'execpath' not in ev:
|
||||
ev['execpath'] = None
|
||||
bin_path = ev['execpath']
|
||||
actionable = can_leverage_userns_event(ev) == 'ok'
|
||||
else:
|
||||
@ -761,7 +773,7 @@ def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles
|
||||
rules_for_profiles.add(raw_rule)
|
||||
|
||||
if rules_for_profiles != set():
|
||||
if profile not in special_profiles:
|
||||
if not is_userns_profile:
|
||||
if profile_path is not None:
|
||||
clean_rules_name = _('profile {}:').format(profile)
|
||||
elif re_snap.match(profile):
|
||||
@ -819,9 +831,6 @@ def main():
|
||||
# setup exception handling
|
||||
enable_aa_exception_handler()
|
||||
|
||||
# setup module translations
|
||||
_ = init_translation()
|
||||
|
||||
# Register the on_exit method with atexit
|
||||
# Takes care of closing the debug log etc
|
||||
atexit.register(aa.on_exit)
|
||||
@ -833,6 +842,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=_('Display AppArmor notifications or messages for DENIED entries.'))
|
||||
parser.add_argument('-p', '--poll', action='store_true', help=_('poll AppArmor logs and display notifications'))
|
||||
parser.add_argument('--display', type=str, help=_('set the DISPLAY environment variable (might be needed if sudo resets $DISPLAY)'))
|
||||
parser.add_argument('--xauthority', type=str, help=_('set the XAUTHORITY environment variable (might be needed if sudo resets XAUTHORITY)'))
|
||||
parser.add_argument('-f', '--file', type=str, help=_('search FILE for AppArmor messages'))
|
||||
parser.add_argument('-l', '--since-last', action='store_true', help=_('display stats since last login'))
|
||||
parser.add_argument('-s', '--since-days', type=int, metavar=('NUM'), help=_('show stats for last NUM days (can be used alone or with -p)'))
|
||||
@ -841,6 +851,7 @@ def main():
|
||||
parser.add_argument('-w', '--wait', type=int, metavar=('NUM'), help=_('wait NUM seconds before displaying notifications (with -p)'))
|
||||
parser.add_argument('-m', '--merge-notifications', action='store_true', help=_('Merge notification for improved readability (with -p)'))
|
||||
parser.add_argument('-F', '--foreground', action='store_true', help=_('Do not fork to the background'))
|
||||
parser.add_argument('-L', '--local', nargs='?', const='yes', choices=['yes', 'no', 'auto'], help=_('Add to local profile'))
|
||||
parser.add_argument('--prompt-filter', type=str, metavar=('PF'), help=_('kind of operations which display a popup prompt'))
|
||||
parser.add_argument('--debug', action='store_true', help=_('debug mode'))
|
||||
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
|
||||
@ -930,6 +941,7 @@ def main():
|
||||
- prompt_filter
|
||||
- maximum_number_notification_profiles
|
||||
- keys_to_aggregate
|
||||
- use_local_profiles
|
||||
- filter.profile,
|
||||
- filter.operation,
|
||||
- filter.name,
|
||||
@ -952,6 +964,7 @@ def main():
|
||||
'message_footer',
|
||||
'maximum_number_notification_profiles',
|
||||
'keys_to_aggregate',
|
||||
'use_local_profiles',
|
||||
'filter.profile',
|
||||
'filter.operation',
|
||||
'filter.name',
|
||||
@ -1027,7 +1040,9 @@ def main():
|
||||
userns_special_profiles = config['']['userns_special_profiles'].strip().split(',')
|
||||
else:
|
||||
# By default, unconfined and unprivileged_userns are the special profiles
|
||||
userns_special_profiles = ['unconfined', 'unprivileged_userns']
|
||||
userns_special_profiles = ['unconfined', 'unprivileged_userns', 'unpriv_.*']
|
||||
# To support regexes
|
||||
userns_special_profiles = set_userns_special_profile(userns_special_profiles)
|
||||
|
||||
if 'ignore_denied_capability' in config['']:
|
||||
ignore_denied_capability = config['']['ignore_denied_capability'].strip().split(',')
|
||||
@ -1059,6 +1074,19 @@ def main():
|
||||
else:
|
||||
keys_to_aggregate = {'operation', 'class', 'name', 'denied', 'target'}
|
||||
|
||||
if not args.local:
|
||||
if 'use_local_profiles' in config['']:
|
||||
if config['']['use_local_profiles'] in {'auto', 'yes', 'no'}:
|
||||
args.local = config['']['use_local_profiles']
|
||||
elif config['']['use_local_profiles'] is None:
|
||||
args.local = 'yes'
|
||||
else:
|
||||
sys.exit(_('ERROR: using an invalid value for use_local_profiles in config {}\nSupported values: {}').format(
|
||||
config['']['use_local_profiles'], ', '.join({'yes', 'auto', 'no'})
|
||||
))
|
||||
else:
|
||||
args.local = 'auto'
|
||||
|
||||
if args.file:
|
||||
logfile = args.file
|
||||
elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
|
||||
@ -1076,6 +1104,8 @@ def main():
|
||||
|
||||
if args.display:
|
||||
os.environ['DISPLAY'] = args.display
|
||||
if args.xauthority:
|
||||
os.environ['XAUTHORITY'] = args.xauthority
|
||||
|
||||
if args.poll:
|
||||
# Exit immediately if show_notifications is no or any of the options below
|
||||
@ -1156,10 +1186,14 @@ def main():
|
||||
if ev['operation'] == 'capable' and ev['comm'] in ignore_denied_capability:
|
||||
continue
|
||||
|
||||
# Special behaivor for userns:
|
||||
if args.prompt_filter and 'userns' in args.prompt_filter and customized_message['userns']['cond'](ev, userns_special_profiles):
|
||||
prompt_userns(ev)
|
||||
continue # Notification already displayed for this event, we go to the next one.
|
||||
# Special behavior for userns:
|
||||
if get_event_special_type(ev, userns_special_profiles) != 'normal':
|
||||
if 'execpath' not in ev:
|
||||
ev['execpath'] = None
|
||||
|
||||
if args.prompt_filter and 'userns' in args.prompt_filter:
|
||||
prompt_userns(ev)
|
||||
continue # Notification already displayed for this event, we go to the next one.
|
||||
|
||||
# Notifications should not be run as root, since root probably is
|
||||
# the wrong desktop user and not the one getting the notifications.
|
||||
|
@ -71,6 +71,14 @@ This has no effect when running under sudo.
|
||||
|
||||
wait NUM seconds before displaying notifications (for use with -p)
|
||||
|
||||
=item -L, --local [{yes,no,auto}]
|
||||
|
||||
add rules to a local profiles instead of the real profiles.
|
||||
This simplify profiles' deployment by keeping local modifications self-contained.
|
||||
- B<yes>: always use a local profile
|
||||
- B<no>: never use a local profile
|
||||
- B<auto>: use a local profile if the main profile already relies on a local profile
|
||||
|
||||
=item -v, --verbose
|
||||
|
||||
show messages with summaries.
|
||||
@ -89,8 +97,8 @@ System-wide configuration for B<aa-notify> is done via
|
||||
# Set to 'no' to disable AppArmor notifications globally
|
||||
show_notifications="yes"
|
||||
|
||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
||||
userns_special_profiles="unconfined,unprivileged_userns"
|
||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. Special profiles use Python's regular expression syntax. If unsure, leave as is.
|
||||
userns_special_profiles="unconfined,unprivileged_userns,unpriv_.*"
|
||||
|
||||
# Theme for aa-notify GUI. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
|
||||
interface_theme="ubuntu"
|
||||
@ -98,6 +106,9 @@ System-wide configuration for B<aa-notify> is done via
|
||||
# Binaries for which we ignore userns-related capability denials
|
||||
ignore_denied_capability="sudo,su"
|
||||
|
||||
# Write change to local profiles if enabled to preserve regular profiles and simplify upgrades (yes, no, auto)
|
||||
use_local_profiles="yes"
|
||||
|
||||
# OPTIONAL - kind of operations which display a popup prompt.
|
||||
prompt_filter="userns"
|
||||
|
||||
|
@ -72,7 +72,7 @@ Filter by profile name
|
||||
|
||||
=item --filter.profile_attach PROFILE_ATTACH
|
||||
|
||||
Filter by profile attachement (i.e. by path of the executable to which this profile applies)
|
||||
Filter by profile attachment (i.e. by path of the executable to which this profile applies)
|
||||
|
||||
=item --filter.profile_path PROFILE_PATH
|
||||
|
||||
|
@ -1703,6 +1703,71 @@ def read_profile(file, is_active_profile, read_error_fatal=False):
|
||||
extra_profiles.add_profile(filename, profile, attachment, profile_data[profile])
|
||||
|
||||
|
||||
# TODO: Split profiles' creating and saving.
|
||||
def create_local_profile_if_needed(profile_name):
|
||||
base_profile = profile_name
|
||||
while True:
|
||||
parent = active_profiles[base_profile].data.get('parent')
|
||||
if parent == '':
|
||||
break
|
||||
base_profile = parent
|
||||
|
||||
local_include = active_profiles[profile_name].get_local_include()
|
||||
|
||||
# Not found: we add a mention of the local profile in the main profile
|
||||
if not local_include:
|
||||
local_include = "local/" + profile_name.replace('/', '.')
|
||||
active_profiles[profile_name]['inc_ie'].add(IncludeRule(local_include, True, True))
|
||||
write_profile_ui_feedback(base_profile)
|
||||
|
||||
inc_file = profile_dir + '/' + local_include
|
||||
|
||||
# Create the include if needed
|
||||
if not include.get(inc_file, {}).get(inc_file, False):
|
||||
include[inc_file] = dict()
|
||||
include[inc_file][inc_file] = ProfileStorage(inc_file, inc_file, "create_local_profile_if_needed")
|
||||
|
||||
return inc_file
|
||||
|
||||
|
||||
def serialize_include(prof_storage, include_metadata=True):
|
||||
lines = []
|
||||
if include_metadata:
|
||||
lines.append('# Last Modified: %s' % time.asctime())
|
||||
|
||||
if prof_storage.get('initial_comment'):
|
||||
lines.append(prof_storage['initial_comment'].rstrip())
|
||||
|
||||
lines.extend(prof_storage.get_rules_clean(0))
|
||||
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def write_include_ui_feedback(include_data, incfile, out_dir=None, include_metadata=True):
|
||||
aaui.UI_Info(_('Writing updated include file %s') % incfile)
|
||||
write_include(include_data, incfile, out_dir, include_metadata)
|
||||
|
||||
|
||||
def write_include(include_data, incfile, out_dir=None, include_metadata=True):
|
||||
target_file = incfile if incfile.startswith('/') else os.path.join(profile_dir, incfile)
|
||||
if out_dir:
|
||||
target_file = os.path.join(out_dir, os.path.basename(target_file))
|
||||
|
||||
include_string = serialize_include(include_data, include_metadata=include_metadata)
|
||||
|
||||
with NamedTemporaryFile('w', suffix='~', delete=False, dir=profile_dir + "/local") as tmp:
|
||||
if os.path.exists(target_file):
|
||||
shutil.copymode(target_file, tmp.name)
|
||||
else:
|
||||
pass # 0o600 (NamedTemporaryFile default)
|
||||
tmp.write(include_string)
|
||||
|
||||
try:
|
||||
shutil.move(tmp.name, target_file)
|
||||
except PermissionError:
|
||||
aaui.UI_Important(_('WARNING: Can\'t write to %s. Please run this script with elevated privileges') % target_file)
|
||||
|
||||
|
||||
def attach_profile_data(profiles, profile_data):
|
||||
profile_data = merged_to_split(profile_data)
|
||||
# Make deep copy of data to avoid changes to
|
||||
|
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import tkinter.font
|
||||
import subprocess
|
||||
import apparmor.aa as aa
|
||||
|
||||
@ -99,12 +100,12 @@ class ProfileRules:
|
||||
for raw_rule in raw_rules:
|
||||
self.rules.append(SelectableRule(raw_rule, self.selectable))
|
||||
|
||||
def get_writable_rules(self, template_path, allow_all=False):
|
||||
def get_writable_rules(self, template_path, local='yes', allow_all=False):
|
||||
out = ''
|
||||
for rule in self.rules:
|
||||
if allow_all or rule.selected.get():
|
||||
if not self.is_userns_profile:
|
||||
out += 'add_rule\t{}\t{}\n'.format(rule.rule, self.profile_name)
|
||||
out += 'add_rule\t{}\t{}\t{}\n'.format(local, rule.rule, self.profile_name)
|
||||
else:
|
||||
out += 'create_userns\t{}\t{}\t{}\t{}\t{}\n'.format(template_path, self.profile_name, self.bin_path, self.profile_path, 'allow')
|
||||
return out
|
||||
@ -157,17 +158,18 @@ class ShowMoreGUIAggregated(GUI):
|
||||
|
||||
self.text_display = tk.Text(self.label_frame, wrap='word', height=40, width=100, yscrollcommand=self.scrollbar.set)
|
||||
|
||||
kwargs = {
|
||||
"height": self.text_display.winfo_reqheight() - 4, # The border are *inside* the canvas but *outside* the textbox. I need to remove 4px (2*the size of the borders) to get the same size
|
||||
"width": self.text_display.winfo_reqwidth() - 4,
|
||||
"borderwidth": self.text_display['borderwidth'],
|
||||
"relief": self.text_display['relief'],
|
||||
"yscrollcommand": self.scrollbar.set,
|
||||
}
|
||||
|
||||
if ttkthemes:
|
||||
self.text_display.configure(background=self.bg_color, foreground=self.fg_color)
|
||||
self.canvas = tk.Canvas(
|
||||
self.label_frame,
|
||||
background=self.bg_color,
|
||||
height=self.text_display.winfo_reqheight() - 4, # The border are *inside* the canvas but *outside* the textbox. I need to remove 4px (2*the size of the borders) to get the same size
|
||||
width=self.text_display.winfo_reqwidth() - 4,
|
||||
borderwidth=self.text_display['borderwidth'],
|
||||
relief=self.text_display['relief'],
|
||||
yscrollcommand=self.scrollbar.set
|
||||
)
|
||||
kwargs['background'] = self.bg_color
|
||||
self.canvas = tk.Canvas(self.label_frame, **kwargs)
|
||||
|
||||
self.inner_frame = ttk.Frame(self.canvas)
|
||||
self.canvas.create_window((2, 2), window=self.inner_frame, anchor='nw')
|
||||
@ -205,7 +207,7 @@ class ShowMoreGUIAggregated(GUI):
|
||||
|
||||
def create_profile_rules_frame(self, parent, clean_rules):
|
||||
for profile_name, profile_rules in clean_rules.items():
|
||||
label = ttk.Label(parent, text=profile_name, font=tk.font.BOLD)
|
||||
label = ttk.Label(parent, text=profile_name, font=tkinter.font.BOLD)
|
||||
label.pack(anchor='w', pady=(5, 0))
|
||||
label.bind("<Button-1>", lambda event, rules=profile_rules: self.toggle_profile_rules(rules))
|
||||
|
||||
|
@ -113,7 +113,6 @@ class ReadLog:
|
||||
return log_entry
|
||||
|
||||
def get_event_type(self, e):
|
||||
|
||||
if e['operation'] == 'exec':
|
||||
return 'file'
|
||||
elif e['class'] and e['class'] == 'namespace':
|
||||
@ -131,6 +130,8 @@ class ReadLog:
|
||||
return 'pivot_root'
|
||||
elif e['class'] and e['class'] == 'net' and e['family'] and e['family'] == 'unix':
|
||||
return 'unix'
|
||||
elif e['operation'] == 'change_onexec':
|
||||
return 'change_profile'
|
||||
elif e['class'] == 'file' or self.op_type(e) == 'file':
|
||||
return 'file'
|
||||
elif e['operation'] == 'capable':
|
||||
@ -160,6 +161,8 @@ class ReadLog:
|
||||
return None
|
||||
|
||||
def create_rule_from_ev(self, ev):
|
||||
if not ev:
|
||||
return None
|
||||
event_type = self.get_event_type(ev)
|
||||
if not event_type:
|
||||
return None
|
||||
@ -244,7 +247,7 @@ class ReadLog:
|
||||
|
||||
elif event_type == 'io_uring':
|
||||
ev['peer_profile'] = event.peer_profile
|
||||
elif event_type == 'capability':
|
||||
elif event_type == 'capability' or ev['operation'] == 'change_onexec':
|
||||
ev['comm'] = event.comm
|
||||
|
||||
if not ev['time']:
|
||||
@ -359,7 +362,7 @@ class ReadLog:
|
||||
self.hashlog[aamode][full_profile]['change_hat'][e['name2']] = True
|
||||
return
|
||||
|
||||
elif e['operation'] == 'change_profile':
|
||||
elif e['operation'] == 'change_profile' or e['operation'] == 'change_onexec':
|
||||
ChangeProfileRule.hashlog_from_event(self.hashlog[aamode][full_profile]['change_profile'], e)
|
||||
return
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
import os
|
||||
import struct
|
||||
import sqlite3
|
||||
import re
|
||||
|
||||
from apparmor.common import AppArmorBug, DebugLogger
|
||||
|
||||
@ -129,3 +130,33 @@ def get_last_login_timestamp_wtmp(username, filename='/var/log/wtmp'):
|
||||
|
||||
# When loop is done, last value should be the latest login timestamp
|
||||
return last_login
|
||||
|
||||
|
||||
def is_special_profile_userns(ev, special_profiles):
|
||||
if 'comm' not in ev:
|
||||
return False # special profiles have a 'comm' entry
|
||||
|
||||
if not special_profiles or not special_profiles.match(ev['profile']):
|
||||
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_event_special_type(ev, special_profiles):
|
||||
if is_special_profile_userns(ev, special_profiles):
|
||||
if ev['operation'] == 'userns_create':
|
||||
if ev['aamode'] == 'REJECTING':
|
||||
return 'userns_denied'
|
||||
else:
|
||||
return 'userns_change_profile'
|
||||
elif ev['operation'] == 'change_onexec':
|
||||
return 'userns_change_profile'
|
||||
elif ev['operation'] == 'capable':
|
||||
return 'userns_capable'
|
||||
else:
|
||||
raise AppArmorBug('unexpected operation: %s' % ev['operation'])
|
||||
return 'normal'
|
||||
|
||||
|
||||
def set_userns_special_profile(special_profiles):
|
||||
return re.compile('^({})$'.format('|'.join(special_profiles)))
|
||||
|
@ -199,6 +199,21 @@ class ProfileStorage:
|
||||
|
||||
return data
|
||||
|
||||
def get_local_include(self):
|
||||
inc = None
|
||||
preferred_inc = self.data['name']
|
||||
if preferred_inc.startswith('/'):
|
||||
preferred_inc = preferred_inc[1:]
|
||||
preferred_inc = 'local/' + preferred_inc.replace('/', '.')
|
||||
|
||||
# If a local profile already exists, we use it.
|
||||
for rule in self.data['inc_ie'].rules:
|
||||
if rule.path.startswith("local/"):
|
||||
inc = rule.path
|
||||
if rule.path == preferred_inc: # Prefer includes that matches the profile name.
|
||||
break
|
||||
return inc
|
||||
|
||||
@classmethod
|
||||
def parse(cls, line, file, lineno, profile, hat):
|
||||
"""parse a profile start line (using parse_profile_startline()) and convert it to an instance of this class"""
|
||||
|
@ -8,8 +8,19 @@ from apparmor import aa
|
||||
from apparmor.logparser import ReadLog
|
||||
|
||||
from apparmor.translations import init_translation
|
||||
|
||||
_ = init_translation()
|
||||
|
||||
is_aa_inited = False
|
||||
|
||||
|
||||
def init_if_needed():
|
||||
global is_aa_inited
|
||||
if not is_aa_inited:
|
||||
aa.init_aa()
|
||||
aa.read_profiles()
|
||||
is_aa_inited = True
|
||||
|
||||
|
||||
def create_userns(template_path, name, bin_path, profile_path, decision):
|
||||
with open(template_path, 'r') as f:
|
||||
@ -27,27 +38,48 @@ def create_userns(template_path, name, bin_path, profile_path, decision):
|
||||
exit(_('Cannot reload updated profile'))
|
||||
|
||||
|
||||
def add_to_profile(rule, profile_name):
|
||||
aa.init_aa()
|
||||
aa.update_profiles()
|
||||
|
||||
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
|
||||
|
||||
rule_obj = rule_class.create_instance(rule)
|
||||
|
||||
if not aa.active_profiles.profile_exists(profile_name):
|
||||
exit(_('Cannot find {} in profiles').format(profile_name))
|
||||
aa.active_profiles[profile_name][rule_type].add(rule_obj, cleanup=True)
|
||||
def add_to_profile(rule_obj, profile_name):
|
||||
aa.active_profiles[profile_name][rule_obj.rule_name].add(rule_obj, cleanup=True)
|
||||
|
||||
# Save changes
|
||||
aa.write_profile_ui_feedback(profile_name)
|
||||
|
||||
|
||||
def add_to_local_profile(rule_obj, profile_name):
|
||||
inc_file = aa.create_local_profile_if_needed(profile_name)
|
||||
|
||||
aa.include[inc_file][inc_file].data[rule_obj.rule_name].add(rule_obj, cleanup=True)
|
||||
aa.write_include_ui_feedback(aa.include[inc_file][inc_file], inc_file)
|
||||
|
||||
|
||||
def add_rule(mode, rule, profile_name):
|
||||
init_if_needed()
|
||||
|
||||
if not aa.active_profiles.profile_exists(profile_name):
|
||||
exit(_('Cannot find {} in profiles').format(profile_name))
|
||||
|
||||
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
|
||||
rule_obj = rule_class.create_instance(rule)
|
||||
|
||||
if mode == 'yes':
|
||||
add_to_local_profile(rule_obj, profile_name)
|
||||
elif mode == 'no':
|
||||
add_to_profile(rule_obj, profile_name)
|
||||
elif mode == 'auto':
|
||||
if aa.active_profiles[profile_name].get_local_include():
|
||||
add_to_local_profile(rule_obj, profile_name)
|
||||
else:
|
||||
add_to_profile(rule_obj, profile_name)
|
||||
else:
|
||||
usage(False)
|
||||
|
||||
aa.reload_base(profile_name)
|
||||
|
||||
|
||||
def usage(is_help):
|
||||
print('This tool is a low level tool - do not use it directly')
|
||||
print('{} create_userns <template_path> <name> <bin_path> <profile_path> <decision>'.format(sys.argv[0]))
|
||||
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
|
||||
print('{} add_rule <mode=yes|no|auto> <rule> <profile_name>'.format(sys.argv[0]))
|
||||
print('{} from_file <file>'.format(sys.argv[0]))
|
||||
if is_help:
|
||||
exit(0)
|
||||
@ -76,9 +108,9 @@ def do_command(command, args):
|
||||
usage(False)
|
||||
create_userns(args[1], args[2], args[3], args[4], args[5])
|
||||
elif command == 'add_rule':
|
||||
if not len(args) == 3:
|
||||
if not len(args) == 4:
|
||||
usage(False)
|
||||
add_to_profile(args[1], args[2])
|
||||
add_rule(args[1], args[2], args[3])
|
||||
elif command == 'help':
|
||||
usage(True)
|
||||
else:
|
||||
|
@ -11,8 +11,8 @@
|
||||
# Set to 'no' to disable AppArmor notifications globally
|
||||
show_notifications="yes"
|
||||
|
||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
||||
userns_special_profiles="unconfined,unprivileged_userns"
|
||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. Special profiles use Python's regular expression syntax. If unsure, leave as is.
|
||||
userns_special_profiles="unconfined,unprivileged_userns,unpriv_.*"
|
||||
|
||||
# Theme to use for aa-notify GUI themes. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
|
||||
interface_theme="ubuntu"
|
||||
@ -20,6 +20,9 @@ interface_theme="ubuntu"
|
||||
# Binaries for which we ignore userns-related capability denials
|
||||
ignore_denied_capability="sudo,su"
|
||||
|
||||
# OPTIONAL - Write changes to local profiles to preserve regular profiles and simplify upgrades (yes, no, auto)
|
||||
# use_local_profiles="yes"
|
||||
|
||||
# OPTIONAL - kind of operations which display a popup prompt.
|
||||
# prompt_filter="userns"
|
||||
|
||||
|
@ -167,8 +167,9 @@ class AANotifyTest(AANotifyBase):
|
||||
|
||||
expected_return_code = 0
|
||||
expected_output_1 = \
|
||||
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
|
||||
[-u USER] [-w NUM] [-m] [-F] [--prompt-filter PF] [--debug]
|
||||
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [--xauthority XAUTHORITY]
|
||||
[-f FILE] [-l] [-s NUM] [-v] [-u USER] [-w NUM] [-m] [-F]
|
||||
[-L [{yes,no,auto}]] [--prompt-filter PF] [--debug]
|
||||
[--filter.profile PROFILE] [--filter.operation OPERATION]
|
||||
[--filter.name NAME] [--filter.denied DENIED]
|
||||
[--filter.family FAMILY] [--filter.socket SOCKET]
|
||||
@ -182,6 +183,9 @@ Display AppArmor notifications or messages for DENIED entries.
|
||||
-p, --poll poll AppArmor logs and display notifications
|
||||
--display DISPLAY set the DISPLAY environment variable (might be needed if
|
||||
sudo resets $DISPLAY)
|
||||
--xauthority XAUTHORITY
|
||||
set the XAUTHORITY environment variable (might be needed
|
||||
if sudo resets XAUTHORITY)
|
||||
-f, --file FILE search FILE for AppArmor messages
|
||||
-l, --since-last display stats since last login
|
||||
-s, --since-days NUM show stats for last NUM days (can be used alone or with
|
||||
@ -193,6 +197,8 @@ Display AppArmor notifications or messages for DENIED entries.
|
||||
-m, --merge-notifications
|
||||
Merge notification for improved readability (with -p)
|
||||
-F, --foreground Do not fork to the background
|
||||
-L, --local [{yes,no,auto}]
|
||||
Add to local profile
|
||||
--prompt-filter PF kind of operations which display a popup prompt
|
||||
--debug debug mode
|
||||
|
||||
@ -231,6 +237,11 @@ Filtering options:
|
||||
), (
|
||||
', --wait NUM ',
|
||||
' NUM, --wait NUM',
|
||||
), (
|
||||
' -L, --local [{yes,no,auto}]\n'
|
||||
+ ' Add to local profile',
|
||||
' -L [{yes,no,auto}], --local [{yes,no,auto}]\n'
|
||||
+ ' Add to local profile'
|
||||
)]
|
||||
for patch in patches:
|
||||
expected_output_2 = expected_output_2.replace(patch[0], patch[1])
|
||||
|
@ -166,7 +166,6 @@ log_to_profile_skip = [
|
||||
|
||||
# tests that cause an empty log
|
||||
log_to_profile_known_empty_log = [
|
||||
'change_onexec_lp1648143', # change_onexec not supported in logparser.py yet (and the log is about "no new privs" error)
|
||||
'ptrace_garbage_lp1689667_1', # no denied= in log
|
||||
'ptrace_no_denied_mask', # no denied= in log
|
||||
'unconfined-change_hat', # unconfined trying to change_hat, which isn't allowed
|
||||
|
@ -12,7 +12,8 @@
|
||||
import unittest
|
||||
|
||||
from apparmor.common import AppArmorBug
|
||||
from apparmor.notify import get_last_login_timestamp, get_last_login_timestamp_wtmp, sane_timestamp
|
||||
from apparmor.notify import get_last_login_timestamp, get_last_login_timestamp_wtmp, sane_timestamp, get_event_special_type, set_userns_special_profile
|
||||
from apparmor.logparser import ReadLog
|
||||
from common_test import AATest, setup_all_loops
|
||||
|
||||
|
||||
@ -87,6 +88,36 @@ class TestGet_last_login_timestamp_wtmp(AATest):
|
||||
get_last_login_timestamp_wtmp('root', 'wtmp-examples/wtmp-x86_64-past')
|
||||
|
||||
|
||||
class TestEventSpecialType(AATest):
|
||||
userns_special_profiles = set_userns_special_profile(['unconfined', 'unprivileged_userns', 'unpriv_.*'])
|
||||
parser = ReadLog('', '', '')
|
||||
tests = (
|
||||
('[ 176.385388] audit: type=1400 audit(1666891380.570:78): apparmor="DENIED" operation="userns_create" class="namespace" profile="/usr/bin/bwrap-userns-restrict" pid=1785 comm="userns_child_ex" requested="userns_create" denied="userns_create"', 'normal'),
|
||||
('[ 839.488169] audit: type=1400 audit(1752065668.819:208): apparmor="DENIED" operation="userns_create" class="namespace" info="Userns create restricted - failed to find unprivileged_userns profile" error=-13 profile="unconfined" pid=12124 comm="unshare" requested="userns_create" denied="userns_create" target="unprivileged_userns"', 'userns_denied'),
|
||||
('[ 429.272003] audit: type=1400 audit(1720613712.153:168): apparmor="AUDIT" operation="userns_create" class="namespace" info="Userns create - transitioning profile" profile="unconfined" pid=5630 comm="unshare" requested="userns_create" target="unprivileged_userns" execpath="/usr/bin/unshare"', 'userns_change_profile'),
|
||||
('[ 52.901383] audit: type=1400 audit(1752064882.228:82): apparmor="DENIED" operation="capable" class="cap" profile="unprivileged_userns" pid=6700 comm="electron" capability=21 capname="sys_admin"', 'userns_capable'),
|
||||
('Jul 31 17:11:16 dbusdev-saucy-amd64 dbus[1692]: apparmor="DENIED" operation="dbus_bind" bus="session" name="com.apparmor.Test" mask="bind" pid=2940 profile="/tmp/apparmor-2.8.0/tests/regression/apparmor/dbus_service"', 'normal'),
|
||||
('[103975.623545] audit: type=1400 audit(1481284511.494:2807): apparmor="DENIED" operation="change_onexec" info="no new privs" error=-1 namespace="root//lxd-tor_<var-lib-lxd>" profile="unconfined" name="system_tor" pid=18593 comm="(tor)" target="system_tor"', 'userns_change_profile'),
|
||||
('[78661.551820] audit: type=1400 audit(1752661047.170:350): apparmor="DENIED" operation="capable" class="cap" profile="unpriv_bwrap" pid=1412550 comm="node" capability=21 capname="sys_admin"', 'userns_capable'),
|
||||
)
|
||||
|
||||
def _run_test(self, ev, expected):
|
||||
parsed_event = self.parser.parse_event(ev)
|
||||
r = self.parser.create_rule_from_ev(parsed_event)
|
||||
self.assertIsNotNone(r)
|
||||
|
||||
real_type = get_event_special_type(parsed_event, self.userns_special_profiles)
|
||||
self.assertEqual(expected, real_type,
|
||||
"ev {}: {} != {}".format(ev, expected, real_type))
|
||||
|
||||
def test_invalid(self):
|
||||
ev = 'type=AVC msg=audit(1333698107.128:273917): apparmor="DENIED" operation="recvmsg" parent=1596 profile="unprivileged_userns" pid=1875 comm="nc" laddr=::ffff:127.0.0.1 lport=2048 faddr=::ffff:127.0.0.1 fport=59180 family="inet6" sock_type="stream" protocol=6'
|
||||
parsed_event = self.parser.parse_event(ev)
|
||||
parsed_event['comm'] = 'something' # Artificially crafted invalid event
|
||||
with self.assertRaises(AppArmorBug):
|
||||
get_event_special_type(parsed_event, self.userns_special_profiles)
|
||||
|
||||
|
||||
setup_all_loops(__name__)
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=1)
|
||||
|
@ -14,6 +14,7 @@ import unittest
|
||||
from apparmor.common import AppArmorBug, AppArmorException
|
||||
from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, split_flags, var_transform
|
||||
from apparmor.rule.capability import CapabilityRule
|
||||
from apparmor.rule.include import IncludeRule
|
||||
from common_test import AATest, setup_all_loops
|
||||
|
||||
|
||||
@ -313,6 +314,27 @@ class AaTest_var_transform(AATest):
|
||||
self.assertEqual(var_transform(params), expected)
|
||||
|
||||
|
||||
class AaTest_include(AATest):
|
||||
tests = (
|
||||
(('profile foo /foo {', []), None), # No include
|
||||
(('profile foo /foo {', ['elsewhere/foo']), None), # No include in local/
|
||||
(('profile foo /foo {', ['local/foo']), "local/foo"), # Single include, we pick it
|
||||
(('profile foo /foo {', ['local/bar']), "local/bar"), # Single include, we pick it
|
||||
(('profile x//y /y {', ['local/x..y', 'local/y']), "local/x..y"), # Pick the include that matches the profile nam
|
||||
(('profile foo /foo {', ['local/bar', 'local/foo', 'local/baz']), "local/foo"), # Pick the include that matches the profile name
|
||||
(('/usr/bin/xx {', ['local/usr.bin.xx', 'local/xx']), "local/usr.bin.xx"), # Pick the include that matches the profile name
|
||||
(('profile foo /foo {', ['local/bar', 'local/baz', 'local/qux']), "local/qux"), # No match, pick the last one
|
||||
)
|
||||
|
||||
def _run_test(self, params, expected):
|
||||
(profile, hat, prof_storage) = ProfileStorage.parse(params[0], 'somefile', 1, None, None)
|
||||
|
||||
for inc in params[1]:
|
||||
prof_storage.data['inc_ie'].add(IncludeRule(inc, True, True))
|
||||
|
||||
self.assertEqual(prof_storage.get_local_include(), expected)
|
||||
|
||||
|
||||
setup_all_loops(__name__)
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user