1094 строки
30 KiB
C
1094 строки
30 KiB
C
/* vim: ts=4:sw=4:noexpandtab
|
|
* audisp-json.c --
|
|
* Copyright (c) 2014 Mozilla Corporation.
|
|
* Portions Copyright 2008 Red Hat Inc., Durham, North Carolina.
|
|
* All Rights Reserved.
|
|
*
|
|
* 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; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*
|
|
* Authors:
|
|
* Guillaume Destuynder <gdestuynder@mozilla.com>
|
|
* Steve Grubb <sgrubb@redhat.com>
|
|
*
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <signal.h>
|
|
#include <syslog.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <pwd.h>
|
|
#include <netdb.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <curl/curl.h>
|
|
#include "libaudit.h"
|
|
#include "auparse.h"
|
|
#include "json-config.h"
|
|
|
|
#define CONFIG_FILE "/etc/audisp/audisp-json.conf"
|
|
#define CONFIG_FILE_LOCAL "audisp-json.conf"
|
|
/* after this amount of time for any response (connect, http reply, etc.) just give up
|
|
* and lose messages.
|
|
* don't set this too high as new curl handles will be created and consume memory while
|
|
* waiting for the connection to work again.
|
|
*/
|
|
#define MAX_CURL_GLOBAL_TIMEOUT 5000L
|
|
#define RING_BUF_LEN 512
|
|
#define MAX_JSON_MSG_SIZE 4096
|
|
#define MAX_ARG_LEN 2048
|
|
#define MAX_SUMMARY_LEN 256
|
|
#define MAX_ATTR_SIZE 1023
|
|
#ifdef REORDER_HACK
|
|
#define NR_LINES_BUFFERED 64
|
|
#endif
|
|
|
|
#define HTTP_CODE_OK 200
|
|
|
|
#ifndef PROGRAM_VERSION
|
|
#define PROGRAM_VERSION "1"
|
|
#endif
|
|
#ifndef PROGRAM_NAME
|
|
#define PROGRAM_NAME "audisp-json"
|
|
#endif
|
|
/* transform macro int and str value to ... str - needed for defining USER_AGENT ;)*/
|
|
#define _STR(x) #x
|
|
#define STR(x) _STR(x)
|
|
#define USER_AGENT PROGRAM_NAME"/"STR(PROGRAM_VERSION)
|
|
|
|
extern int h_errno;
|
|
|
|
static volatile int stop = 0;
|
|
static volatile int hup = 0;
|
|
static json_conf_t config;
|
|
static char *hostname = NULL;
|
|
static auparse_state_t *au = NULL;
|
|
static int machine = -1;
|
|
|
|
static long int curl_timeout = -1;
|
|
CURLM *multi_h;
|
|
CURL *easy_h;
|
|
struct curl_slist *slist1;
|
|
|
|
typedef struct { char *val; } msg_t;
|
|
typedef struct ring_buf_msg {
|
|
int size;
|
|
int start;
|
|
int end;
|
|
msg_t *data;
|
|
} ring_buf_msg_t;
|
|
|
|
static ring_buf_msg_t msg_list;
|
|
|
|
typedef struct ll {
|
|
char value[MAX_ATTR_SIZE];
|
|
struct ll *next;
|
|
} attr_t;
|
|
|
|
struct json_msg_type {
|
|
char *category;
|
|
char *summary;
|
|
char *severity;
|
|
char *hostname;
|
|
int processid;
|
|
char *processname;
|
|
char *timestamp;
|
|
struct ll *details;
|
|
};
|
|
|
|
/* ring buffer functions:
|
|
* we've to keep the data we send to MozDef around for cURL.
|
|
* in case we get overloaded with messages, we don't want to DOS ourselves so we're limited to the size of the ring
|
|
* buffer.
|
|
*/
|
|
|
|
int ring_full(ring_buf_msg_t *rb)
|
|
{
|
|
return rb->end == (rb->start ^ rb->size);
|
|
}
|
|
|
|
int ring_empty(ring_buf_msg_t *rb)
|
|
{
|
|
return rb->end == rb->start;
|
|
}
|
|
|
|
int ring_add(ring_buf_msg_t *rb, int p)
|
|
{
|
|
return (p + 1)&(2*rb->size-1);
|
|
}
|
|
|
|
void ring_write(ring_buf_msg_t *rb, char *val)
|
|
{
|
|
rb->data[rb->end&(rb->size-1)].val = val;
|
|
if (ring_full(rb))
|
|
rb->start = ring_add(rb, rb->start);
|
|
rb->end = ring_add(rb, rb->end);
|
|
}
|
|
|
|
char *ring_read(ring_buf_msg_t *rb)
|
|
{
|
|
char *val;
|
|
val = rb->data[rb->start&(rb->size-1)].val;
|
|
rb->start = ring_add(rb, rb->start);
|
|
return val;
|
|
}
|
|
|
|
void prepare_curl_handle(void)
|
|
{
|
|
curl_easy_reset(easy_h);
|
|
curl_easy_setopt(easy_h, CURLOPT_URL, config.mozdef_url);
|
|
curl_easy_setopt(easy_h, CURLOPT_NOPROGRESS, 1L);
|
|
curl_easy_setopt(easy_h, CURLOPT_USERAGENT, USER_AGENT);
|
|
curl_easy_setopt(easy_h, CURLOPT_HTTPHEADER, slist1);
|
|
curl_easy_setopt(easy_h, CURLOPT_MAXREDIRS, 10L);
|
|
curl_easy_setopt(easy_h, CURLOPT_CUSTOMREQUEST, "POST");
|
|
/* keep alive is on by default and only settable in recent libcurl
|
|
* keeping this around in case its not actually default in some cases and needs
|
|
* to be conditionally enabled
|
|
*/
|
|
// curl_easy_setopt(easy_h, CURLOPT_TCP_KEEPALIVE, 1L);
|
|
curl_easy_setopt(easy_h, CURLOPT_VERBOSE, config.curl_verbose);
|
|
curl_easy_setopt(easy_h, CURLOPT_TIMEOUT_MS, MAX_CURL_GLOBAL_TIMEOUT);
|
|
curl_easy_setopt(easy_h, CURLOPT_SSL_VERIFYHOST, config.ssl_verify);
|
|
curl_easy_setopt(easy_h, CURLOPT_SSL_VERIFYPEER, config.ssl_verify);
|
|
curl_easy_setopt(easy_h, CURLOPT_CAINFO, config.curl_cainfo);
|
|
}
|
|
|
|
/* select and fetch urls */
|
|
void curl_perform(void)
|
|
{
|
|
int msgs_left;
|
|
int curl_nr_h = -1;
|
|
int maxfd = -1;
|
|
long http_code = 0;
|
|
struct timeval timeout;
|
|
int rc;
|
|
CURLMsg *msg;
|
|
CURL *eh;
|
|
CURLcode ret;
|
|
|
|
while (curl_nr_h != 0) {
|
|
fd_set r, w, e;
|
|
FD_ZERO(&r);
|
|
FD_ZERO(&w);
|
|
FD_ZERO(&e);
|
|
|
|
/* With cURL you get the timeout you have to wait back from the library, so we use that for the select() call */
|
|
ret = curl_multi_timeout(multi_h, &curl_timeout);
|
|
if (ret != CURLM_OK) {
|
|
syslog(LOG_ERR, "%s", curl_multi_strerror(ret));
|
|
}
|
|
timeout.tv_sec = 1;
|
|
timeout.tv_usec = 0;
|
|
if (curl_timeout >= 0) {
|
|
timeout.tv_sec = curl_timeout / 1000;
|
|
if (timeout.tv_sec > 1)
|
|
timeout.tv_sec = 1;
|
|
else
|
|
timeout.tv_usec = (curl_timeout % 1000) * 1000;
|
|
}
|
|
ret = curl_multi_fdset(multi_h, &r, &w, &e, &maxfd);
|
|
if (ret != CURLM_OK) {
|
|
syslog(LOG_ERR, "%s", curl_multi_strerror(ret));
|
|
return;
|
|
}
|
|
|
|
rc = select(maxfd+1, &r, &w, &e, &timeout);
|
|
|
|
switch(rc) {
|
|
case -1:
|
|
syslog(LOG_ERR, "%s", strerror(errno));
|
|
break;
|
|
case 0:
|
|
default:
|
|
/* This also sets curl_nr_h to exactly 0 if all the handles have been processed. */
|
|
while ((ret = curl_multi_perform(multi_h, &curl_nr_h)) && (ret == CURLM_CALL_MULTI_PERFORM)) {
|
|
continue;
|
|
}
|
|
if (ret != CURLM_OK) {
|
|
syslog(LOG_ERR, "%s", curl_multi_strerror(ret));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Cleanup completed handles */
|
|
while (msg = curl_multi_info_read(multi_h, &msgs_left)) {
|
|
if (msg->msg == CURLMSG_DONE) {
|
|
ret = curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_code);
|
|
if (ret != CURLM_OK) {
|
|
syslog(LOG_ERR, "Couldn't send JSON message (message is lost): %s.", curl_easy_strerror(ret));
|
|
}
|
|
if (http_code != HTTP_CODE_OK) {
|
|
syslog(LOG_ERR, "Couldn't send JSON message (message is lost): HTTP error code %ld.", http_code);
|
|
}
|
|
}
|
|
}
|
|
|
|
while (!ring_empty(&msg_list)) {
|
|
char *new_msg = ring_read(&msg_list);
|
|
curl_multi_remove_handle(multi_h, easy_h);
|
|
prepare_curl_handle();
|
|
curl_easy_setopt(easy_h, CURLOPT_COPYPOSTFIELDS, new_msg);
|
|
free(new_msg);
|
|
curl_multi_add_handle(multi_h, easy_h);
|
|
}
|
|
}
|
|
|
|
static void handle_event(auparse_state_t *au,
|
|
auparse_cb_event_t cb_event_type, void *user_data);
|
|
|
|
static void term_handler( int sig )
|
|
{
|
|
stop = 1;
|
|
}
|
|
|
|
static void hup_handler( int sig )
|
|
{
|
|
hup = 1;
|
|
}
|
|
|
|
static void reload_config(void)
|
|
{
|
|
hup = 0;
|
|
}
|
|
|
|
#ifdef REORDER_HACK
|
|
/*
|
|
* Hack to reorder input
|
|
* libaudit's auparse seems not to correlate messages correctly if event ids are out of sequence, ex (event id are
|
|
* 418143181 and 418143182):
|
|
* type=EXECVE msg=audit(1418253698.016:418143181): argc=3 a0="sh" a1="-c" a2=[redacted]
|
|
* type=EXECVE msg=audit(1418253698.016:418143182): argc=3 a0="sh" a1="-c" a2=[redacted]
|
|
* type=CWD msg=audit(1418253698.016:418143181): cwd="/opt/observium"
|
|
* type=CWD msg=audit(1418253698.016:418143182): cwd="/opt/observium"
|
|
*
|
|
* This hack sort them back so that event ids are back to back like this:
|
|
* type=EXECVE msg=audit(1418253698.016:418143181): argc=3 a0="sh" a1="-c" a2=[redacted]
|
|
* type=CWD msg=audit(1418253698.016:418143181): cwd="/opt/observium"
|
|
* type=EXECVE msg=audit(1418253698.016:418143182): argc=3 a0="sh" a1="-c" a2=[redacted]
|
|
* type=CWD msg=audit(1418253698.016:418143182): cwd="/opt/observium"
|
|
*
|
|
* Without the hack, when the event id correlation fails, auparse would only return the parsed event until the point of
|
|
* failure (so basically half of the message will be missing from the event/fields will be empty...)
|
|
*
|
|
* WARNING: The hack relies on properly null terminated strings here and there and doesn't do much bound checking other
|
|
* than that. Be careful.
|
|
* NOTE: This hack is only necessary when you can't fix libaudit easily, obviously. It's neither nice neither all that fast.
|
|
*/
|
|
|
|
/* count occurences of c in *in */
|
|
unsigned int strcharc(char *in, char c)
|
|
{
|
|
unsigned int i = 0;
|
|
|
|
for (i = 0; in[i]; in[i] == c ? i++ : *in++);
|
|
return i;
|
|
}
|
|
|
|
static int eventcmp(const void *p1, const void *p2)
|
|
{
|
|
char *s1, *s2;
|
|
char *a1, *a2;
|
|
int i;
|
|
s1 = *(char * const*)p1;
|
|
s2 = *(char * const*)p2;
|
|
|
|
if (!s1 || !s2)
|
|
return 0;
|
|
|
|
a1 = s1;
|
|
i = 0;
|
|
while (a1[0] != ':' && a1[0] != '\0' && i < MAX_AUDIT_MESSAGE_LENGTH) {
|
|
i++;
|
|
a1++;
|
|
}
|
|
|
|
a2 = s2;
|
|
i = 0;
|
|
while (a2[0] != ':' && a2[0] != '\0' && i < MAX_AUDIT_MESSAGE_LENGTH) {
|
|
i++;
|
|
a2++;
|
|
}
|
|
|
|
return strcmp(a1, a2);
|
|
}
|
|
|
|
size_t reorder_input_hack(char **sorted_tmp, char *tmp)
|
|
{
|
|
unsigned int lines = 0;
|
|
unsigned int llen = 0;
|
|
size_t flen = 0;
|
|
unsigned int i = 0;
|
|
lines = strcharc(tmp, '\n');
|
|
|
|
char *buf[lines];
|
|
char *line;
|
|
char *saved;
|
|
|
|
line = strtok_r(tmp, "\n", &saved);
|
|
if (!line) {
|
|
syslog(LOG_ERR, "message has no LF, message lost!");
|
|
return 0;
|
|
}
|
|
|
|
llen = strnlen(line, MAX_AUDIT_MESSAGE_LENGTH);
|
|
buf[i] = malloc(llen + 1);
|
|
if (!buf[i]) {
|
|
*sorted_tmp = tmp;
|
|
syslog(LOG_ERR, "reorder_input_hack() malloc failed won't reorder");
|
|
return strnlen(tmp, MAX_AUDIT_MESSAGE_LENGTH);
|
|
}
|
|
snprintf(buf[i], llen+1, "%s", line);
|
|
i++;
|
|
|
|
for (i; i < lines; i++) {
|
|
line = strtok_r(NULL, "\n", &saved);
|
|
if (!line) {
|
|
continue;
|
|
}
|
|
llen = strnlen(line, MAX_AUDIT_MESSAGE_LENGTH);
|
|
buf[i] = malloc(llen + 1);
|
|
if (!buf[i]) {
|
|
syslog(LOG_ERR, "reorder_input_hack() malloc failed partially reordering");
|
|
continue;
|
|
}
|
|
snprintf(buf[i], llen+1, "%s", line);
|
|
}
|
|
|
|
qsort(&buf, lines, sizeof(char *), eventcmp);
|
|
|
|
for (i = 0; i < lines; i++) {
|
|
flen += snprintf(*sorted_tmp+flen, MAX_AUDIT_MESSAGE_LENGTH, "%s\n", buf[i]);
|
|
if (buf[i]) {
|
|
free(buf[i]);
|
|
}
|
|
}
|
|
return flen;
|
|
}
|
|
#endif
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
char tmp[MAX_AUDIT_MESSAGE_LENGTH];
|
|
int len=0;
|
|
struct sigaction sa;
|
|
struct hostent *ht;
|
|
char nodename[64];
|
|
CURLMcode ret;
|
|
|
|
sa.sa_flags = 0;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_handler = term_handler;
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
sa.sa_handler = hup_handler;
|
|
|
|
openlog(PROGRAM_NAME, LOG_CONS, LOG_DAEMON);
|
|
|
|
if (gethostname(nodename, 63)) {
|
|
snprintf(nodename, 10, "localhost");
|
|
}
|
|
nodename[64] = '\0';
|
|
ht = gethostbyname(nodename);
|
|
if (ht == NULL) {
|
|
hostname = strdup("localhost");
|
|
syslog(LOG_ALERT,
|
|
"gethostbyname could not find machine hostname, please fix this. Using %s as fallback. Error: %s",
|
|
hostname, hstrerror(h_errno));
|
|
} else {
|
|
hostname = strdup(ht->h_name);
|
|
}
|
|
|
|
if (load_config(&config, CONFIG_FILE))
|
|
if (load_config(&config, CONFIG_FILE_LOCAL))
|
|
return 1;
|
|
|
|
au = auparse_init(AUSOURCE_FEED, NULL);
|
|
if (au == NULL) {
|
|
syslog(LOG_ERR, "could not initialize auparse");
|
|
return -1;
|
|
}
|
|
|
|
machine = audit_detect_machine();
|
|
if (machine < 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* libcurl stuff */
|
|
msg_list.size = RING_BUF_LEN;
|
|
msg_list.start = 0;
|
|
msg_list.end = 0;
|
|
msg_list.data = (msg_t *)calloc(RING_BUF_LEN, sizeof(msg_t));
|
|
|
|
if (!msg_list.data) {
|
|
syslog(LOG_ERR, "calloc() failed");
|
|
return -1;
|
|
}
|
|
|
|
if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
|
|
syslog(LOG_ERR, "curl_global_init() failed");
|
|
return -1;
|
|
}
|
|
|
|
easy_h = curl_easy_init();
|
|
multi_h = curl_multi_init();
|
|
slist1 = NULL;
|
|
slist1 = curl_slist_append(slist1, "Content-Type:application/json");
|
|
if (!(easy_h && multi_h && slist1)) {
|
|
syslog(LOG_ERR, "cURL handles creation failed, this is fatal");
|
|
return -1;
|
|
}
|
|
|
|
#ifdef REORDER_HACK
|
|
int start = 0;
|
|
int stop = 0;
|
|
int i = 0;
|
|
char *full_str_tmp = malloc(NR_LINES_BUFFERED*MAX_AUDIT_MESSAGE_LENGTH);
|
|
char *sorted_tmp = malloc(NR_LINES_BUFFERED*MAX_AUDIT_MESSAGE_LENGTH);
|
|
if (!sorted_tmp || !full_str_tmp) {
|
|
syslog(LOG_ERR, "main() malloc failed for sorted_tmp || full_str_tmp, this is fatal");
|
|
return -1;
|
|
}
|
|
sorted_tmp[0] = '\0';
|
|
full_str_tmp[0] = '\0';
|
|
#endif
|
|
|
|
auparse_add_callback(au, handle_event, NULL, NULL);
|
|
syslog(LOG_INFO, "%s loaded\n", PROGRAM_NAME);
|
|
|
|
/* At this point we're initialized so we'll read stdin until closed and feed the data to auparse, which in turn will
|
|
* call our callback (handle_event) every time it finds a new complete message to parse.
|
|
*/
|
|
do {
|
|
if (hup)
|
|
reload_config();
|
|
|
|
/* NOTE: There's quite a few reasons for auparse_feed() from libaudit to fail parsing silently so we have to be careful here.
|
|
* Anything passed to it:
|
|
* - must have the same timestamp for a given event id. (kernel takes care of that, if not, you're out of luck).
|
|
* - must always be LF+NULL terminated ("\n\0"). (fgets takes care of that even thus it's not nearly as fast as fread).
|
|
* - must always have event ids in sequential order. (REORDER_HACK takes care of that, it also buffer lines, since, well, it needs to).
|
|
*/
|
|
while (fgets_unlocked(tmp, MAX_AUDIT_MESSAGE_LENGTH, stdin)) {
|
|
len = strnlen(tmp, MAX_AUDIT_MESSAGE_LENGTH);
|
|
#ifdef REORDER_HACK
|
|
if (strncmp(tmp, "type=EOE", 8) == 0) {
|
|
stop++;
|
|
} else if (strncmp(tmp, "type=SYSCALL", 12) == 0) {
|
|
start++;
|
|
}
|
|
if (i > NR_LINES_BUFFERED || start != stop) {
|
|
strncat(full_str_tmp, tmp, len);
|
|
i++;
|
|
} else {
|
|
strncat(full_str_tmp, tmp, len);
|
|
len = reorder_input_hack(&sorted_tmp, full_str_tmp);
|
|
auparse_feed(au, sorted_tmp, len);
|
|
i = 0;
|
|
stop = stop = 0;
|
|
sorted_tmp[0] = '\0';
|
|
full_str_tmp[0] = '\0';
|
|
}
|
|
#else
|
|
auparse_feed(au, tmp, len);
|
|
#endif
|
|
}
|
|
|
|
if (feof(stdin))
|
|
break;
|
|
} while (stop == 0);
|
|
|
|
auparse_flush_feed(au);
|
|
|
|
while (!ring_empty(&msg_list)) {
|
|
curl_perform();
|
|
}
|
|
|
|
auparse_destroy(au);
|
|
curl_global_cleanup();
|
|
free(msg_list.data);
|
|
free_config(&config);
|
|
free(hostname);
|
|
#ifdef REORDER_HACK
|
|
free(sorted_tmp);
|
|
#endif
|
|
syslog(LOG_INFO, "%s unloaded\n", PROGRAM_NAME);
|
|
closelog();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* This function seeks to the specified record returning its type on succees
|
|
*/
|
|
static int goto_record_type(auparse_state_t *au, int type)
|
|
{
|
|
int cur_type;
|
|
|
|
auparse_first_record(au);
|
|
do {
|
|
cur_type = auparse_get_type(au);
|
|
if (cur_type == type) {
|
|
auparse_first_field(au);
|
|
return type; // Normal exit
|
|
}
|
|
} while (auparse_next_record(au) > 0);
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Removes quotes
|
|
* Remove CR and LF
|
|
* @const char *in: if NULL, no processing is done.
|
|
*/
|
|
char *unescape(const char *in)
|
|
{
|
|
if (in == NULL)
|
|
return NULL;
|
|
|
|
char *dst = (char *)in;
|
|
char *s = dst;
|
|
char *src = (char *)in;
|
|
char c;
|
|
|
|
while ((c = *src++) != '\0') {
|
|
if ((c == '"') || (c == '\n') || (c == '\r') || (c == '\t')
|
|
|| (c == '\b') || (c == '\f') || (c == '\\'))
|
|
continue;
|
|
*dst++ = c;
|
|
}
|
|
*dst++ = '\0';
|
|
return s;
|
|
}
|
|
|
|
/* Add a field to the json msg's details={}
|
|
* @attr_t *list: the attribute list to extend
|
|
* @const char *st: the attribute name to add
|
|
* @const char *val: the attribut value - if NULL, we won't add the field to the json message at all.
|
|
*/
|
|
attr_t *json_add_attr(attr_t *list, const char *st, const char *val)
|
|
{
|
|
attr_t *new;
|
|
|
|
if (st == NULL || !strncmp(st, "(null)", 6) || val == NULL || !strncmp(val, "(null)", 6)) {
|
|
return list;
|
|
}
|
|
|
|
new = malloc(sizeof(attr_t));
|
|
if (!new) {
|
|
syslog(LOG_ERR, "json_add_attr() malloc failed attribute will be empty: %s", st);
|
|
return list;
|
|
}
|
|
snprintf(new->value, MAX_ATTR_SIZE, "\t\t\"%s\": \"%s\"", st, unescape(val));
|
|
new->next = list;
|
|
return new;
|
|
}
|
|
|
|
void json_del_attrs(attr_t *head)
|
|
{
|
|
attr_t *prev;
|
|
while (head) {
|
|
prev = head;
|
|
head = head->next;
|
|
free(prev);
|
|
}
|
|
}
|
|
|
|
/* Resolve uid to username */
|
|
char *get_username(int uid)
|
|
{
|
|
size_t bufsize;
|
|
char *buf;
|
|
char *name;
|
|
struct passwd pwd;
|
|
struct passwd *result;
|
|
|
|
bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
|
|
if (bufsize == -1)
|
|
bufsize = 16384;
|
|
buf = (char *)alloca(bufsize);
|
|
if (!buf) {
|
|
return NULL;
|
|
}
|
|
|
|
if (uid == -1) {
|
|
return NULL;
|
|
}
|
|
if (getpwuid_r(uid, &pwd, buf, bufsize, &result) != 0) {
|
|
return NULL;
|
|
}
|
|
if (result == NULL) {
|
|
return NULL;
|
|
}
|
|
name = strdupa(pwd.pw_name);
|
|
return name;
|
|
}
|
|
|
|
/* Resolve process name from pid */
|
|
char *get_proc_name(int pid)
|
|
{
|
|
char p[1024];
|
|
int ret;
|
|
static char proc[64];
|
|
FILE *fp;
|
|
snprintf(p, 512, "/proc/%d/status", pid);
|
|
fp = fopen(p, "r");
|
|
if (fp) {
|
|
ret = fscanf(fp, "Name: %63s", proc);
|
|
fclose(fp);
|
|
} else
|
|
return NULL;
|
|
|
|
if (ret == 0)
|
|
return NULL;
|
|
|
|
return proc;
|
|
}
|
|
|
|
/* This creates the JSON message we'll send over by deserializing the C struct into a char array */
|
|
void syslog_json_msg(struct json_msg_type json_msg)
|
|
{
|
|
attr_t *head = json_msg.details;
|
|
attr_t *prev;
|
|
char *msg;
|
|
int len;
|
|
|
|
msg = malloc((size_t)MAX_JSON_MSG_SIZE);
|
|
if (!msg) {
|
|
syslog(LOG_ERR, "syslog_json_msg() malloc failed, message lost!");
|
|
return;
|
|
}
|
|
|
|
len = snprintf(msg, MAX_JSON_MSG_SIZE,
|
|
"{\n\
|
|
\"category\": \"%s\",\n\
|
|
\"summary\": \"%s\",\n\
|
|
\"severity\": \"%s\",\n\
|
|
\"hostname\": \"%s\",\n\
|
|
\"processid\": \"%i\",\n\
|
|
\"processname\": \"%s\",\n\
|
|
\"timestamp\": \"%s\",\n\
|
|
\"tags\": [\n\
|
|
\"%s\",\n\
|
|
\"%s\",\n\
|
|
\"audit\"\n\
|
|
],\n\
|
|
\"details\": {",
|
|
json_msg.category, json_msg.summary, json_msg.severity, json_msg.hostname, json_msg.processid,
|
|
PROGRAM_NAME, json_msg.timestamp, PROGRAM_NAME, STR(PROGRAM_VERSION));
|
|
|
|
while (head) {
|
|
len += snprintf(msg+len, MAX_JSON_MSG_SIZE, "\n%s,", head->value);
|
|
prev = head;
|
|
head = head->next;
|
|
free(prev);
|
|
|
|
if (head == NULL) {
|
|
msg[len-1] = '\n';
|
|
}
|
|
}
|
|
|
|
len += snprintf(msg+len, MAX_JSON_MSG_SIZE, " }\n}");
|
|
msg[MAX_JSON_MSG_SIZE-1] = '\0';
|
|
|
|
ring_write(&msg_list, msg);
|
|
#ifdef DEBUG
|
|
printf("%s\n", msg);
|
|
#endif
|
|
}
|
|
|
|
/* The main event handling, parsing function */
|
|
static void handle_event(auparse_state_t *au,
|
|
auparse_cb_event_t cb_event_type, void *user_data)
|
|
{
|
|
int type, rc, num=0;
|
|
|
|
struct json_msg_type json_msg = {
|
|
.category = NULL,
|
|
.hostname = hostname,
|
|
.processid = 0,
|
|
.processname = NULL,
|
|
.severity = "INFO",
|
|
.summary = NULL,
|
|
.timestamp = NULL,
|
|
.details = NULL,
|
|
};
|
|
|
|
typedef enum {
|
|
CAT_EXECVE,
|
|
CAT_WRITE,
|
|
CAT_PTRACE,
|
|
CAT_ATTR,
|
|
CAT_APPARMOR,
|
|
CAT_CHMOD,
|
|
CAT_CHOWN,
|
|
CAT_PROMISC
|
|
} category_t;
|
|
category_t category;
|
|
|
|
const char *cwd = NULL, *argc = NULL, *cmd = NULL;
|
|
const char *path = NULL;
|
|
const char *dev = NULL;
|
|
const char *sys;
|
|
const char *syscall = NULL;
|
|
char fullcmd[MAX_ARG_LEN+1] = "\0";
|
|
char serial[64] = "\0";
|
|
time_t t;
|
|
struct tm *tmp;
|
|
|
|
char f[8];
|
|
int len, tmplen;
|
|
int argcount, i;
|
|
int promisc;
|
|
int havejson = 0;
|
|
|
|
/* wait until the lib gives up a full/ready event */
|
|
if (cb_event_type != AUPARSE_CB_EVENT_READY) {
|
|
return;
|
|
}
|
|
|
|
json_msg.timestamp = (char *)alloca(64);
|
|
json_msg.summary = (char *)alloca(MAX_SUMMARY_LEN);
|
|
if (!json_msg.summary || !json_msg.timestamp) {
|
|
syslog(LOG_ERR, "handle_event() alloca failed, message lost!");
|
|
return;
|
|
}
|
|
|
|
while (auparse_goto_record_num(au, num) > 0) {
|
|
type = auparse_get_type(au);
|
|
rc = 0;
|
|
|
|
if (!auparse_first_field(au))
|
|
continue;
|
|
|
|
t = auparse_get_time(au);
|
|
tmp = localtime(&t);
|
|
strftime(json_msg.timestamp, 64, "%FT%T%z", tmp);
|
|
snprintf(serial, 63, "%lu", auparse_get_serial(au));
|
|
json_msg.details = json_add_attr(json_msg.details, "auditserial", serial);
|
|
|
|
switch (type) {
|
|
case AUDIT_ANOM_PROMISCUOUS:
|
|
dev = auparse_find_field(au, "dev");
|
|
if (!dev)
|
|
return;
|
|
|
|
havejson = 1;
|
|
category = CAT_PROMISC;
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "dev", dev);
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "promiscious", auparse_find_field(au, "prom"));
|
|
promisc = auparse_get_field_int(au);
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "old_promicious", auparse_find_field(au, "old_prom"));
|
|
goto_record_type(au, type);
|
|
if (auparse_find_field(au, "auid")) {
|
|
json_msg.details = json_add_attr(json_msg.details, "originaluser",
|
|
get_username(auparse_get_field_int(au)));
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "originaluid", auparse_get_field_str(au));
|
|
}
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "uid")) {
|
|
json_msg.details = json_add_attr(json_msg.details, "user", get_username(auparse_get_field_int(au)));
|
|
json_msg.details = json_add_attr(json_msg.details, "uid", auparse_get_field_str(au));
|
|
}
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "gid", auparse_find_field(au, "gid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "session", auparse_find_field(au, "ses"));
|
|
goto_record_type(au, type);
|
|
break;
|
|
|
|
case AUDIT_AVC:
|
|
argc = auparse_find_field(au, "apparmor");
|
|
if (!argc)
|
|
return;
|
|
|
|
havejson = 1;
|
|
category = CAT_APPARMOR;
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "aaresult", auparse_get_field_str(au));
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.summary = unescape(auparse_find_field(au, "info"));
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "aacoperation", auparse_find_field(au, "operation"));
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "aaprofile", auparse_find_field(au, "profile"));
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "aacommand", auparse_find_field(au, "comm"));
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "parent"))
|
|
json_msg.details = json_add_attr(json_msg.details, "parentprocess",
|
|
get_proc_name(auparse_get_field_int(au)));
|
|
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "pid"))
|
|
json_msg.details = json_add_attr(json_msg.details, "processname",
|
|
get_proc_name(auparse_get_field_int(au)));
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "aaerror", auparse_find_field(au, "error"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "aaname", auparse_find_field(au, "name"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "aasrcname", auparse_find_field(au, "srcname"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "aaflags", auparse_find_field(au, "flags"));
|
|
goto_record_type(au, type);
|
|
break;
|
|
|
|
case AUDIT_EXECVE:
|
|
argc = auparse_find_field(au, "argc");
|
|
if (argc)
|
|
argcount = auparse_get_field_int(au);
|
|
else
|
|
argcount = 0;
|
|
fullcmd[0] = '\0';
|
|
len = 0;
|
|
for (i = 0; i != argcount; i++) {
|
|
goto_record_type(au, type);
|
|
tmplen = snprintf(f, 7, "a%d", i);
|
|
f[tmplen] = '\0';
|
|
cmd = auparse_find_field(au, f);
|
|
cmd = auparse_interpret_field(au);
|
|
if (!cmd)
|
|
continue;
|
|
if (MAX_ARG_LEN-strlen(fullcmd) > strlen(cmd)) {
|
|
if (len == 0)
|
|
len += sprintf(fullcmd+len, "%s", cmd);
|
|
else
|
|
len += sprintf(fullcmd+len, " %s", cmd);
|
|
}
|
|
}
|
|
json_msg.details = json_add_attr(json_msg.details, "command", fullcmd);
|
|
break;
|
|
|
|
case AUDIT_CWD:
|
|
cwd = auparse_find_field(au, "cwd");
|
|
if (cwd) {
|
|
auparse_interpret_field(au);
|
|
json_msg.details = json_add_attr(json_msg.details, "cwd", auparse_find_field(au, "cwd"));
|
|
}
|
|
break;
|
|
|
|
case AUDIT_PATH:
|
|
path = auparse_find_field(au, "name");
|
|
json_msg.details = json_add_attr(json_msg.details, "path", path);
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "inode", auparse_find_field(au, "inode"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "dev", auparse_find_field(au, "dev"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "mode", auparse_find_field(au, "mode"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "ouid", auparse_find_field(au, "ouid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "ogid", auparse_find_field(au, "ogid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "rdev", auparse_find_field(au, "rdev"));
|
|
goto_record_type(au, type);
|
|
break;
|
|
|
|
case AUDIT_SYSCALL:
|
|
syscall = auparse_find_field(au, "syscall");
|
|
if (!syscall) {
|
|
json_del_attrs(json_msg.details);
|
|
return;
|
|
}
|
|
i = auparse_get_field_int(au);
|
|
sys = audit_syscall_to_name(i, machine);
|
|
if (!sys) {
|
|
syslog(LOG_INFO, "System call %u is not supported by %s", i, PROGRAM_NAME);
|
|
json_del_attrs(json_msg.details);
|
|
return;
|
|
}
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "processname", auparse_find_field(au, "comm"));
|
|
goto_record_type(au, type);
|
|
|
|
if (!strncmp(sys, "write", 5) || !strncmp(sys, "open", 4) || !strncmp(sys, "unlink", 6) || !strncmp(sys,
|
|
"rename", 6)) {
|
|
havejson = 1;
|
|
category = CAT_WRITE;
|
|
} else if (!strncmp(sys, "setxattr", 8)) {
|
|
havejson = 1;
|
|
category = CAT_ATTR;
|
|
} else if (!strncmp(sys, "chmod", 5)) {
|
|
havejson = 1;
|
|
category = CAT_CHMOD;
|
|
} else if (!strncmp(sys, "chown", 5) || !strncmp(sys, "fchown", 6)) {
|
|
havejson = 1;
|
|
category = CAT_CHOWN;
|
|
} else if (!strncmp(sys, "ptrace", 6)) {
|
|
havejson = 1;
|
|
category = CAT_PTRACE;
|
|
} else if (!strncmp(sys, "execve", 6)) {
|
|
havejson = 1;
|
|
category = CAT_EXECVE;
|
|
} else if (!strncmp(sys, "ioctl", 6)) {
|
|
category = CAT_PROMISC;
|
|
} else {
|
|
syslog(LOG_INFO, "System call %u %s is not supported by %s", i, sys, PROGRAM_NAME);
|
|
}
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "auditkey", auparse_find_field(au, "key"));
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "ppid"))
|
|
json_msg.details = json_add_attr(json_msg.details, "parentprocess",
|
|
get_proc_name(auparse_get_field_int(au)));
|
|
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "auid")) {
|
|
json_msg.details = json_add_attr(json_msg.details, "originaluser",
|
|
get_username(auparse_get_field_int(au)));
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "originaluid", auparse_get_field_str(au));
|
|
}
|
|
goto_record_type(au, type);
|
|
|
|
if (auparse_find_field(au, "uid")) {
|
|
json_msg.details = json_add_attr(json_msg.details, "user", get_username(auparse_get_field_int(au)));
|
|
json_msg.details = json_add_attr(json_msg.details, "uid", auparse_get_field_str(au));
|
|
}
|
|
goto_record_type(au, type);
|
|
|
|
json_msg.details = json_add_attr(json_msg.details, "tty", auparse_find_field(au, "tty"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "process", auparse_find_field(au, "exe"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "pid", auparse_find_field(au, "pid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "gid", auparse_find_field(au, "gid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "euid", auparse_find_field(au, "euid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "suid", auparse_find_field(au, "suid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "fsuid", auparse_find_field(au, "fsuid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "egid", auparse_find_field(au, "egid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "sgid", auparse_find_field(au, "sgid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "fsgid", auparse_find_field(au, "fsgid"));
|
|
goto_record_type(au, type);
|
|
json_msg.details = json_add_attr(json_msg.details, "session", auparse_find_field(au, "ses"));
|
|
goto_record_type(au, type);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
num++;
|
|
}
|
|
|
|
if (!havejson) {
|
|
json_del_attrs(json_msg.details);
|
|
return;
|
|
}
|
|
|
|
/* We set the category/summary here as the JSON msg structure is complete at this point. (i.e. just before
|
|
* syslog_json_msg...) Since we don't know the order of messages, this is the only way to ensure we can fill a
|
|
* useful summary from various AUDIT messages (sometimes the values are set from AUDIT_EXECVE, sometimes AUDIT_PATH,
|
|
* and so on.
|
|
*/
|
|
|
|
if (category == CAT_EXECVE) {
|
|
#ifdef IGNORE_EMPTY_EXECVE_COMMAND
|
|
/* Didn't get a type=EXECVE message? Then fullcmd will be empty.
|
|
* This happens when executing scripts for example:
|
|
* /usr/local/bin/test.sh => exec
|
|
* => exec /bin/bash
|
|
* => kernel sends execve syscall for the bash exec without an EXECVE message but a path set to:
|
|
* dirname(script_path)/exec_name (e.g.: /usr/local/bin/bash in example above).
|
|
* then fork again for the "real" command (e.g.: /bin/bash /local/bin/test.sh).
|
|
* While it's correct we only really care for that last command (which has an EXECVE type)
|
|
* Thus we're skipping the messages without EXECVE altogether, they're mostly noise for our purposes.
|
|
* It's a little wasteful as we have to free the attributes we've allocated, but as messages can be out of order..
|
|
* .. we don't really have a choice.
|
|
*/
|
|
if (strlen(fullcmd) == 0) {
|
|
attr_t *head = json_msg.details;
|
|
attr_t *prev;
|
|
|
|
while (head) {
|
|
prev = head;
|
|
head = head->next;
|
|
free(prev);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
json_msg.category = "execve";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Execve: %s",
|
|
unescape(fullcmd));
|
|
} else if (category == CAT_WRITE) {
|
|
json_msg.category = "write";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Write: %s",
|
|
unescape(path));
|
|
} else if (category == CAT_ATTR) {
|
|
json_msg.category = "attribute";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Attribute: %s",
|
|
unescape(path));
|
|
} else if (category == CAT_CHMOD) {
|
|
json_msg.category = "chmod";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Chmod: %s",
|
|
unescape(path));
|
|
} else if (category == CAT_CHOWN) {
|
|
json_msg.category = "chown";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Chown: %s",
|
|
unescape(path));
|
|
} else if (category == CAT_PTRACE) {
|
|
json_msg.category = "ptrace";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Ptrace");
|
|
} else if (category == CAT_PROMISC) {
|
|
json_msg.category = "promiscuous";
|
|
snprintf(json_msg.summary,
|
|
MAX_SUMMARY_LEN,
|
|
"Promisc: Interface %s set promiscous %s",
|
|
unescape(dev), promisc ? "on": "off");
|
|
}
|
|
|
|
/* syslog_json_msg() also frees json_msg.details when called. */
|
|
syslog_json_msg(json_msg);
|
|
curl_perform();
|
|
}
|