With
version 4.0 and later, the functionality of Osiris can be extended through a
modular interface. These are
compiled modules, not dynamic modules.
This is useful because it allows for many different extensions to the software
to be developed and made available to users, and without bloating the code
base. An administrator can pick
and choose the modules that fit their needs or requirements and exclude the
rest. This will also attract more
developers to the project given that module development is a relatively small,
yet effective, undertaking.
This
document is targeted for developers and attempts to explain how to develop a
module by walking through a simple example. All questions related to module development should be sent
to the Osiris development list: osiris-devel@lists.shmoo.com.
Note:
at this point in time, only generic text-based scan records are supported. Support for modularized comparison
routines is being developed.
1.0
Overall
Process
Developing
a module involves implementing the module handler function, creating a README
file that specifies what the module does and how to use it, and generating a
tar.gz file containing the module (if you plan to submit it).
The
general principle behind modules is that they collect one or more records of
data with each scan, to be stored in a database. Subsequent scans follow the same process and the records are
compared with the previous scan.
There are a few common functions that all modules will use to create and
put records into the output queue (see example in next section).
1.1
General
Considerations
One of the biggest and first things to consider is what
platforms your module will support.
Osiris runs on Windows as well as Unix and Unix like platforms. Thus, there may be environmental
vectors specific to a certain platform, but it is recommended that you make the
module as usable on as many platforms as possible. Make sure to #ifdef the platform specific code, when
applicable (see mod_users as an example).
1.2
Submitting
Modules for Others to Use
There is a section of the Osiris project website devoted
to modules submitted by the community.
If you would like to submit your module for posting, please send it to brian@shmoo.com. Upon review it will be posted to the website.
2.0
Simple
Module Example: Monitoring
hostname
The following is an example module that attempts to
monitor the hostname which usually appears in /etc/hostname. This is a fairly useless module, but it
demonstrates the important parts that you as the developer need to do in order
to implement a module.
First, we start by creating a directory under the scan
agent source and make a directory:
cd src/osirisd/modules
mkdir mod_hostname
Next, modify the Makefile to build your source file, in
this case, we change the SRCS variable to be “mod_example.c” which we will
create next. The makefile should
look something like:
include ../Makefile
SRCS=mod_example.c
OBJS=$(SRCS:.c=.o)
module: ${SRCS} ${OBJS}
INCS=-I../.. -I../../../libosiris -I../../../libfileapi
-I../../../..
# meta-rule for compiling any "C" source file.
$(OBJS): $(SRCS)
$(CC) $(DEFS) $(DEFAULT_INCLUDES) ${INCLUDES} ${INCS} $(AM_CPPFLAGS) \
$(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c $(SRCS)
cp $@ ..
Next, create the module source file (mod_hostname.c)
using the example module
template, which looks something like:
#include "libosiris.h"
#include "libfileapi.h"
#include "rootpriv.h"
#include "common.h"
#include "version.h"
#include "scanner.h"
#include "logging.h"
static char *MODULE_NAME = "mod_example";
void mod_example( SCANNER *scanner )
{
SCAN_RECORD_TEXT_1 record;
if( scanner == NULL )
{
return;
}
/* initialize the record and copy in the module name. */
initialize_scan_record( (SCAN_RECORD *)&record,
SCAN_RECORD_TYPE_TEXT_1 );
osi_strlcpy( record.module_name, MODULE_NAME,
sizeof(record.module_name) );
/* YOUR CODE HERE. */
/* put record in output queue. */
send_scan_data( scanner, (SCAN_RECORD *)&record );
}
Change the name of the function and the MODULE_NAME
variable to be the name of the module, in this case, “mod_hostname”. We will use the function, gethostname()
and send a single record back to be stored in the database and compared against
upon each scan.
To initialize the record, use the
initialize_scan_record() function.
After the record has been populated, use the send_scan_data() method.
The completed module method looks something like:
1 void mod_hostname( SCANNER
*scanner )
2 {
3 char name[255];
4 SCAN_RECORD_TEXT_1 record;
5
6 if( scanner == NULL )
7 {
8 return;
9 }
10
11 /* get hostname value. */
12
13 if( gethostname( name, sizeof( name ) )
< 0 )
14 {
15 log_error( NULL,
"module: %s, error getting hostname.",
16
MODULE_NAME );
17 return;
18 }
19
20 initialize_scan_record( (SCAN_RECORD
*)&record,
21
SCAN_RECORD_TYPE_TEXT_1 );
22
23 /* copy module name into record. */
24 osi_strlcpy( record.module_name,
MODULE_NAME,
25
sizeof(record.module_name) );
26
27 /* copy a unique record name into the
record’s name field. */
28 osi_strlcpy( record.name, "hostname", sizeof(
record.name ) );
29
30 /* copy value for this record. */
31 osi_strlcpy( record.data, name, sizeof(
record.data ) );
32
33 /* send data. */
34 send_scan_data( scanner, (SCAN_RECORD
*)&record );
35 }
Starting with line 13, we attempt to get the hostname
value.
Line 20 initializes the a scan record type and zeros it
out.
Lines 23-31 fill in values for the record. The module name must always be put into
the record. The record.name value
must be something unique because it is used as a key to later retrieve the same
record in another database for comparison. Finally, line 34 sends the scan record back to the
management console.
In
this example, only a single record is sent for this module. For examples of many records, see
mod_users that ships with Osiris.
Basically, it’s the same process only involving multiple scan record
items.
Packaging.
After
the module has been compiled make sure to test it. Compile the module with ‘make module’. Then, add it to a scan config, in this
case, we will create a basic scan config that looks like:
IncludeAll
Hash md5
<System>
Include mod_hostname
</System>
Run a scan to populate the database with the hostname
record created by the module, then change the hostname and run another scan to
verify that the change was detected and reported in the logs. Testing will vary depending upon the
complexity of your code.
Next, create a README file explains how to use the
module and what it does. There is
a good example of such a README file in the mod_users, mod_groups, or mod_kmods
modules that ship with Osiris.
Finally, tar the module directory:
tar cvfz mod_hostname.tar.gz mod_hostname
Scan
records end up being stored as a record in the scan databases. Scan record structures are all defined
in src/libosiris/scan_record.h.
There are built-in records for various platforms and file systems.
Currently,
there is only one supported scan record that modules can take advantage of:
#define MAX_MODULE_NAME_LENGTH 128
#define MAX_TEXT_RECORD_NAME_LENGTH 128
#define MAX_TEXT_RECORD_DATA_LENGTH 512
typedef struct SCAN_RECORD_TEXT_1
{
osi_uint16 type;
osi_uint16 unused;
char module_name[MAX_MODULE_NAME_LENGTH];
char name[MAX_TEXT_RECORD_NAME_LENGTH];
char data[MAX_TEXT_RECORD_DATA_LENGTH];
} SCAN_RECORD_TEXT_1;
The obvious limitation is the amount of data stored with
each record, in this case 512 bytes.
Modules
are specified in the scan config, under the system block. For example:
<System>
Include
mod_users
</System>
To specify parameters, use the “param” directive:
<System>
Include
mod_foo
Param
“foo” “name” “value”
Param
“foo” “name2” “value2”
</System>
The first argument is the name of the module, the second
is the parameter name, and the third is the value. The parameter name does not have to be unique.
The parameters for a module are stored in the
config. As a module developer, you
have access to these modules through the scanner object passed as an argument
to the module handler. For
example, assuming the above module, “mod_foo” exists, we get the module
structure from the config as follows:
module
*m;
m
= get_module_with_name( scanner->config, “mod_foo” );
Each
module structure has a list of parameter objects. You can traverse this list as follows:
param
*p;
node
*n;
n
= list_get_first_node( mod->params );
while(
n )
{
param *p = (param
*)n->data;
if( p )
{
/*
p->name is the parameter name. */
/*
p->value is the parameter value. */
}
n = n->next;
}
Appendix C:
Common Functions.
The osiris code base has a lot of functions that should
be reused as much as possible instead of rewriting them for modules. For example, instead of using the
dangerous strcpy and sprintf functions, instead, use osi_strlcpy() and osi_sprintf(). See the example module listed above for examples of using
the osi_strlcpy() function. Most
of these functions can be found in the osiris library found under
src/libosiris.