Menu

Probe Development

David Rothenberg

Background

A probe is a class written as a singleton, which is responsible for collecting items from a computer subject to the constraints expressed in an object.

Objects and Items

Object

An object represents something similar to a search specification. Each object contains one or more entities, which describe constraints on individual properties of the items to be collected. Since items also have entities, we will refer to those associated with objects as object entities when it would otherwise be ambiguous.

An object's entities are chosen to represent that minimal set of properties which are required to uniquely identify a particular item on a system. For example, every network interface on a unix system has a unique name. Therefore, the interface object for unix only needs one entity: a name.

Item

An item represents the result of a search. Items also contain entities, which we will call item entities. Each item entity which corresponds to an object entity must satisfy the constraint expressed by the object. Items may have other entities as well, as specified by the OVAL language. All item entities must be constructed according to the language schema, which dictates things like legal values and the ordering of item entities within their containing item.

The task of the probe author is to write code to collect system information in whatever way is appropriate, subject to the constraints expressed by the object, and construct schema-compliant items representing this information.

Software Architecture

All probes extend an abstract class named AbsProbe. The relevant parts are below.

typedef vector < Item* > ItemVector;

class AbsProbe {

public:

    virtual ~AbsProbe();

protected:

    virtual ItemVector* CollectItems(Object* object) = 0;

    virtual Item* CreateItem() = 0;

};

The CollectItems(Object*) method is the primary method you would implement to perform the search.

In addition, you must implement a static method Instance(), which returns your singleton. It is not shown in AbsProbe because static methods
cannot be virtual. And obviously, the constructor in your singleton subclass should probably be private.

Constructing an Item

The pure-virtual method CreateItem() must be implemented in your probe. You may then call this method whenever you want to create a new item. It simply creates an item with particular XML schema details, e.g. tag name, namespace, qname prefix, etc. Here is a sample:

Item* InterfaceProbe::CreateItem() {

    Item* item = new Item(0,
            "http://oval.mitre.org/XMLSchema/oval-system-characteristics-5#unix",
            "unix-sc",
            "http://oval.mitre.org/XMLSchema/oval-system-characteristics-5#unix unix-system-characteristics-schema.xsd",
        OvalEnum::STATUS_ERROR,
        "interface_item");

    return item;
}

The item's ID (the first constructor parameter) is always set to 0. It will be automatically set to a unique value by the interpreter.

Constructing Item Entities

Constructing an item entity is straightforward; most of the time you only need to pass the entity's name and value, and maybe a datatype. But there is one important detail to remember: each item entity has within it a flag which tells the interpreter whether it corresponds to an object entity. Since an object's entities are selected to uniquely identify items, this effectively lets the interpreter determine which of an item's properties uniquely identify the item. This is necessary to compute a unique identifier which is used to cache and retrieve the item. So when creating an item, don't forget to set this flag on the appropriate entities, or the caching system won't work, and the interpreter will generate invalid results.

Error Handling

The OVAL language contains many symbols representing particular error and status conditions. It is important to handle errors in a way that is sensible and compliant with the language.

  • A flag is associated with a "collected object", i.e. it represents what happened during the collection (complete, error, not collected, etc).
  • A status is assocated with an individual item. It takes on values which are a subset of the flag values.

The interpreter automatically computes the flag for a collected object based on the status values of its items. You must be careful to set appropriate status values, but currently need not concern yourself with the flags, since the interpreter handles them automatically. The accepted policy for error handling is as follows:

  • If an error occurs before there is enough information to begin searching, throw a ProbeException.
  • If an error occurs while getting the value for an item entity which you know would have otherwise matched the object, set an appropriate status value on ''both'' the item entity and the item, e.g. OvalEnum::STATUS_ERROR. Attempt to continue to search for more items.
    • Caveat: Be practical and use common sense in the application of this rule. A strict interpretation means you must always attempt to match as many object entities as you can, even if you can tell right from the beginning that there was an error (e.g. because the language requires a value from a predefined set, but the user provided a value that was not in the set). This can cause a combinatorial explosion in the number of items, for objects with many entities which reference variables. Item entities may not reference variables, so you have to dereference all of the object's variable references to obtain values to build items with, which can produce a lot of items due to that one error. It may not make sense to build a single partial item, nor be practical to build large numbers of complete items. The most sensible thing to do would be to just throw an exception.
  • If an error occurs after searching has begun, but is not associated with a matching item, throw a ProbeException.

Integration Into The Interpreter

Probes are obtained via an instance of ProbeFactory. There are different implementations of this class for different platforms. They live in subdirectories of <checkout dir>/src according to the platform, e.g. src/windows, src/linux, etc.

The probe factories have a static GetProbe(std::string) method which gets the probe singleton instance corresponding to the given object element name. The factories we've written thus far simply check the name in a big if-then-else statement, and return the appropriate probe. So in order to integrate your probe into the interpreter, you must locate the probe factory for your platform, and then insert code to check for the corresponding object name and instantiate the probe. The code looks roughly like:

AbsProbe* ProbeFactory::GetProbe(string objectName) {

    if(objectName.compare("family_object") == 0) {
        probe = FamilyProbe::Instance();
    } else if(objectName.compare("filemd5_object") == 0) {
        probe = FileMd5Probe::Instance();
    } else if(objectName.compare("filehash_object") == 0) {
etc...

Implementation

Basics

To obtain the parameters of the search, first get the entities of the given object. These will be of type ObjectEntity. This class has a method GetEntityValues(StringVector&) (inherited from AbsEntity), which you should use to get the entity's values. This method handles details like variable references, variable flags, etc, which probes should not handle themselves. The values will be inserted into the given vector. The return value is a flag, which gives the status of the variable evaluation, if one was done. If a variable reference was not used, OvalEnum::FLAG_COMPLETE is always returned.

Note: Currently, flag values are ignored by probes, so you may ignore the return value of this method.

If you need to check whether an item matches your object, the best way to do it is via one of two Analyze methods. Most of the time, it is easiest to create an ItemEntity and then call ObjectEntity::Analyze(ItemEntity*) (inherited from AbsEntity) on it. This method automatically handles variable references and all of the various possible operators (e.g. equals, not equals, pattern match, etc) to determine whether there is a match. If there is a match, OvalEnum::RESULT_TRUE is returned. If any other result is returned, there was not a match.

Another alternative is to create a complete Item, and invoke Object::Analyze(Item*) on it. If the item doesn't match though, all the work creating it is wasted, and there may be a lot of junk to delete, so there may be a performance penalty. But it can potentially simplify probe code.

Rough pseudocode might look like:

ItemVector* YourProbe::CollectItems(Object* object) {
    ObjectEntity* usernameEntity = object->GetElementByName ( "username" );

    // search through system data in a loop...
    // see if we found a match
    ItemEntity *nameItemEntity = new ItemEntity("username", username, OvalEnum::DATATYPE_STRING, true);
    if (usernameEntity->Analyze(nameItemEntity) == OvalEnum::RESULT_TRUE) {
        // found a match!
    } else
        delete nameItemEntity;
}

Advanced Strategies

The CollectItems(Object*) method can be implemented in any way you like. But it should be made as efficient as possible. Some ideas we've used include:

  • If you know the total number of items is small, you can pre-compute and cache all items when the probe is first instantiated. Then all you need to do is search through the list to return the matches.
  • Try to be smarter about analyzing the search parameters to see if you can speed up the search. Searching an entire filesystem or windows registry can take a very long time, so it should be avoided if possible.

Related

Wiki: Home

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.