1 /*
2  * AIDE (Advanced Intrusion Detection Environment)
3  *
4  * Copyright (C) 1999-2006, 2009-2011, 2015-2016, 2019-2021 Rami Lehti,
5  *               Pablo Virolainen, Richard van den Berg, Hannes von Haugwitz
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation; either version 2 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21 
22 #include <stdlib.h>
23 #include <string.h>
24 
25 #include "seltree.h"
26 #include "log.h"
27 #include "util.h"
28 
29 #define PARTIAL_RULE_MATCH       (-1)
30 #define NO_RULE_MATCH            (0)
31 #define RESTRICTED_RULE_MATCH    (1)
32 #define RULE_MATCH               (2)
33 
log_tree(LOG_LEVEL log_level,seltree * tree,int depth)34 void log_tree(LOG_LEVEL log_level, seltree* tree, int depth) {
35 
36     list* r;
37     rx_rule* rxc;
38 
39     log_msg(log_level, "%-*s %s:", depth, depth?"\u251d":"\u250c", tree->path, tree);
40 
41     char *attr_str, *rs_str;
42 
43     for(r=tree->equ_rx_lst;r!=NULL;r=r->next) {
44         rxc=r->data;
45         log_msg(log_level, "%-*s  '=%s %s %s' (%s:%d: '%s')", depth+2, "\u2502", rxc->rx, rs_str = get_restriction_string(rxc->restriction), attr_str = diff_attributes(0, rxc->attr), rxc->config_filename, rxc->config_linenumber, rxc->config_line);
46         free(rs_str);
47         free(attr_str);
48     }
49     for(r=tree->sel_rx_lst;r!=NULL;r=r->next) {
50         rxc=r->data;
51         log_msg(log_level, "%-*s  '%s %s %s' (%s:%d: '%s')", depth+2, "\u2502", rxc->rx, rs_str = get_restriction_string(rxc->restriction), attr_str = diff_attributes(0, rxc->attr), rxc->config_filename, rxc->config_linenumber, rxc->config_line);
52         free(rs_str);
53         free(attr_str);
54     }
55     for(r=tree->neg_rx_lst;r!=NULL;r=r->next) {
56         rxc=r->data;
57         log_msg(log_level, "%-*s  '!%s %s' (%s:%d: '%s')", depth+2, "\u2502", rxc->rx, rs_str = get_restriction_string(rxc->restriction), rxc->config_filename, rxc->config_linenumber, rxc->config_line);
58         free(rs_str);
59     }
60 
61     for(r=tree->childs;r!=NULL;r=r->next) {
62         log_tree(log_level, r->data, depth+2);
63     }
64     if (depth == 0) {
65         log_msg(log_level, "%s", "\u2514");
66     }
67 }
68 
69 /*
70  * strrxtok()
71  * return a pointer to a copy of the non-regexp path part of the argument
72  */
strrxtok(char * rx)73 static char* strrxtok(char* rx)
74 {
75   char*p=NULL;
76   char*t=NULL;
77   size_t i=0;
78 
79   /* The following code assumes that the first character is a slash */
80   size_t lastslash=1;
81 
82   p=checked_strdup(rx);
83   p[0]='/';
84 
85   for(i=1;i<strlen(p);i++){
86     switch(p[i])
87       {
88       case '/':
89 	lastslash=i;
90 	break;
91       case '(':
92       case '^':
93       case '$':
94       case '?':
95       case '*':
96       case '[':
97 	i=strlen(p);
98 	break;
99       case '\\':
100 	t=checked_strdup(p);
101 	strcpy(p+i,t+i+1);
102 	free(t);
103 	t=NULL;
104 	break;
105       default:
106 	break;
107       }
108   }
109 
110   p[lastslash]='\0';
111 
112   return p;
113 }
114 
strlastslash(char * str)115 static char* strlastslash(char*str)
116 {
117   char* p=NULL;
118   size_t lastslash=1;
119   size_t i=0;
120 
121   for(i=1;i<strlen(str);i++){
122     if(str[i]=='/'){
123       lastslash=i;
124     }
125   }
126 
127   p=(char*)checked_malloc(sizeof(char)*lastslash+1);
128   strncpy(p,str,lastslash);
129   p[lastslash]='\0';
130 
131   return p;
132 }
133 
strgetndirname(char * path,int depth)134 char* strgetndirname(char* path,int depth)
135 {
136   char* r=NULL;
137   char* tmp=NULL;
138   int i=0;
139 
140   for(r=path;;r+=1){
141     if(*r=='/')
142       i++;
143     if(*r=='\0')
144       break;
145     if(i==depth)
146       break;
147   }
148   /* If we ran out string return the whole string */
149   if(!(*r))
150     return checked_strdup(path);
151 
152   tmp=checked_strdup(path);
153 
154   tmp[r-path]='\0';
155 
156   return tmp;
157 }
158 
treedepth(seltree * node)159 int treedepth(seltree* node)
160 {
161   seltree* r=NULL;
162   int depth=0;
163 
164   for(r=node;r;r=r->parent)
165     depth++;
166 
167   return depth;
168 }
169 
compare_node_by_path(const void * n1,const void * n2)170 int compare_node_by_path(const void *n1, const void *n2)
171 {
172     const seltree *x1 = n1;
173     const seltree *x2 = n2;
174     return strcmp(x1->path, x2->path);
175 }
176 
get_seltree_node(seltree * tree,char * path)177 seltree* get_seltree_node(seltree* tree,char* path)
178 {
179   seltree* node=NULL;
180   list* r=NULL;
181   char* tmp=NULL;
182 
183   if(tree==NULL){
184     return NULL;
185   }
186 
187   if(strncmp(path,tree->path,strlen(path)+1)==0){
188     return tree;
189   }
190   else{
191     tmp=strgetndirname(path,treedepth(tree)+1);
192     for(r=tree->childs;r;r=r->next){
193       if(strncmp(((seltree*)r->data)->path,tmp,strlen(tmp)+1)==0){
194 	node=get_seltree_node((seltree*)r->data,path);
195 	if(node!=NULL){
196 	  /* Don't leak memory */
197 	  free(tmp);
198 	  return node;
199 	}
200       }
201     }
202     free(tmp);
203   }
204   return NULL;
205 }
206 
207 
new_seltree_node(seltree * tree,char * path,int isrx,rx_rule * r)208 seltree* new_seltree_node(
209         seltree* tree,
210         char*path,
211         int isrx,
212         rx_rule* r)
213 {
214   seltree* node=NULL;
215   seltree* parent=NULL;
216   char* tmprxtok = NULL;
217 
218   node=(seltree*)checked_malloc(sizeof(seltree));
219   node->childs=NULL;
220   node->path=checked_strdup(path);
221   node->sel_rx_lst=NULL;
222   node->neg_rx_lst=NULL;
223   node->equ_rx_lst=NULL;
224   node->checked=0;
225   node->new_data=NULL;
226   node->old_data=NULL;
227   node->changed_attrs = 0;
228 
229   if(tree!=NULL){
230     tmprxtok = strrxtok(path);
231     if(isrx){
232       parent=get_seltree_node(tree,tmprxtok);
233     }else {
234       char* dirn=strlastslash(path);
235       parent=get_seltree_node(tree,dirn);
236       free(dirn);
237     }
238     if(parent==NULL){
239       if(isrx){
240 	parent=new_seltree_node(tree,tmprxtok,isrx,r);
241       }else {
242         char* dirn=strlastslash(path);
243         parent=new_seltree_node(tree,dirn,isrx,r);
244         free(dirn);
245       }
246     }
247     free(tmprxtok);
248     parent->childs=list_sorted_insert(parent->childs,(void*)node, compare_node_by_path);
249     node->parent=parent;
250   }else {
251     node->parent=NULL;
252   }
253   log_msg(LOG_LEVEL_DEBUG, "new node '%s' (%p, parent: %p)", node->path, node, node->parent);
254   return node;
255 }
256 
init_tree()257 seltree *init_tree() {
258     seltree* node = new_seltree_node(NULL,"/",0,NULL);
259     log_msg(LOG_LEVEL_DEBUG, "added new node '%s' (%p) for '%s' (reason: root node)", node->path, node, "/");
260     return node;
261 }
262 
add_rx_to_tree(char * rx,RESTRICTION_TYPE restriction,int rule_type,seltree * tree,const char ** rule_error,int * rule_erroffset)263 rx_rule * add_rx_to_tree(char * rx, RESTRICTION_TYPE restriction, int rule_type, seltree *tree, const char **rule_error, int *rule_erroffset) {
264     rx_rule* r = NULL;
265     seltree *curnode = NULL;
266     char *rxtok = NULL;
267 
268     r=(rx_rule*)checked_malloc(sizeof(rx_rule));
269 
270     r->rx=rx;
271     r->restriction = restriction;
272 
273     r->config_filename = NULL;
274     r->config_line = NULL;
275     r->config_linenumber = -1;
276     r->attr = 0;
277 
278     if((r->crx=pcre_compile(r->rx, PCRE_ANCHORED|PCRE_UTF8, rule_error, rule_erroffset, NULL)) == NULL) {
279         free(r);
280         return NULL;
281     } else {
282         rxtok=strrxtok(r->rx);
283         curnode=get_seltree_node(tree,rxtok);
284 
285         for(size_t i=1;i < strlen(rxtok); ++i){
286             if (rxtok[i] == '/' && rxtok[i-1] == '/') {
287                 *rule_error = "invalid double slash" ;
288                 *rule_erroffset = i;
289                 free(r);
290                 return NULL;
291             }
292         }
293 
294         if(curnode == NULL){
295             curnode=new_seltree_node(tree,rxtok,1,r);
296             log_msg(LOG_LEVEL_DEBUG, "added new node '%s' (%p) for '%s' (reason: new rule '%s')", curnode->path, curnode, rxtok, r->rx);
297         }
298         r->node = curnode;
299         switch (rule_type){
300             case AIDE_NEGATIVE_RULE:{
301                 curnode->neg_rx_lst=list_append(curnode->neg_rx_lst,(void*)r);
302                 break;
303             }
304             case AIDE_EQUAL_RULE:{
305                 curnode->equ_rx_lst=list_append(curnode->equ_rx_lst,(void*)r);
306                 break;
307             }
308             case AIDE_SELECTIVE_RULE:{
309                 curnode->sel_rx_lst=list_append(curnode->sel_rx_lst,(void*)r);
310                 break;
311             }
312         }
313         free(rxtok);
314     }
315     return r;
316 }
317 
318 #define LOG_MATCH(log_level, border, format, ...) \
319     log_msg(log_level, "%s %*c'%s' " #format " of %s (%s:%d: '%s')", border, depth+2, ' ', text, __VA_ARGS__, get_rule_type_long_string(rule_type), rx->config_filename, rx->config_linenumber, rx->config_line);
320 
check_list_for_match(list * rxrlist,char * text,rx_rule ** rule,RESTRICTION_TYPE file_type,int rule_type,int depth,bool unrestricted_only)321 static int check_list_for_match(list* rxrlist,char* text, rx_rule* *rule, RESTRICTION_TYPE file_type, int rule_type, int depth, bool unrestricted_only)
322 {
323   list* r=NULL;
324   int retval=NO_RULE_MATCH;
325   int pcre_retval;
326   pcre_extra *pcre_extra = NULL;
327   char *rs_str = NULL;
328   for(r=rxrlist;r;r=r->next){
329       rx_rule *rx = (rx_rule*)r->data;
330 
331       if (!(unrestricted_only && rx->restriction)) {
332 
333       pcre_retval=pcre_exec((pcre*)rx->crx, pcre_extra, text, strlen(text), 0, PCRE_PARTIAL_SOFT, NULL, 0);
334       if (pcre_retval >= 0) {
335           if (!rx->restriction || file_type&rx->restriction) {
336                   *rule = rx;
337                   retval = rx->restriction?RESTRICTED_RULE_MATCH:RULE_MATCH;
338                   LOG_MATCH(LOG_LEVEL_RULE, "\u251d", matches regex '%s' and restriction '%s', rx->rx, rs_str = get_restriction_string(rx->restriction))
339                   free(rs_str);
340                   break;
341           } else {
342               LOG_MATCH(LOG_LEVEL_RULE, "\u2502", does not match restriction '%s', rs_str = get_restriction_string(rx->restriction))
343               free(rs_str);
344               retval=PARTIAL_RULE_MATCH;
345           }
346       } else if (pcre_retval == PCRE_ERROR_PARTIAL) {
347           LOG_MATCH(LOG_LEVEL_RULE, "\u2502", partially matches regex '%s', rx->rx)
348           retval=PARTIAL_RULE_MATCH;
349       } else {
350           LOG_MATCH(LOG_LEVEL_RULE, "\u2502", does not match regex '%s', rx->rx)
351       }
352 
353       } else {
354           log_msg(LOG_LEVEL_RULE, "\u2502 %*cskip restricted '%s' rule as requested (%s:%d: '%s')", depth+2, ' ', rs_str = get_restriction_string(rx->restriction), rx->config_filename, rx->config_linenumber, rx->config_line);
355           free(rs_str);
356       }
357   }
358   return retval;
359 }
360 
361 /*
362  * Function check_node_for_match()
363  * calls itself recursively to go to the top and then back down.
364  * uses check_list_for_match()
365  * returns:
366  * 0,  if a negative rule was matched
367  * 1,  if a selective rule was matched
368  * 2,  if a equals rule was matched
369  * retval if no rule was matched.
370  * retval&3 if no rule was matched and first in the recursion
371  * to keep state revat is orred with:
372  * 4,  matched deeper on equ rule
373  * 8,  matched deeper on sel rule
374  *16,  this is a recursed call
375  *32,  top-level call
376  */
check_node_for_match(seltree * node,char * text,RESTRICTION_TYPE file_type,int retval,rx_rule ** rule,int depth)377 static int check_node_for_match(seltree *node, char *text, RESTRICTION_TYPE file_type, int retval, rx_rule* *rule, int depth)
378 {
379 
380   if(node==NULL){
381       return retval;
382   }
383 
384   if (node->equ_rx_lst || node->sel_rx_lst || node->neg_rx_lst) {
385 
386   /* if this call is not recursive we check the equals list and we set top *
387    * and retval so we know following calls are recursive */
388   if(!(retval&16)){
389       retval|=16;
390 
391       if (node->equ_rx_lst) {
392           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': check equal list", depth, ' ', node->path);
393           switch (check_list_for_match(node->equ_rx_lst, text, rule, file_type, AIDE_EQUAL_RULE, depth, false)) {
394               case RESTRICTED_RULE_MATCH:
395               case RULE_MATCH: {
396                           log_msg(LOG_LEVEL_RULE, "\u2502 %*cequal match for '%s' (node: '%s')", depth, ' ', text, node->path);
397                           retval|=2|4;
398                           break;
399                       }
400               case PARTIAL_RULE_MATCH: {
401                            if(file_type&FT_DIR && get_seltree_node(node,text)==NULL) {
402                                seltree *new_node = new_seltree_node(node,text,0,NULL);
403                                log_msg(LOG_LEVEL_DEBUG, "added new node '%s' (%p) for '%s' (reason: partial equal match for directory)", new_node->path, new_node, text);
404                            }
405                            break;
406                        }
407           }
408       } else {
409           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip equal list (reason: list is empty)", depth, ' ', node->path);
410       }
411   } else {
412       log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s' skip equal list (reason: not on top level)", depth, ' ', node->path);
413   }
414   /* We'll use retval to pass information on whether to recurse
415    * the dir or not */
416 
417   /* If 4 and 8 are not set, we will check for matches */
418   if(!(retval&(4|8))){
419       if (node->sel_rx_lst) {
420           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': check selective list", depth, ' ', node->path);
421           switch (check_list_for_match(node->sel_rx_lst, text, rule, file_type, AIDE_SELECTIVE_RULE, depth, false)) {
422               case RESTRICTED_RULE_MATCH:
423               case RULE_MATCH: {
424                           log_msg(LOG_LEVEL_RULE, "\u2502 %*cselective match for '%s' (node: '%s')", depth, ' ', text, node->path);
425                           retval|=1|8;
426                           break;
427                       }
428               case PARTIAL_RULE_MATCH: {
429                            if(file_type&FT_DIR && get_seltree_node(node,text)==NULL) {
430                                seltree *new_node = new_seltree_node(node,text,0,NULL);
431                                log_msg(LOG_LEVEL_DEBUG, "added new node '%s', (%p) for '%s' (reason: partial selective match for directory)", new_node->path, new_node, text);
432                            }
433                            break;
434                        }
435           }
436       } else {
437           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip selective list (reason: list is empty)", depth, ' ', node->path);
438       }
439   } else {
440       log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip selective list (reason: previous positive match)", depth, ' ', node->path);
441   }
442 
443   /* Now let's check the ancestors */
444   retval=check_node_for_match(node->parent, text, file_type, retval&~32, rule, depth+2);
445 
446   /* Negative regexps are the strongest so they are checked last */
447   /* If this file is to be added */
448   if(retval&(1|2)){
449       if (node->neg_rx_lst) {
450           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': check negative list (reason: previous positive match)", depth, ' ', node->path);
451 
452           char* parentname=checked_strdup(text);
453           do {
454               char *tmp=strrchr(parentname,'/');
455               if(tmp != parentname){
456                   *tmp='\0';
457               } else {
458                   parentname[1]='\0';
459               }
460               if (strcmp(parentname,node->path) > 0) {
461                   log_msg(LOG_LEVEL_RULE, "\u2502 %*ccheck parent directory '%s' (unrestricted rules only)", depth+2, ' ', parentname);
462                   if (check_list_for_match(node->neg_rx_lst, parentname, rule, FT_DIR, AIDE_NEGATIVE_RULE, depth+4, true) == RULE_MATCH) {
463                       log_msg(LOG_LEVEL_RULE, "\u2502 %*cnegative match for parent directory '%s'", depth, ' ', parentname);
464                       retval=0;;
465                       break;
466                   }
467               }
468           } while (strcmp(parentname,node->path) > 0);
469           free(parentname);
470 
471           if (retval) {
472           log_msg(LOG_LEVEL_RULE, "\u2502 %*ccheck file '%s'", depth+2, ' ', text);
473           switch (check_list_for_match(node->neg_rx_lst, text, rule, file_type, AIDE_NEGATIVE_RULE, depth+2, false)) {
474               case RESTRICTED_RULE_MATCH: {
475                   if(file_type&FT_DIR && get_seltree_node(node,text)==NULL) {
476                       seltree *new_node = new_seltree_node(node,text,0,NULL);
477                       log_msg(LOG_LEVEL_DEBUG, "added new node '%s' (%p) for '%s' (reason: restricted negative match for directory)", new_node->path, new_node, text);
478                   }
479 
480               }
481               // fall through
482               case RULE_MATCH: {
483                   log_msg(LOG_LEVEL_RULE, "\u2502 %*cnegative match for '%s' (node: '%s')", depth, ' ', text, node->path);
484                   retval=0;
485                   break;
486               }
487           }
488           } else {
489             log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip checking file '%s' (reason: negative match for a parent directory)", depth, ' ', node->path, text);
490           }
491       } else {
492           log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip negative list (reason: list is empty)", depth, ' ', node->path);
493       }
494   } else {
495       log_msg(LOG_LEVEL_RULE, "\u2502 %*cnode: '%s': skip negative list (reason: no previous positive match)", depth, ' ', node->path);
496   }
497 
498   } else {
499     log_msg(LOG_LEVEL_DEBUG, "\u2502 %*cskip node '%s' (reason: no regex rules)", depth, ' ', node->path);
500     retval = check_node_for_match(node->parent, text, file_type, (retval|16)&~32, rule, depth);
501   }
502 
503   /* Now we discard the info whether a match was made or not *
504    * and just return 0,1 or 2 */
505   if(!(retval&32)){
506       retval&=3;
507   }
508   return retval;
509 }
510 
check_seltree(seltree * tree,char * filename,RESTRICTION_TYPE file_type,rx_rule ** rule)511 int check_seltree(seltree *tree, char *filename, RESTRICTION_TYPE file_type, rx_rule* *rule) {
512   log_msg(LOG_LEVEL_RULE, "\u2502 check '%s'", filename);
513   char * tmp=NULL;
514   char * parentname=NULL;
515   seltree* pnode=NULL;
516   int retval = 0;
517 
518   parentname=checked_strdup(filename);
519 
520   do {
521 
522   tmp=strrchr(parentname,'/');
523   if(tmp!=parentname){
524     *tmp='\0';
525   }else {
526 
527     if(parentname[1]!='\0'){
528       /* we are in the root dir */
529       parentname[1]='\0';
530     }
531   }
532 
533   pnode=get_seltree_node(tree,parentname);
534   if (pnode == NULL) {
535     retval |= 16;
536   }
537 
538   } while (pnode == NULL);
539 
540   log_msg(LOG_LEVEL_DEBUG, "got parent node '%s' (%p) for parentname '%s'", pnode->path, pnode, parentname);
541 
542   free(parentname);
543 
544   retval = check_node_for_match(pnode, filename, file_type, retval|32 ,rule, 0);
545 
546   if (retval) {
547     char *str;
548     log_msg(LOG_LEVEL_RULE, "\u2534 ADD '%s' to the tree (attr: '%s')", filename, str = diff_attributes(0, (*rule)->attr));
549     free(str);
550 
551     if(get_seltree_node(tree,filename)==NULL) {
552         seltree *new_node = new_seltree_node(tree,filename,0,NULL);
553         log_msg(LOG_LEVEL_DEBUG, "added new node '%s', (%p) for '%s' (reason: full match)", new_node->path, new_node, filename);
554     }
555   } else {
556     log_msg(LOG_LEVEL_RULE, "\u2534 do NOT add '%s' to the tree", filename);
557   }
558   return retval;
559 }
560