Realtime in no time
The audit system can transfer events in realtime to a dispatcher program. This program, audispd, can be thought of as a multiplexor. It takes one event record at a time and gives it to the plugins.
The plugins have to be designed a certain way.
- They have to read events from stdin
- They have to handle SIGHUP and SIGTERM
- They must dequeue events as fast as possible
- They must provide a plugin config file so that audispd knows how to start it.
The plugin config file has a specific format:
active = no
direction = out
path = /sbin/audisp-example
type = always
args = test
format = string
You can tell audispd if the plugin should be started by setting active to yes. The direction should always be out. (In the past there was the idea of being able to chain plugins together to transform events. But support for that has not become a priority.) Then you tell audispd what the path is to your plugin. You can tell it if the plugin type should always be running or its a builtin plugin. Since we are making our own, it should be always. You can pass up to 2 arguments. They are hard coded meaning that audispd does not do any kind of lookup but instead passes exactly what is on this line. It can be used to point to the location of a config file or anything else you can think up. And lastly, you can tell audispd what format the plugin expects its events to be in. There are 2 formats, the native audit structure and string. For any program that is designed to use auparse, always tell it string.
The design requirement for audispd plugins to read from stdin also provides the way to debug your application. During the development you may need to test different things. But if its running under a daemon how do you see what its doing?
The way I debug audispd programs is to capture some audit events in the raw format to a file. Then I "cat" the file into plugin on the bash command line.
OK. Let's get started. When events come in during realtime, it is one record at a time. This means you can't do anything until all records in the event have landed. So, the records need to be accumulated and the program must go back to waiting for the next piece so that it can process a whole event. The nice thing is that auparse has an event feed interface that takes care of this for you. The feed API is configured with a callback function that will be called as soon as an event is detected as complete. The callback function is handed an auparse_state_t variable that has one complete event in it. You cannot loop across events because there is only one. You can loop across records and fields.
The basic structure is:
Init auparse and tell it the source will be the feed API
Init the feed API by registering a callback function
Setup asynchronous reading via select, poll, or epoll
Read in the one record
Push record into feed API
If stdin has error, cleanup and exit
Let's make simple test program that simply writes the record type and how many fields that record contains to stdout. All error checking and signal support has been removed for clarity. In a real program these must be handled.
You can ave the program as audisp-example.c and compile it like this:
Now run the program as stated earlier by cat'ing some captured raw events out of a file.
OK, great we have a working program. But its not too useful since what its doing is written to stdout. How about if we make a real program that sends a notification to the desktop anytime someone logs in? Wouldn't that be fun?
Login Alert Plugin
The first thing to know is that you cannot send a notification as root because it tries to connect to the system bus. Any program launched by audispd is started as root. So, here's the first challenge. Instead what you have to do is send it to the user's session bus that is logged in and you have to do it using their uid. We are going to cheat because working this out adds code and removes clarity of the auparse portion. You can google around for the missing pieces if you really want to develop this further. But we will cheat by creating a define that holds our account number. The program will not work unless you have the exact account number you are currently logged in with. On a default install, you are most likely to be account 1000. Adjust the define accordingly.
Save it as audisp-example2.c and compile as follows. You may need to install libnotify-devel and gdk-pixbuf2-devel before compiling:
Let's talk a bit about what the program is doing. If you look at the main function, you will see that reading events is almost exactly the same as in the first example program. We have to put the bus location into the environment for libnotify and change to the logged in user's account. But that's it for changes. The biggest difference from the previous program is in the callback function.
The callback function looks at the event type. If it is the user_login event, then we process it. If you used ausearch to grab a couple user_login events, you will notice that the event is different on success and failure. On failure we have an "acct" field with the user's account. But on success we have an "auid" field that has this information. This creates a problem that I'll show in the next blog post how to solve in a nicer way. I wanted to point this out now so that when we do this nicer in the next blog post it informs why we needed the auparse_normalizer interface.
But for now we'll look for "acct" using auparse_find_field(). If it fails, we'll reset to the beginning and scan for the "auid "field. It should be noted that if you scan for a field using auparse_find_field() and it fails to find the field, the internal cursor will have run "off the end". Auparse_find_field() will iterate across all fields and all records in an event. It will not cross the event boundary, though. Since this is a 1 record event, we can just call auparse_first_record() to place the internal cursor back at the beginning.
Creating a notice is very simple. We just call notify_notification_new() with the message and associate it with the note that we initialized with. We can also add an icon if we wanted, but for simplicity we'll skip that. We set the urgency, give it a length of time to be shown, and then we show it. After that we have to clean up by releasing allocated memory.
In any event, let's give the program a spin. Copy it to /sbin and fill out a plugin config file as follows (we don't need args so comment it out):
The plugin config file should be copied to /etc/audisp/plugins.d/audisp-example2.conf. Then disable selinux enforcement by setenforce 0. Selinux doesn't know about our plugin and will block changing to your account and sending the notification. Stop auditd service and start it. Check its status.
OK, looks good. Now ssh localhost and see what happens.
Once you understand the recipe of creating a realtime plugin, there are many things you can do. The audit system really is easy to work with. But as I hinted there are quirks that you have to know that perhaps you wished you didn't have to know. This is why the auparse_normalizer interface was created. Next blog post we will see how to use it in an audispd plugin to simplify event processing by creating a plugin that will send an email whenever it sees an event with a specific key.