Thursday, July 16, 2015

Snort++: Introducing Piglet

In any sort of software development, having lightweight and thorough test coverage is essential. The scope of tests can range from the small (unit tests) to the large (regressions tests). When developing new plugins for Snort++, it can be useful to test individual plugin methods. These sort of tests -- the ones that lie somewhere between unit tests and regressions in scope -- are problematic. Unit testing plugin methods would require extensive mocking and stubbing and hence too much knowledge of Snort++ internals. On the other hand, it may be difficult to setup regression tests to trigger specific plugin methods; even then, there is very limited control over the range of inputs each method might receive.

We now have a new feature, called the Piglet test harness, that fills the gap in test scope.

The Piglet test harness allows you to use Lua scripts to exercise individual methods. Let's say you are developing a new Snort++ inspector called FooInspector. For simplicity's sake, all FooInspector will do is unset the PKT_FROM_CLIENT flag on a packet if it is set:
// foo_inspector.cc
// ... Plugin boilerplate omitted

class FooFlowData;

class FooInspector : public Inspector
{
public:
    FooInspector() { }
    void eval(Packet*) override;
}

void FooInspector::eval(Packet* p)
{
    if ( !p->from_client() )
        return;

    p->packet_flags &= ~PKT_FROM_CLIENT;
}

// ... Plugin boilerplate omitted

Now we create a test harness script to setup a Packet, call FooInspector.eval() on it, and then inspect the packet flags:
-- foo.lua
-- Required plugin header
plugin =
{
    type = "piglet",
    version = 1,
    name = ""
}

PKT_FROM_CLIENT = 0x80

-- Piglet test harness header
piglet =
{
    name = "make sure that FooInspector foos",

    -- Plugin type
    type = "inspector",

    -- Plugin name
    target = "foo_inspector",

    -- Test entry point
    test = function()
        local buf = RawBuffer.new(1024)
        local p = Packet.new(buf, 0, 100)
        p:set_fields({
            packet_flags = PKT_FROM_CLIENT
        })

        -- Call FooInspector.eval()
        Inspector.eval(p)

        if p:get_fields().packet_flags == 0 then
            -- test passed
            return true
        end

        -- test failed
        return false 
    end
}

To run Snort in Piglet mode, we must first compile with piglet mode enabled. In CMake, do this by running cmake with -DENABLE_PIGLET:BOOL=ON. If you are using autotools, pass the --enable-piglet flag to configure. Then, simply run snort with --script-path set to the directory containing the test script and the --piglet flag.
snort --piglet --script-path=/path/to/foo/tests
You'll get some libcheck-like output indicating whether each test passed or failed. More example scripts can be found in the /piglet_scripts folder in the source tree. For more on plugin development and piglet usage, see the Extending section in the Snort manual.