Developing Modules for Osiris

 

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

 

Appendix A: Scan Records

 

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:

 

SCAN_RECORD_TEXT_1

 

#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.

 

Appendix B: Using Parameters to Modules

 

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.