new upstream release (3.3.0); modify package compatibility for Stretch
[ossec-hids.git] / src / analysisd / decoders / syscheck.c
1 /* Copyright (C) 2009 Trend Micro Inc.
2  * All right reserved.
3  *
4  * This program is a free software; you can redistribute it
5  * and/or modify it under the terms of the GNU General Public
6  * License (version 2) as published by the FSF - Free Software
7  * Foundation
8  */
9
10 /* Syscheck decoder */
11
12 #include "eventinfo.h"
13 #include "os_regex/os_regex.h"
14 #include "config.h"
15 #include "alerts/alerts.h"
16 #include "decoder.h"
17
18 #ifdef SQLITE_ENABLED
19 #include "syscheck-sqlite.h"
20 #endif
21
22 typedef struct __sdb {
23     char buf[OS_MAXSTR + 1];
24     char comment[OS_MAXSTR + 1];
25
26     char size[OS_FLSIZE + 1];
27     char perm[OS_FLSIZE + 1];
28     char owner[OS_FLSIZE + 1];
29     char gowner[OS_FLSIZE + 1];
30     char md5[OS_FLSIZE + 1];
31     char sha1[OS_FLSIZE + 1];
32
33     char agent_cp[MAX_AGENTS + 1][1];
34     char *agent_ips[MAX_AGENTS + 1];
35     FILE *agent_fps[MAX_AGENTS + 1];
36
37     int db_err;
38
39     /* Ids for decoder */
40     int id1;
41     int id2;
42     int id3;
43     int idn;
44     int idd;
45
46     /* Syscheck rule */
47     OSDecoderInfo  *syscheck_dec;
48
49     /* File search variables */
50     fpos_t init_pos;
51
52 } _sdb; /* syscheck db information */
53
54 /* Local variables */
55 static _sdb sdb;
56
57 /* Extract a token from a string */
58 char *extract_token(const char *s, char *delim, int position) {
59     int count = 0;
60     char tmp[OS_MAXSTR + 1];
61     char *token;
62     strncpy(tmp,s, OS_MAXSTR);
63     token = strtok(tmp, delim);
64     while (token != NULL) {
65         count++;
66         token = strtok(NULL, delim);
67         if (count == position) {
68             return(token);
69         }
70     }
71     return(NULL);
72 }
73
74
75 /* Validate a MD5 string format */
76 int validate_md5(char *s) {
77     unsigned int i;
78     char *hex_chars = "abcdefABCDEF0123456789";
79     if (strlen(s) != 32) {
80         return(0);
81     }
82     for (i = 0; i < strlen(s); i++) {
83         if (!strchr(hex_chars, s[i])) return(0);
84     }
85     return(1);
86 }
87
88
89 /* Initialize the necessary information to process the syscheck information */
90 void SyscheckInit()
91 {
92     int i = 0;
93
94     sdb.db_err = 0;
95
96     for (; i <= MAX_AGENTS; i++) {
97         sdb.agent_ips[i] = NULL;
98         sdb.agent_fps[i] = NULL;
99         sdb.agent_cp[i][0] = '0';
100     }
101
102     /* Clear db memory */
103     memset(sdb.buf, '\0', OS_MAXSTR + 1);
104     memset(sdb.comment, '\0', OS_MAXSTR + 1);
105
106     memset(sdb.size, '\0', OS_FLSIZE + 1);
107     memset(sdb.perm, '\0', OS_FLSIZE + 1);
108     memset(sdb.owner, '\0', OS_FLSIZE + 1);
109     memset(sdb.gowner, '\0', OS_FLSIZE + 1);
110     memset(sdb.md5, '\0', OS_FLSIZE + 1);
111     memset(sdb.sha1, '\0', OS_FLSIZE + 1);
112
113     /* Create decoder */
114     os_calloc(1, sizeof(OSDecoderInfo), sdb.syscheck_dec);
115     sdb.syscheck_dec->id = getDecoderfromlist(SYSCHECK_MOD);
116     sdb.syscheck_dec->name = SYSCHECK_MOD;
117     sdb.syscheck_dec->type = OSSEC_RL;
118     sdb.syscheck_dec->fts = 0;
119
120     sdb.id1 = getDecoderfromlist(SYSCHECK_MOD);
121     sdb.id2 = getDecoderfromlist(SYSCHECK_MOD2);
122     sdb.id3 = getDecoderfromlist(SYSCHECK_MOD3);
123     sdb.idn = getDecoderfromlist(SYSCHECK_NEW);
124     sdb.idd = getDecoderfromlist(SYSCHECK_DEL);
125
126     debug1("%s: SyscheckInit completed.", ARGV0);
127     return;
128 }
129
130 /* Check if the db is completed for that specific agent */
131 #define DB_IsCompleted(x) (sdb.agent_cp[x][0] == '1')?1:0
132
133 static void __setcompleted(const char *agent)
134 {
135     FILE *fp;
136
137     /* Get agent file */
138     snprintf(sdb.buf, OS_FLSIZE , "%s/.%s.cpt", SYSCHECK_DIR, agent);
139
140     fp = fopen(sdb.buf, "w");
141     if (fp) {
142         fprintf(fp, "#!X");
143         fclose(fp);
144     }
145 }
146
147 static int __iscompleted(const char *agent)
148 {
149     FILE *fp;
150
151     /* Get agent file */
152     snprintf(sdb.buf, OS_FLSIZE , "%s/.%s.cpt", SYSCHECK_DIR, agent);
153
154     fp = fopen(sdb.buf, "r");
155     if (fp) {
156         fclose(fp);
157         return (1);
158     }
159     return (0);
160 }
161
162 /* Set the database of a specific agent as completed */
163 static void DB_SetCompleted(const Eventinfo *lf)
164 {
165     int i = 0;
166
167     /* Find file pointer */
168     while (sdb.agent_ips[i] != NULL &&  i < MAX_AGENTS) {
169         if (strcmp(sdb.agent_ips[i], lf->location) == 0) {
170             /* Return if already set as completed */
171             if (DB_IsCompleted(i)) {
172                 return;
173             }
174
175             __setcompleted(lf->location);
176
177             /* Set as completed in memory */
178             sdb.agent_cp[i][0] = '1';
179             return;
180         }
181
182         i++;
183     }
184 }
185
186
187 /* Return the file pointer to be used to verify the integrity */
188 static FILE *DB_File(const char *agent, int *agent_id)
189 {
190     int i = 0;
191
192     /* Find file pointer */
193     while (sdb.agent_ips[i] != NULL  &&  i < MAX_AGENTS) {
194         if (strcmp(sdb.agent_ips[i], agent) == 0) {
195             /* Point to the beginning of the file */
196             fseek(sdb.agent_fps[i], 0, SEEK_SET);
197             *agent_id = i;
198             return (sdb.agent_fps[i]);
199         }
200
201         i++;
202     }
203
204     /* If here, our agent wasn't found */
205     if (i == MAX_AGENTS) {
206         merror("%s: Unable to open integrity file. Increase MAX_AGENTS.", ARGV0);
207         return (NULL);
208     }
209
210     os_strdup(agent, sdb.agent_ips[i]);
211
212     /* Get agent file */
213     snprintf(sdb.buf, OS_FLSIZE , "%s/%s", SYSCHECK_DIR, agent);
214
215     /* r+ to read and write. Do not truncate */
216     sdb.agent_fps[i] = fopen(sdb.buf, "r+");
217     if (!sdb.agent_fps[i]) {
218         /* Try opening with a w flag, file probably does not exist */
219         sdb.agent_fps[i] = fopen(sdb.buf, "w");
220         if (sdb.agent_fps[i]) {
221             fclose(sdb.agent_fps[i]);
222             sdb.agent_fps[i] = fopen(sdb.buf, "r+");
223         }
224     }
225
226     /* Check again */
227     if (!sdb.agent_fps[i]) {
228         merror("%s: Unable to open '%s'", ARGV0, sdb.buf);
229
230         free(sdb.agent_ips[i]);
231         sdb.agent_ips[i] = NULL;
232         return (NULL);
233     }
234
235     /* Return the opened pointer (the beginning of it) */
236     fseek(sdb.agent_fps[i], 0, SEEK_SET);
237     *agent_id = i;
238
239     /* Check if the agent was completed */
240     if (__iscompleted(agent)) {
241         sdb.agent_cp[i][0] = '1';
242     }
243
244     return (sdb.agent_fps[i]);
245 }
246
247 /* Search the DB for any entry related to the file being received */
248 static int DB_Search(const char *f_name, const char *c_sum, Eventinfo *lf)
249 {
250     int p = 0;
251     size_t sn_size;
252     int agent_id;
253
254     char *saved_sum;
255     char *saved_name;
256
257     FILE *fp;
258
259     /* Expose filename variable for active response */
260     os_strdup(f_name, lf->filename);
261
262
263
264     /* Get db pointer */
265     fp = DB_File(lf->location, &agent_id);
266     if (!fp) {
267         merror("%s: Error handling integrity database.", ARGV0);
268         sdb.db_err++;
269         lf->data = NULL;
270         return (0);
271     }
272
273     /* Read the integrity file and search for a possible entry */
274     if (fgetpos(fp, &sdb.init_pos) == -1) {
275         merror("%s: Error handling integrity database (fgetpos).", ARGV0);
276         return (0);
277     }
278
279     /* Loop over the file */
280     while (fgets(sdb.buf, OS_MAXSTR, fp) != NULL) {
281         /* Ignore blank lines and lines with a comment */
282         if (sdb.buf[0] == '\n' || sdb.buf[0] == '#') {
283             fgetpos(fp, &sdb.init_pos); /* Get next location */
284             continue;
285         }
286
287         /* Get name */
288         saved_name = strchr(sdb.buf, ' ');
289         if (saved_name == NULL) {
290             merror("%s: Invalid integrity message in the database.", ARGV0);
291             fgetpos(fp, &sdb.init_pos); /* Get next location */
292             continue;
293         }
294         *saved_name = '\0';
295         saved_name++;
296
297         /* New format - with a timestamp */
298         if (*saved_name == '!') {
299             saved_name = strchr(saved_name, ' ');
300             if (saved_name == NULL) {
301                 merror("%s: Invalid integrity message in the database", ARGV0);
302                 fgetpos(fp, &sdb.init_pos); /* Get next location */
303                 continue;
304             }
305             saved_name++;
306         }
307
308         /* Remove newline from saved_name */
309         sn_size = strlen(saved_name);
310         sn_size -= 1;
311         if (saved_name[sn_size] == '\n') {
312             saved_name[sn_size] = '\0';
313         }
314
315         /* If name is different, go to next one */
316         if (strcmp(f_name, saved_name) != 0) {
317             /* Save current location */
318             fgetpos(fp, &sdb.init_pos);
319             continue;
320         }
321
322         saved_sum = sdb.buf;
323
324         /* First three bytes are for frequency check */
325         saved_sum += 3;
326
327         /* Checksum match, we can just return and keep going */
328         if (strcmp(saved_sum, c_sum) == 0) {
329             lf->data = NULL;
330             return (0);
331         }
332
333
334
335
336         /* If we reached here, the checksum of the file has changed */
337         if (saved_sum[-3] == '!') {
338             p++;
339             if (saved_sum[-2] == '!') {
340                 p++;
341                 if (saved_sum[-1] == '!') {
342                     p++;
343                 } else if (saved_sum[-1] == '?') {
344                     p += 2;
345                 }
346             }
347         }
348
349         /* Check the number of changes */
350         if (!Config.syscheck_auto_ignore) {
351             sdb.syscheck_dec->id = sdb.id1;
352         } else {
353             switch (p) {
354                 case 0:
355                     sdb.syscheck_dec->id = sdb.id1;
356                     break;
357
358                 case 1:
359                     sdb.syscheck_dec->id = sdb.id2;
360                     break;
361
362                 case 2:
363                     sdb.syscheck_dec->id = sdb.id3;
364                     break;
365
366                 default:
367                     lf->data = NULL;
368                     return (0);
369                     break;
370             }
371         }
372
373         /* Add new checksum to the database */
374         /* Commenting the file entry and adding a new one later */
375         if (fsetpos(fp, &sdb.init_pos)) {
376             merror("%s: Error handling integrity database (fsetpos).", ARGV0);
377             return (0);
378         }
379         fputc('#', fp);
380
381         /* Add the new entry at the end of the file */
382         fseek(fp, 0, SEEK_END);
383         fprintf(fp, "%c%c%c%s !%ld %s\n",
384                 '!',
385                 p >= 1 ? '!' : '+',
386                 p == 2 ? '!' : (p > 2) ? '?' : '+',
387                 c_sum,
388                 (long int)lf->time,
389                 f_name);
390         fflush(fp);
391
392         /* File deleted */
393         if (c_sum[0] == '-' && c_sum[1] == '1') {
394             sdb.syscheck_dec->id = sdb.idd;
395             snprintf(sdb.comment, OS_MAXSTR,
396                      "File '%.756s' was deleted. Unable to retrieve "
397                      "checksum.", f_name);
398         }
399
400         /* If file was re-added, do not compare changes */
401         else if (saved_sum[0] == '-' && saved_sum[1] == '1') {
402             sdb.syscheck_dec->id = sdb.idn;
403             snprintf(sdb.comment, OS_MAXSTR,
404                      "File '%.756s' was re-added.", f_name);
405         }
406
407         else {
408             int oldperm = 0, newperm = 0;
409
410             /* Provide more info about the file change */
411             const char *oldsize = NULL, *newsize = NULL;
412             char *olduid = NULL, *newuid = NULL;
413             char *c_oldperm = NULL, *c_newperm = NULL;
414             char *oldgid = NULL, *newgid = NULL;
415             char *oldmd5 = NULL, *newmd5 = NULL;
416             char *oldsha1 = NULL, *newsha1 = NULL;
417
418             oldsize = saved_sum;
419             newsize = c_sum;
420
421             c_oldperm = strchr(saved_sum, ':');
422             c_newperm = strchr(c_sum, ':');
423
424             /* Get old/new permissions */
425             if (c_oldperm && c_newperm) {
426                 *c_oldperm = '\0';
427                 c_oldperm++;
428
429                 *c_newperm = '\0';
430                 c_newperm++;
431
432                 /* Get old/new uid/gid */
433                 olduid = strchr(c_oldperm, ':');
434                 newuid = strchr(c_newperm, ':');
435
436                 if (olduid && newuid) {
437                     *olduid = '\0';
438                     *newuid = '\0';
439                     olduid++;
440                     newuid++;
441
442                     oldgid = strchr(olduid, ':');
443                     newgid = strchr(newuid, ':');
444
445                     if (oldgid && newgid) {
446                         *oldgid = '\0';
447                         *newgid = '\0';
448                         oldgid++;
449                         newgid++;
450
451                         /* Get MD5 */
452                         oldmd5 = strchr(oldgid, ':');
453                         newmd5 = strchr(newgid, ':');
454
455                         if (oldmd5 && newmd5) {
456                             *oldmd5 = '\0';
457                             *newmd5 = '\0';
458                             oldmd5++;
459                             newmd5++;
460
461                             /* Get SHA-1 */
462                             oldsha1 = strchr(oldmd5, ':');
463                             newsha1 = strchr(newmd5, ':');
464
465                             if (oldsha1 && newsha1) {
466                                 *oldsha1 = '\0';
467                                 *newsha1 = '\0';
468                                 oldsha1++;
469                                 newsha1++;
470                             }
471                         }
472                     }
473                 }
474             }
475
476             /* Get integer values */
477             if (c_newperm && c_oldperm) {
478                 newperm = atoi(c_newperm);
479                 oldperm = atoi(c_oldperm);
480             }
481
482             /* Generate size message */
483             if (!oldsize || !newsize || strcmp(oldsize, newsize) == 0) {
484                 sdb.size[0] = '\0';
485             } else {
486                 snprintf(sdb.size, OS_FLSIZE,
487                          "Size changed from '%s' to '%s'\n",
488                          oldsize, newsize);
489
490                 os_strdup(oldsize, lf->size_before);
491                 os_strdup(newsize, lf->size_after);
492             }
493
494             /* Permission message */
495             if (oldperm == newperm) {
496                 sdb.perm[0] = '\0';
497             } else if (oldperm > 0 && newperm > 0) {
498                 char opstr[10];
499                 char npstr[10];
500
501                 strncpy(opstr, agent_file_perm(c_oldperm), sizeof(opstr) - 1);
502                 strncpy(npstr, agent_file_perm(c_newperm), sizeof(npstr) - 1);
503
504                 snprintf(sdb.perm, OS_FLSIZE, "Permissions changed from "
505                          "'%9.9s' to '%9.9s'\n", opstr, npstr);
506
507                 lf->perm_before = oldperm;
508                 lf->perm_after = newperm;
509             }
510
511             /* Ownership message */
512             if (!newuid || !olduid || strcmp(newuid, olduid) == 0) {
513                 sdb.owner[0] = '\0';
514             } else {
515                 snprintf(sdb.owner, OS_FLSIZE, "Ownership was '%s', "
516                          "now it is '%s'\n",
517                          olduid, newuid);
518
519
520                 os_strdup(olduid, lf->owner_before);
521                 os_strdup(newuid, lf->owner_after);
522             }
523
524             /* Group ownership message */
525             if (!newgid || !oldgid || strcmp(newgid, oldgid) == 0) {
526                 sdb.gowner[0] = '\0';
527             } else {
528                 snprintf(sdb.gowner, OS_FLSIZE, "Group ownership was '%s', "
529                          "now it is '%s'\n",
530                          oldgid, newgid);
531                 os_strdup(oldgid, lf->gowner_before);
532                 os_strdup(newgid, lf->gowner_after);
533             }
534
535             /* MD5 message */
536             if (!newmd5 || !oldmd5 || strcmp(newmd5, oldmd5) == 0) {
537                 sdb.md5[0] = '\0';
538             } else {
539                 snprintf(sdb.md5, OS_FLSIZE, "Old md5sum was: '%s'\n"
540                          "New md5sum is : '%s'\n",
541                          oldmd5, newmd5);
542                 os_strdup(oldmd5, lf->md5_before);
543                 os_strdup(newmd5, lf->md5_after);
544             }
545
546             /* SHA-1 message */
547             if (!newsha1 || !oldsha1 || strcmp(newsha1, oldsha1) == 0) {
548                 sdb.sha1[0] = '\0';
549             } else {
550                 snprintf(sdb.sha1, OS_FLSIZE, "Old sha1sum was: '%s'\n"
551                          "New sha1sum is : '%s'\n",
552                          oldsha1, newsha1);
553                 os_strdup(oldsha1, lf->sha1_before);
554                 os_strdup(newsha1, lf->sha1_after);
555             }
556
557             /* Provide information about the file */
558             snprintf(sdb.comment, OS_MAXSTR, "Integrity checksum changed for: "
559                      "'%.756s'\n"
560                      "%s"
561                      "%s"
562                      "%s"
563                      "%s"
564                      "%s"
565                      "%s"
566                      "%s%s",
567                      f_name,
568                      sdb.size,
569                      sdb.perm,
570                      sdb.owner,
571                      sdb.gowner,
572                      sdb.md5,
573                      sdb.sha1,
574                      lf->data == NULL ? "" : "What changed:\n",
575                      lf->data == NULL ? "" : lf->data
576                     );
577         }
578
579         /* Create a new log message */
580         free(lf->full_log);
581         os_strdup(sdb.comment, lf->full_log);
582         lf->log = lf->full_log;
583         lf->data = NULL;
584
585         /* Set decoder */
586         lf->decoder_info = sdb.syscheck_dec;
587
588         return (1);
589
590     } /* Continue */
591
592     /* If we reach here, this file is not present in our database */
593     fseek(fp, 0, SEEK_END);
594     fprintf(fp, "+++%s !%ld %s\n", c_sum, (long int)lf->time, f_name);
595     fflush(fp);
596
597     /* Alert if configured to notify on new files */
598     /* TODO: debugging this - Scott */
599     /* if ((Config.syscheck_alert_new == 1) && (DB_IsCompleted(agent_id))) { */
600     if (Config.syscheck_alert_new == 1)  {
601         sdb.syscheck_dec->id = sdb.idn;
602
603         /* New file message */
604         snprintf(sdb.comment, OS_MAXSTR,
605                  "New file '%.756s' "
606                  "added to the file system.", f_name);
607
608
609         /* Create a new log message */
610         free(lf->full_log);
611         os_strdup(sdb.comment, lf->full_log);
612         lf->log = lf->full_log;
613
614         /* Set decoder */
615         lf->decoder_info = sdb.syscheck_dec;
616         lf->data = NULL;
617
618         return (1);
619     }
620
621     lf->data = NULL;
622     return (0);
623 }
624
625 /* Special decoder for syscheck
626  * Not using the default decoding lib for simplicity
627  * and to be less resource intensive
628  */
629 int DecodeSyscheck(Eventinfo *lf)
630 {
631     const char *c_sum;
632     char *f_name;
633
634 #ifdef SQLITE_ENABLED
635     char *p;
636     char stmt[OS_MAXSTR + 1];
637     sqlite3_stmt *res;
638     int error = 0;
639     int rec_count = 0;
640     const char *tail;
641 #endif // SQLITE_ENABLED
642
643     /* Every syscheck message must be in the following format:
644      * checksum filename
645      */
646     f_name = strchr(lf->log, ' ');
647     if (f_name == NULL) {
648         /* If we don't have a valid syscheck message, it may be
649          * a database completed message
650          */
651         if (strcmp(lf->log, HC_SK_DB_COMPLETED) == 0) {
652             DB_SetCompleted(lf);
653             return (0);
654         }
655
656         merror(SK_INV_MSG, ARGV0);
657         return (0);
658     }
659
660     /* Zero to get the check sum */
661     *f_name = '\0';
662     f_name++;
663
664     /* Get diff */
665     lf->data = strchr(f_name, '\n');
666     if (lf->data) {
667         *lf->data = '\0';
668         lf->data++;
669     } else {
670         lf->data = NULL;
671     }
672
673     /* Check if file is supposed to be ignored */
674     if (Config.syscheck_ignore) {
675         char **ff_ig = Config.syscheck_ignore;
676
677         while (*ff_ig) {
678             if (strncasecmp(*ff_ig, f_name, strlen(*ff_ig)) == 0) {
679                 lf->data = NULL;
680                 return (0);
681             }
682
683             ff_ig++;
684         }
685     }
686
687     /* Checksum is at the beginning of the log */
688     c_sum = lf->log;
689
690     /* Extract the MD5 hash and search for it in the allowlist
691      * Sample message:
692      * 0:0:0:0:78f5c869675b1d09ddad870adad073f9:bd6c8d7a58b462aac86475e59af0e22954039c50
693      */
694 #ifdef SQLITE_ENABLED
695     if (Config.md5_allowlist)  {
696         extern sqlite3 *conn;
697         if ((p = extract_token(c_sum, ":", 4))) {
698             if (!validate_md5(p)) { /* Never trust input from other origin */
699                 merror("%s: Not a valid MD5 hash: '%s'", ARGV0, p);
700                 return(0);
701             }
702             debug1("%s: Checking MD5 '%s' in %s", ARGV0, p, Config.md5_allowlist);
703             sprintf(stmt, "select md5sum from files where md5sum = \"%s\"", p);
704             error = sqlite3_prepare_v2(conn, stmt, 1000, &res, &tail);
705             if (error == SQLITE_OK) {
706                 while (sqlite3_step(res) == SQLITE_ROW) {
707                     rec_count++;
708                 }
709                 if (rec_count) {    
710                     sqlite3_finalize(res);
711                     //sqlite3_close(conn);
712                     merror(MD5_NOT_CHECKED, ARGV0, p);
713                     return(0);
714                 }
715             }
716             sqlite3_finalize(res);
717         }
718     }
719 #endif
720  
721
722     /* Search for file changes */
723     return (DB_Search(f_name, c_sum, lf));
724 }
725