silkpython(3)

NAME
silkpython - SiLK Python plug-in (silkpython.so)

SYNOPSIS
 rwfilter --python-file=FILENAME [--python-file=FILENAME ...] ...

 rwfilter --python-expr=PYTHON_EXPRESSION ...

 rwcut --python-file=FILENAME [--python-file=FILENAME ...]
       --fields=FIELDS ...

 rwgroup --python-file=FILENAME [--python-file=FILENAME ...]
       --id-fields=FIELDS ...

 rwsort --python-file=FILENAME [--python-file=FILENAME ...]
       --fields=FIELDS ...

 rwstats --python-file=FILENAME [--python-file=FILENAME ...]
       --fields=FIELDS --values=VALUES ...

 rwuniq --python-file=FILENAME [--python-file=FILENAME ...]
       --fields=FIELDS --values=VALUES ...

DESCRIPTION
The SiLK Python plug-in provides a way to use PySiLK (the SiLK extension for Python described in pysilk(3)) to extend the capability of several SiLK tools.

•   In rwfilter(1), new partitioning rules can be defined in PySiLK to determine whether a SiLK Flow record is written to the --pass-destination or --fail-destination.

•   In rwcut(1), new fields can be defined in PySiLK and displayed for each record.

•   New fields can also be defined in rwgroup(1) and rwsort(1). These fields are used as part of the key when grouping or sorting the records.

•   For rwstats(1) and rwuniq(1), two types of fields can be defined: Key fields are used to categorize the SiLK Flow records into bins, and aggregate value fields compute a value across all the SiLK Flow records that are categorized into a bin. (An example of a built-in aggregate value field is the number of packets that were seen for all flow records that match a particular key.)

To extend the SiLK tools using PySiLK, the user writes a Python file that calls Python functions defined in the silk.plugin Python module and described in this manual page. When the user specifies the --python-file switch to a SiLK application, the application loads the Python file and makes the new functionality available.

The following sections will describe

•   how to create a command line switch with PySiLK that allows one to modify the run-time behavior of their PySiLK code

•   how to use PySiLK with rwfilter

•   a simple API for creating fields in rwcut, rwgroup, rwsort, rwstats, and rwuniq

•   the advanced API for creating fields in those applications

Typically you will not need to explicitly import the silk.plugin module, since the --python-file switch does this for you. In a module used by a Python plug-in, the module can gain access to the functions defined in this manual page by importing them from silk.plugin:

 from silk.plugin import *

Hint: If you want to check whether the Python code in FILENAME is defining the switches and fields you expect, you can load the Python file and examine the output of --help, for example:

 rwcut --python-file=FILENAME --help

  User-defined command line switches
Command line switches can be added and handled from within a SiLK Python plug-in. In order to add a new switch, use the following function:

register_switch(switch_name, handler=handler, [arg=needs_arg], [help=help_string])

switch_name
    Provides the name of the switch you are registering, a string. Do not include the leading "--" in the name. If a switch already exists with the name switch_name, the application will exit with an error message.

handler
    Names a function that will be called by the application while it is processing its command line if and only if the command line includes the switch --switch_name. (If the switch is not given, the handler function will not be called.) When the arg parameter is specified and its value is False, the handler function will be called with no arguments. Otherwise, the handler function will be called with a single argument: a string representing the value the user passed to the --switch_name switch. The return value from this function is ignored. Note that the register_switch() function requires a handler argument which must be passed by keyword.

needs_arg
    Specifies a boolean value that determines whether the user must specify an argument to --switch_name, and determines whether the handler function should expect an argument. When arg is not specified or needs_arg is True, the user must specify an argument to --switch_name and the handler function will be called with a single argument. When needs_arg is False, it is an error to specify an argument to --switch_name and handler will be called with no arguments.

help_string
    Provides the usage text to print describing this switch when the user runs the application with the --help switch. This argument is optional; when it is not provided, a simple "No help for this switch" message is printed.

  rwfilter usage
When used in conjunction with rwfilter(1), the SiLK Python plug-in allows users to define arbitrary partitioning criteria using the SiLK extension to the Python programming language. To use this capability, the user creates a Python file and specifies its name with the --python-file switch in rwfilter. The file should call the register_filter() function for each filter that it wants to create:

register_filter(filter, [finalize=finalize], [initialize=initialize])

filter
    Names a function that must accept a single argument, a silk.RWRec object (see pysilk(3)). When the rwfilter program is run, it finds the records that match the selection options, and hands each record to the built-in partitioning switches. A record that passes all of the built-in switches is handed to the first Python filter() function as an RWRec object. The return value of the function determines what happens to the record. The record fails the filter() function (and the record is immediately written to the --fail-destination, if specified) when the function returns one of the following: "False", "None", numeric zero of any type, an empty string, or an empty container (including strings, tuples, lists, dictionaries, sets, and frozensets). If the function returns any other value, the record passes the first filter() function, and the record is handed to the next Python filter() function. If all filter() functions pass the record, the record is written to the --pass-destination, if specified. (Note that when the --plugin switch is present, the code it specifies will be called after the PySiLK code.)

initialize
    Names a function that takes no arguments. When this function is specified, is will be called after rwfilter has completed its argument processing, and just before rwfilter opens the first input file. The return value of this function is ignored.

finalize
    Names a function that takes no arguments. When this function is specified, it will be called after all flow records have been processed. One use of the these functions is to print any statistics that the filter() function was computing. The return value from this function is ignored.

If register_filter() is called multiple times, the filter(), initialize(), and finalize() functions will be invoked in the order in which the register_filter() functions were seen.

NOTE: For backwards compatibility, when the file named by --python-file does not call register_filter(), rwfilter will search the Python file for functions named rwfilter() and finalize(). If it finds the rwfilter() function, rwfilter will act as if the file contained:

 register_filter(rwfilter, finalize=finalize)

The --python-file switch requires the user to create a file containing Python code. To allow the user to write a small filtering check in Python, rwfilter supports the --python-expr switch. The value of the switch should be a Python expression whose result determines whether a given record passes or fails, using the same criterion as the filter() function described above. In the expression, the variable "rec" is bound to the current silk.RWRec object. There is no support for the initialize() and finalize() functions. The user may consider --python-expr=PYTHON_EXPRESSION as being implemented by

 from silk import *
 def temp_filter(rec):
     return (PYTHON_EXPRESSION)

 register_filter(temp_filter)

The --python-file and --python-expr switches allow for much flexibility but at the cost of speed: converting a SiLK Flow record into an RWRec is expensive relative to most operations in rwfilter. The user should use rwfilter's built-in partitioning switches to whittle down the input as much as possible, and only use the Python code to do what is difficult or impossible to do otherwise.

  Simple field registration functions
The silk.plugin module defines a function that can be used to define fields for use in rwcut, rwgroup, rwsort, rwstats, and rwuniq. That function is powerful, but it is also complex. To make it easy to define fields for the common cases, the silk.plugin provides the functions described in this section that create a key field or an aggregate value field. The advanced function is described later in this manual page ("Advanced field registration function").

Once you have created a key field or aggregate value field, you must include the field's name in the argument to the --fields or --values switch to tell the application to use the field.

Integer key field

The following function is used to create a key field whose value is an unsigned integer.

register_int_field(field_name, int_function, min, max, [width])

field_name
    The name of the new field, a string. If you attempt to add a key field that already exists, you will get an an error message.

int_function
    A function that accepts a silk.RWRec object as its sole argument, and returns an unsigned integer which represents the value of this field for the given record.

min A number representing the minimum integer value for the field. If int_function returns a value less than min, an error is raised.

max A number representing the maximum integer value for the field. If int_function returns a value greater than max, an error is raised.

width
    The column width to use when displaying the field. This parameter is optional; the default is the number of digits necessary to display the integer max.

IPv4 address key field

This function is used to create a key field whose value is an IPv4 address. (See also register_ip_field()).

register_ipv4_field(field_name, ipv4_function, [width])

field_name
    The name of the new field, a string. If you attempt to add a key field that already exists, you will get an an error message.

ipv4_function
    A function that accepts a silk.RWRec object as its sole argument, and returns a silk.IPv4Addr object. This IPv4Addr object will be the IPv4 address that represents the value of this field for the given record.

width
    The column width to use when displaying the field. This parameter is optional, and it defaults to 15.

IP address key field

The next function is used to create a key field whose value is an IPv4 or IPv6 address.

register_ip_field(field_name, ip_function, [width])

field_name
    The name of the new field, a string. If you attempt to add a key field that already exists, you will get an an error message.

ip_function
    A function that accepts a silk.RWRec object as its sole argument, and returns a silk.IPAddr object which represents the value of this field for the given record.

width
    The column width to use when displaying the field. This parameter is optional. The default width is 39.

This key field requires more memory internally than fields registered by the register_ipv4_field() function. If SiLK is compiled without IPv6 support, register_ip_field() works exactly like register_ipv4_field(), including the default width of 15.

Enumerated object key field

The following function is used to create a key field whose value is any Python object. The maximum number of different objects that can be represented is 4,294,967,296, or 2^32.

register_enum_field(field_name, enum_function, width, [ordering])

field_name
    The name of the new field, a string. If you attempt to add a key field that already exists, you will get an an error message.

enum_function
    A function that accepts a silk.RWRec object as its sole argument, and returns a Python object which represents the value of this field for the given record. For typical usage, the Python objects returned by the enum_function will be strings representing some categorical value.

width
    The column width to use when displaying this field. The parameter is required.

ordering
    A list of objects used to determine ordering for rwsort and rwuniq. This parameter is optional. If specified, it lists the objects in the order in which they should be sorted. If the enum_function returns a object that is not in ordering, the object will be sorted after all the objects in ordering.

Integer sum aggregate value field

This function is used to create an aggregate value field that maintains a running unsigned integer sum.

register_int_sum_aggregator(agg_value_name, int_function, [max_sum], [width])

agg_value_name
    The name of the new aggregate value field, a string. The agg_value_name must be unique among all aggregate values, but an aggregate value field and key field can have the same name.

int_function
    A function that accepts a silk.RWRec object as its sole argument, and returns an unsigned integer which represents the value that should be added to the running sum for the current bin.

max_sum
    The maximum possible sum. This parameter is optional; if not specified, the default is 2^64-1 (18,446,744,073,709,551,615).

width
    The column width to use when displaying the aggregate value. This parameter is optional. The default is the number of digits necessary to display max_sum.

Integer maximum aggregate value field

The following function is used to create an aggregate value field that maintains the maximum unsigned integer value.

register_int_max_aggregator(agg_value_name, int_function, [max_max], [width])

agg_value_name
    The name of the new aggregate value field, a string. The agg_value_name must be unique among all aggregate values, but an aggregate value field and key field can have the same name.

int_function
    A function that accepts a silk.RWRec object as its sole argument, and returns an integer which represents the value that should be considered for the current highest value for the current bin.

max_max
    The maximum possible value for the maximum. This parameter is optional; if not specified, the default is 2^64-1 (18,446,744,073,709,551,615).

width
    The column width to use when displaying the aggregate value. This parameter is optional. The default is the number of digits necessary to display max_max.

Integer minimum aggregate value field

This function is used to create an aggregate value field that maintains the minimum unsigned integer value.

register_int_min_aggregator(agg_value_name, int_function, [max_min], [width])

agg_value_name
    The name of the new aggregate value field, a string. The agg_value_name must be unique among all aggregate values, but an aggregate value field and key field can have the same name.

int_function
    A function that accepts a silk.RWRec object as its sole argument, and returns an integer which represents the value that should be considered for the current lowest value for the current bin.

max_min
    The maximum possible value for the minimum. When this optional parameter is not specified, the default is 2^64-1 (18,446,744,073,709,551,615).

width
    The column width to use when displaying the aggregate value. This parameter is optional. The default is the number of digits necessary to display max_min.

  Advanced field registration function
The previous section provided functions to register a key field or an aggregate value field when dealing with common objects. When you need to use a complex object, or you want more control over how the object is handled in PySiLK, you can use the register_field() function described in this section.

Many of the arguments to the register_field() function are callback functions that you must create and that the application will invoke. (The simple registration functions above have already taken care of defining these callback functions.)

Often the callback functions for handling fields will either take (as a parameter) or return a representation of a numeric value that can be processed from C. The most efficient way to handle these representations is as a string containing binary characters, including the null byte. We will use the term "byte sequence" for these representations; other possible terms include "array of bytes", "byte strings", or "binary values". For hints on creating byte sequences from Python, see the "Byte sequences" section below.

To define a new field or aggregate value, the user calls:

register_field(field_name, [add_rec_to_bin=add_rec_to_bin,] [bin_compare=bin_compare,] [bin_bytes=bin_bytes,] [bin_merge=bin_merge,] [bin_to_text=bin_to_text,] [column_width=column_width,] [description=description,] [initial_value=initial_value,] [initialize=initialize,] [rec_to_bin=rec_to_bin,] [rec_to_text=rec_to_text])

Although the keyword arguments to register_field() are all optional from Python's perspective, certain keyword arguments must be present before an application will define the key or aggregate value. The following table summarizes the keyword arguments used by each application. An "F" means the argument is required for a key field, an "A" means the argument is required for an aggregate value field, "f" and "a" mean the application will use the argument for a key field or an aggregate value if the argument is present, and blank space means the application completely ignores the argument.

                   rwcut  rwgroup  rwsort  rwstats  rwuniq
 add_rec_to_bin                               A       A
 bin_compare                                  A
 bin_bytes                   F       F       F,A     F,A
 bin_merge                                    A       A
 bin_to_text                                 F,A     F,A
 column_width        F                       F,A     F,A
 description         f       f       f       f,a     f,a
 initial_value                                a       a
 initialize          f       f       f       f,a     f,a
 rec_to_bin                  F       F        F       F
 rec_to_text         F

The following sections describe how to use register_field() in each application.

  rwcut usage
The purpose of rwcut(1) is to print attributes of (or attributes derived from) every SiLK record it reads as input. A plug-in used by rwcut must produce a printable (textual) attribute from a SiLK record. To define a new attribute, the register_field() method should be called as shown:

register_field(field_name, column_width=column_width, rec_to_text=rec_to_text, [description=description,] [initialize=initialize])

field_name
    Names the field being defined, a string. If you attempt to add a field that already exists, you will get an an error message. To display the field, include field_name in the argument to the --fields switch.

column_width
    Specifies the length of the longest printable representation. rwcut will use it as the width for the field_name column when columnar output is selected.

rec_to_text
    Names a callback function that takes a silk.RWRec object as its sole argument and produces a printable representation of the field being defined. The length of the returned text should not be greater than column_width. If the value returned from this function is not a string, the returned value is converted to a string by the Python str() function.

description
    Provides a string giving a brief description of the field, suitable for printing in --help output. This argument is optional.

initialize
    Names a callback function that will be invoked after the application has completed its argument processing, and just before it opens the first input file. This function is only called when --fields includes field_name. The function takes no arguments and its return value is ignored. This argument is optional.

If the rec_to_text and column_width arguments are not present, the register_field() function will do nothing when called from rwcut.

  rwgroup and rwsort usage
The rwsort(1) tool sorts SiLK records by their attributes or attributes derived from them. rwgroup(1) reads sorted SiLK records and writes a common value into the next hop IP field of all records that have common attributes. The output from both of these tools is a stream of SiLK records (the output typically includes every record that was read as input). A plug-in used by these tools must return a value that the application can use internally to compare records. To define a new field that may be included in the --id-fields switch to rwgroup or the --fields switch to rwsort, the register_field() method should be invoked as follows:

register_field(field_name, bin_bytes=bin_bytes, rec_to_bin=rec_to_bin, [description=description,] [initialize=initialize])

field_name
    Names the field being defined, a string. If you attempt to add a field that already exists, you will get an an error message. To have rwgroup or rwsort use this field, include field_name in the argument to --id-fields or --fields.

bin_bytes
    Specifies a positive integer giving the length, in bytes, of the byte sequence that the rec_to_bin() function produces; the byte sequence must be exactly this length.

rec_to_bin
    Names a callback function that takes a silk.RWRec object and returns a byte sequence that represents the field being defined. The returned value should be exactly bin_bytes byte long. For proper grouping or sorting, the byte sequence should be returned in network byte order (i.e., big endian).

description
    Provides a string giving a brief description of the field, suitable for printing in --help output. This argument is optional.

initialize
    Names a callback function that will be invoked after the application has completed its argument processing, and just before it opens the first input file. This function is only called when field_name is included in the list of fields. The function takes no arguments and its return value is ignored. This argument is optional.

If the rec_to_bin and bin_bytes arguments are not present, the register_field() function will do nothing when called from rwgroup or rwsort.

  rwstats and rwuniq usage
rwstats(1) and rwuniq(1) group SiLK records into bins based on key fields. Once a record is matched to a bin, the record is used to update the aggregate values (e.g., the sum of bytes) that are being computed, and the record is discarded. Once all records have been processed, the key fields and the aggregate values are printed.

Key Field

A plug-in used by rwstats or rwuniq for creating a new key field must return a value that the application can use internally to compare records, and there must be a function that converts that value to a printable representation. The following invocation of register_field() will produce a key field that can be used in the --fields switch of rwstats or rwuniq:

register_field(field_name, bin_bytes=bin_bytes, bin_to_text=bin_to_text, column_width=column_width, rec_to_bin=rec_to_bin, [description=description,] [initialize=initialize])

The arguments are:

field_name
    Contains the name of the field being defined, a string. If you attempt to add a field that already exists, you will get an an error message. The field will only be active when field_name is specified as an argument to --fields.

bin_bytes
    Contains a positive integer giving the length, in bytes, of the byte sequence that the rec_to_bin() function produces and that the bin_to_text() function accepts. The byte sequences must be exactly this length.

bin_to_text
    Names a callback function that takes a byte sequence, of length bin_bytes, as produced by the rec_to_bin function and returns a printable representation of the byte sequence. The length of the text should be no longer than the value specified by column_width. If the value returned from this function is not a string, the returned value is converted to a string by the Python str() function.

column_width
    Contains a positive integer specifying the length of the longest textual field that the bin_to_text callback function returns. This length will used as the column width when columnar output is requested.

rec_to_bin
    Names a callback function that takes a silk.RWRec object and returns a byte sequence that represents the field being defined. The returned value should be exactly bin_bytes byte long. For proper sorting, the byte sequence should be returned in network byte order (i.e., big endian).

description
    Provides a brief description of the field, suitable for printing in --help output. This argument is optional.

initialize
    Names a callback function that is called after the command line arguments have been processed, and before opening the first file. This function is only called when --fields includes field_name. The function takes no arguments and its return value is ignored. This argument is optional.

Aggregate Value

A plug-in used by rwstats or rwuniq for creating a new aggregate value must be able to use a SiLK record to update an aggregate value, take two aggregate values and merge them to a new value, and convert that aggregate value to a printable representation. To use an aggregate value for ordering the bins in rwstats, the plug-in must also define a function to compare two aggregate values. The aggregate values are represented as byte sequences.

To define a new aggregate value in rwstats, the user calls:

register_field(agg_value_name, add_rec_to_bin=add_rec_to_bin, bin_bytes=bin_bytes, bin_merge=bin_merge, bin_to_text=bin_to_text, column_width=column_width, [bin_compare=bin_compare,] [description=description,] [initial_value=initial_value,] [initialize=initialize])

The call to define a new aggregate value in rwuniq is nearly identical:

register_field(agg_value_name, add_rec_to_bin=add_rec_to_bin, bin_bytes=bin_bytes, bin_merge=bin_merge, bin_to_text=bin_to_text, column_width=column_width, [description=description,] [initial_value=initial_value,] [initialize=initialize])

The arguments are:

agg_value_name
    Contains the name of the aggregate value field being defined, a string. The name of value must be unique among all aggregate values, but an aggregate value field and key field can have the same name. The value will only be active when agg_value_name is specified as an argument to --values.

add_rec_to_bin
    Names a callback function that whose two argument are a silk.RWRec object and an aggregate value. The function updates the aggregate value with data from the record and returns a new aggregate value. Both aggregate values are represented as byte sequences of exactly bin_bytes bytes.

bin_bytes
    Contains a positive integer representing the length, in bytes, of the binary aggregate value used by the various callback functions. Every byte sequence for this field must be exactly this length, and it also governs the length of the byte sequence specified by initial_value.

bin_merge
    Names a callback function which returns the result of merging two binary aggregate values into a new binary aggregate value. This merge function will often be addition; however, if the aggregate value is a bitmap, the result of merge function could be the union of the bitmaps. The function should take two byte sequence arguments and return a byte sequence, where all byte sequences are exactly bin_bytes bytes in length. If merging the aggregate values is not possible, the function should throw an exception. This function is used when the data structure used by rwstats or rwuniq runs out memory. When that happens, the application writes its current state to a temporary file, empties its buffers, and continues reading records. Once all records have been processed, the application needs to merge the temporary files to produce the final output. The bin_merge function is used when merging these binary aggregate values.

bin_to_text
    Names a callback function that takes a byte sequence representing an aggregate value as an argument and returns a printable representation of that aggregate value. The byte sequence input to bin_to_text will be exactly bin_bytes bytes long. The length of the text should be no longer than the value specified by column_width. If the value returned from this function is not a string, the returned value is converted to a string by the Python str() function.

column_width
    Contains a positive integer specifying the length of the longest textual field that the bin_to_text callback function returns. This length will used as the column width when columnar output is requested.

bin_compare
    Names a callback function that is called with two aggregate values, each represented as a byte sequence of exactly bin_bytes bytes. The function returns (1) a value less than 0 if the first argument is less than the second, (2) a value greater than 0 if the first is greater than the second, or (3) 0 if the two values are equal. This function is used by rwstats to sort the bins into top-N order.

description
    Provides a brief description of the aggregate value, suitable for printing in --help output. This argument is optional.

initial_value
    Specifies a byte sequence representing the initial state of the binary aggregate value. This byte sequence must be of length bin_bytes bytes. If this argument is not specified, the aggregate value is set to a byte sequence containing bin_bytes null bytes.

initialize
    Names a callback function that is called after the command line arguments have been processed, and before opening the first file. This function is only called when --values includes agg_value_name. The function takes no arguments and its return value is ignored. This argument is optional.

  Byte sequences
The rwgroup, rwsort, rwstats, and rwuniq programs make extensive use of "byte sequences" (a.k.a., "array of bytes", "byte strings", or "binary values") in their plug-in functions. The byte sequences are used in both key fields and aggregate values.

When used as key fields, the values can represent uniqueness or indicate sort order. Two records with the same byte sequence for a field will be considered identical with respect to that field. When sorting, the byte sequences are compared in network byte order. That is, the most significant byte is compared first, followed by the next-most-significant byte, etc. This equates to string comparison starting with the left-hand side of the string.

When used as an aggregate field, the byte sequences are expected to behave more like numbers, with the ability to take binary record and add a value to it, or to merge (e.g., add) two byte sequences outside the context of a SiLK record.

Every byte sequence has an associated length, which is passed into the register_field() function in the bin_bytes argument. The length determines how many values the byte sequence can represent. A byte sequence with a length of 1 can represent up to 256 unique values (from 0 to 255 inclusive). A byte sequence with a length of 2 can represent up to 65536 unique values (0 to 65535). To generalize, a byte sequence with a length of n can represent up to 2^(8n) unique values (0 to 2^(8n)-1).

Python does not make conversions between integers and byte sequences particularly natural. As a result, here are some pointers on how to do these conversions:

Use the format operator

For the novice, the easiest way to create byte sequence for grouping or sorting is to return the printable string representation of the value. One can do this using the Python format operator (%) with the following caveats:

•   each value must be the same length; you can specify a width in the format string to accomplish this

•   the result will be sorted in ASCII order

•   the string representation will be more expensive than a byte sequence representation in terms of memory used and perhaps in terms of speed

To encode a record's source port for use with rwsort, you could use:

 def sport_to_binary(rec):
     return "%5d" % rec.sport

 register_field("my_sport", bin_bytes = 5,
                rec_to_bin = sport_to_binary)

The 5 in the format string ensures that the returned string is 5 bytes long, and the call to register_field sets the bin_bytes to 5.

This makes "converting" from the binary value to the printable representation very simple. For example, to register that field in rwuniq:

 def sport_to_binary(rec):
     return "%5d" % rec.sport

 def binary_to_text(bin):
     return bin

 register_field("my_sport", bin_bytes = 5, column_width = 5,
                rec_to_bin = sport_to_binary,
                bin_to_text = binary_to_text)

To encode the source IPv4 address (a silk.IPv4Addr object), you may be tempted to use:

 "%15s" % str(rec.sip)  # WRONG

but that will cause '255.0.0.0' to be considered less than '1.10.10.10' since it is shorter. Instead you must convert the IPv4Addr to an integer:

 "%10u" % int(rec.sip)

This encoding will require 10 bytes per IPv4 address. If a binary representation were used instead, an IPv4 address would fit into 4 bytes. The use of the format operator is discouraged as there are more efficient ways to do the encoding.

Use the chr() and ord() methods

If you converting a single integer value that is less than 256, the easiest way to convert it to a byte sequence is to use the chr() function; to convert it back, use the ord() function.

 str = chr(num)
 num = ord(str)

The chr() function takes an integer between 0 and 255 inclusive, and returns a single byte (a byte sequence of length 1 (bin_bytes=1)). The ord() function takes a single byte and returns an integer between 0 and 255.

Use the struct module

When the value you are converting to a byte sequence is 255 or greater, you have to go with another option. One of the simpler options is to use Python's built-in struct module. With this module, you can encode a number or a set of numbers into a byte sequence using the struct.pack() function. To convert that byte sequence back to the number or set of numbers, use struct.unpack(). The module also provides the struct.calcsize() function that returns the length of the encoded byte sequence. A formatting string is used to indicate how the numbers are encoded into binary. For example:

 import struct

 #Encode two 64-bit numbers as a byte sequence
 str = struct.pack("!QQ", num1, num2)
 #Unpack a byte sequence back into two 64-bit numbers
 (num1, num2) = struct.unpack("!QQ", str)
 #Length of the encoded byte sequence
 bin_bytes = struct.calcsize("!QQ")

In the above, "Q" represents a single unsigned 64-bit number (an unsigned long long or quad). The "!" at the beginning of the string forces network byte order. (For sort comparison purposes, always pack in network byte order.)

Here is another example, which encodes a signed 16-bit integer and a floating point number:

 import struct

 #Encode a 16-bit signed integer and a float as a byte sequence
 str = struct.pack("!hf", intval, floatval)
 #Unpack a byte sequence back into a 16-bit signed integer and a float
 (intval, floatval) = struct.unpack("!hf", str)
 #Length of the encoded byte sequence
 bin_bytes = struct.calcsize("!hf")

Note that struct.unpack returns a sequence. When unpacking a single value, assign the result of unpack to (variable_name,), as shown:

 import struct

 #Encode an unsigned 32-bit integer as a byte sequence
 str = struct.pack("!I", num1)
 #Unpack a byte sequence back into a unsigned 32-bit integer
 (num1,) = struct.unpack("!I", str)
 #Length of the encoded byte sequence
 bin_bytes = struct.calcsize("!I")

The full list of codes can be found in the Python library documentation for the struct module, <http://docs.python.org/library/struct.html>.

Use the array module

The Python array module provides another way to create byte sequences. Beware that the array module does not provide an automatic way to encode the values in network byte order.

OPTIONS
The following options are available when the SiLK Python plug-in is used from rwfilter.

--python-file=FILENAME
    Load the Python file FILENAME. The Python code may call register_filter() multiple times to define new partitioning functions that takes a silk.RWRec object as an argument. The return value of the function determines whether the record passes the filter. For backwards compatibility, if register_filter() is not called and a function named rwfilter() exists, that function is automatically registered as the filtering function. Multiple --python-file switches may be used to load multiple plug-ins.

--python-expr=PYTHON_EXPRESSION
    Pass the SiLK Flow record if the result of the processing the record with the specified PYTHON_EXPRESSION is true. The expression is evaluated in the following context:

    •   The record is represented by the variable named rec, which is a silk.RWRec object.

    •   There is an implicit from silk import * in effect.

The following options are available when the SiLK Python plug-in is used from rwcut, rwgroup, rwsort, rwstats, or rwuniq:

--python-file=FILENAME
    Load the Python file FILENAME. The Python code may call register_field() multiple times to define new fields for use by the application. When used with rwstats or rwuniq, the Python code may call register_field() multiple times to create new aggregate fields. Multiple --python-file switches may be used to load multiple plug-ins.

EXAMPLES

  rwfilter --python-expr
Suppose you want to find traffic destined to a particular host, 10.0.0.23, that is either ICMP or coming from 1434/udp. If you attempt to use:

 rwfilter --daddr=10.0.0.23 --proto=1,17 --sport=1434   \
     --pass=outfile.rwf  flowrec.rwf

the --sport option will not match any of the ICMP traffic, and your result will not contain ICMP records. To avoid having to use two invocations of rwfilter, you can use the SiLK Python plugin to do the check in a single pass:

 rwfilter --daddr=10.0.0.23 --proto=1,17                \
     --python-expr 'rec.protocol==1 or rec.sport==1434' \
     --pass=outfile.rwf  flowrec.rwf

Since the Python code is slower than the C code used internally by rwfilter, we want to limit the number of records processed in Python as much as possible. We use the rwfilter switches to do the address check and protocol check, and in Python we only need to check whether the record is ICMP or if the source port is 1434 (if the record is not ICMP we know it is UDP because of the --proto switch).

  rwfilter --python-file
To see all records whose protocol is different from the proceeding record, use the following Python code. The code also prints a message to the standard output on completion.

 import sys

 def filter(rec):
     global lastproto
     if rec.protocol != lastproto:
         lastproto = rec.protocol
         return True
     return False

 def initialize():
     global lastproto
     lastproto = None

 def finalize():
     sys.stdout.write("Finished processing records.\n")

 register_filter(filter, initialize = initialize, finalize = finalize)

The proceeding file, if called lastproto.py, can be used like this:

 rwfilter --python-file lastproto.py --pass=outfile.rwf flowrec.rwf

Note: Be careful when using a Python plug-in to write to the standard output, since the Python output could get intermingled with the output from --pass=stdout and corrupt the SiLK output file. In general, printing to the standard error is safer.

  Command line switch
The following code registers the command line switch "count-protocols". This switch is similar to the standard --protocol switch on rwfilter, in that it passes records whose protocol matches a value specified in a list. In addition, when rwfilter exits, the plug-in prints a count of the number of records that matched each specified protocol. Note that the Python code does not except ranges.

 import sys
 from silk.plugin import *

 pro_count = {}

 def proto_count(rec):
     global pro_count
     if rec.protocol in pro_count.keys():
         pro_count[rec.protocol] += 1
         return True
     return False

 def print_counts():
     for p,c in pro_count.iteritems():
         sys.stdout.write("%3d|%10d|\n" % (p, c))

 def parse_protocols(protocols):
     global pro_count
     for p in protocols.split(","):
         pro_count[int(p)] = 0
     register_filter(proto_count, finalize = print_counts)

 register_switch("count-protocols", handler=parse_protocols,
                 help="Like --proto, but prints count of flow records")

When this code is saved to the file count-proto.py, it can be used with rwfilter as shown to get a count of TCP and UDP flow records:

 rwfilter --start-date=2008/08/08 --type=out            \
      --python-file=count-proto.py --count-proto=6,17   \
      --pass=/dev/null

Note that --pass is required, but since our plug-in is printing the data we want, we send the output to /dev/null.

  Simple: Integer key field
This example creates a field that contains the sum of the source and destination port. While this value may not be interesting to display in rwcut, it provides a way to sort fields so traffic between two low ports will usually be sorted before traffic between a low port and a high port.

 def port_sum(rec):
     return rec.sport + rec.dport

 register_int_field("port-sum", port_sum)

If the above code is saved in a file named portsum.py, it can be used to sort traffic prior to printing it (low-port to low-port will appear first):

 rwfilter --start-date=2008/08/08 --type=out,outweb   \
     --proto=6,17 --pass=stdout                       \
 | rwsort --python-file=portsum.py --fields=port-sum  \
 | rwcut

To see high-port to high-port traffic first, reverse the sort:

 rwfilter --start-date=2008/08/08 --type=out,outweb   \
     --proto=6,17 --pass=stdout                       \
 | rwsort --python-file=portsum.py --fields=port-sum  \
     --reverse                                        \
 | rwcut

  Simple: IP key field
SiLK stores uni-directional flows. For network conversations that cross the network border, the source and destination hosts are swapped depending on the direction of the flow. For analysis, you often want to know the internal and external hosts.

The following Python plug-in file defines two new fields: "internal-ip" will display the destination IP for an incoming flow, and the source IP for an outgoing flow, and "external-ip" field shows the reverse.

 import silk

 # for convenience, create lists of the types
 in_types = ['in', 'inweb', 'innull', 'inicmp']
 out_types = ['out', 'outweb', 'outnull', 'outicmp']

 def internal(rec):
     "Returns the IP Address of the internal side of the connection"
     if rec.typename in out_types:
         return rec.sip
     else:
         return rec.dip

 def external(rec):
     "Returns the IP Address of the external side of the connection"
     if rec.typename in in_types:
         return rec.sip
     else:
         return rec.dip

 register_ip_field("internal-ip", internal)
 register_ip_field("external-ip", external)

If the above code is saved in a file named direction.py, it can be used to show the internal and external IP addresses and flow direction for all traffic on 1434/udp from Aug 8, 2008.

 rwfilter --start-date=2008/08/08 --type=all      \
     --proto=17 --aport=1434 --pass=stdout        \
 | rwcut --python-file direction.py               \
     --fields internal-ip,external-ip,3-12

  Simple: Minimum/Maximum integer aggregate value
The following example will create new aggregate fields to print the minimum and maximum byte values:

 register_int_min_aggregator("min-bytes", lambda rec: rec.bytes,
                             (1 << 32) - 1)
 register_int_max_aggregator("min-bytes", lambda rec: rec.bytes,
                             (1 << 32) - 1)

The lambda expression allows one to create an anonymous function. In this code, we need to return the number of bytes for the given record, and we can easily do that with the anonymous function. Since the SiLK bytes field is 32 bits, the maximum 32-bit number is passed the registration functions.

Assuming the code is stored in a file bytes.py, it can be used with rwuniq to see the minimum and maximum byte counts for each source IP address:

 rwuniq --python-file=bytes.py --fields=sip      \
     --values=records,bytes,min-bytes,max-bytes

  rwcut
This example is similar to the simple IP example above, but it uses the advanced API. It also creates another field to indicate the direction of the flow, and it does not print the IPs when the traffic does not cross the border. Note that this code has to determine the column width itself.

 import silk, os

 # for convenience, create lists of the types
 in_types = ['in', 'inweb', 'innull', 'inicmp']
 out_types = ['out', 'outweb', 'outnull', 'outicmp']
 internal_only = ['int2int']
 external_only = ['ext2ext']

 # determine the width of the IP field depending on whether SiLK
 # was compiled with IPv6 support, and allow the IP_WIDTH environment
 # variable to override that width.
 ip_len = 15
 if silk.ipv6_enabled():
     ip_len = 39
 ip_len = int(os.getenv("IP_WIDTH", ip_len))

 def cut_internal(rec):
     "Returns the IP Address of the internal side of the connection"
     if rec.typename in in_types:
         return rec.dip
     if rec.typename in out_types:
         return rec.sip
     if rec.typename in internal_only:
         return "both"
     if rec.typename in external_only:
         return "neither"
     return "unknown"

 def cut_external(rec):
     "Returns the IP Address of the external side of the connection"
     if rec.typename in in_types:
         return rec.sip
     if rec.typename in out_types:
         return rec.dip
     if rec.typename in internal_only:
         return "neither"
     if rec.typename in external_only:
         return "both"
     return "unknown"

 def internal_external_direction(rec):
     """Generates a string pointing from the sip to the dip, assuming
     internal is on the left, and external is on the right."""
     if rec.typename in in_types:
         return "<---"
     if rec.typename in out_types:
         return "--->"
     if rec.typename in internal_only:
         return "-><-"
     if rec.typename in external_only:
         return "<-->"
     return "????"

 register_field("internal-ip", column_width = ip_len,
                rec_to_text = cut_internal)
 register_field("external-ip", column_width = ip_len,
                rec_to_text = cut_external)
 register_field("int_to_ext", column_width = 4,
                rec_to_text = internal_external_direction)

The cut_internal() and cut_external() functions may return an IPAddr object instead of a string. For those cases, the Python str() function is invoked automatically to convert the IPAddr to a string.

If the above code is saved in a file named direction.py, it can be used to show the internal and external IP addresses and flow direction for all traffic on 1434/udp from Aug 8, 2008.

 rwfilter --start-date=2008/08/08 --type=all           \
     --proto=17 --aport=1434 --pass=stdout             \
 | rwcut --python-file direction.py                    \
     --fields internal-ip,int_to_ext,external-ip,3-12

  rwsort
The following example Python plug-in creates one new field, "lowest_port", for use in rwsort. Using this field will sort records based on the lesser of the source port or destination port; for example, flows where either the source or destination port is 22 will occur before flows where either port is 25. This example shows using the Python struct module with multiple record attributes.

 import struct

 def lowest_port(rec):
     if rec.sport < rec.dport:
         return struct.pack("!HH", rec.sport, rec.dport)
     else:
         return struct.pack("!HH", rec.dport, rec.sport)

 register_field("lowest_port", bin_bytes = struct.calcsize("!HH"),
                rec_to_bin = lowest_port)

To use this example to sort the records in flowrec.rwf, one saves the code to the file sort.py and uses it as shown:

 rwsort --python-file=sort.py --fields=lowest_port    \
     flowrec.rwf > outfile.rwf

  rwstats and rwuniq
The following example defines two key fields for use by rwstats or rwuniq: "prefixed-sip" and "prefixed-dip". Using these fields, the user can count flow records based on the source and/or destination IP address blocks (CIDR blocks). The default CIDR prefix is 16, but it can be changed by specifying the --prefix switch that the example creates. This example uses the Python struct module to convert between the IP address and a binary string.

 import os, struct
 from silk import *

 default_prefix = 16

 def set_mask(prefix):
     global mask
     mask = 0xFFFFFFFF
     # the value we are handed is a string
     prefix = int(prefix)
     if 0 < prefix < 32:
         mask = mask ^ (mask >> prefix)

 # Convert from an IPv4Addr to a byte sequence
 def cidr_to_bin(ip):
     if ip.isipv6():
         raise ValueError, "Does not support IPv6"
     return struct.pack("!L", int(ip) & mask)

 # Convert from a byte sequence to an IPv4Addr
 def cidr_bin_to_text(string):
     (num,) = struct.unpack("!L", string)
     return IPv4Addr(num)

 register_field("prefixed-sip", column_width = 15,
                rec_to_bin = lambda rec: cidr_to_bin(rec.sip),
                bin_to_text = cidr_bin_to_text,
                bin_bytes = struct.calcsize("!L"))

 register_field("prefixed-dip", column_width = 15,
                rec_to_bin = lambda rec: cidr_to_bin(rec.dip),
                bin_to_text = cidr_bin_to_text,
                bin_bytes = struct.calcsize("!L"))

 register_switch("prefix", handler=set_mask,
                 help="Set prefix for prefixed-sip/prefixed-dip fields")

 set_mask(default_prefix)

The lambda expression allows one to create an anonymous function. In this code, the lambda function is used to pass the appropriate IP address into the cidr_to_bin() function. To write the code without the lambda would require separate functions for the source and destination IP addresses:

 def sip_cidr_to_bin(rec):
     return cidr_to_bin(rec.sip)

 def dip_cidr_to_bin(rec):
     return cidr_to_bin(rec.dip)

The lambda expression helps to simplify the code.

If the code is saved in the file mask.py, it can be used as follows to count the number of flow records seen in the /8 of each source IP address. The flow records are read from flowrec.rwf. The --ipv6-policy=ignore switch is used to restrict processing to IPv4 addresses.

 rwuniq --ipv6-policy=ignore --python-file mask.py     \
     --prefix 8 --fields prefixed-sip flowrec.rwf

The following example creates a new aggregate value that can be used by rwstats and rwuniq. The value is "avg-bytes", a value that calculates the average number of bytes seen across all flows that match the key. It does this by maintaining running totals of the byte count and number of flows.

 import struct

 fmt = "QQ"
 binsize = struct.calcsize(fmt)
 initial = struct.pack(fmt, 0, 0)
 textsize = 15
 textformat = "%%%d.2f" % textsize

 # add byte and flow count from 'rec' to 'current'
 def avg_bytes(rec, current):
     (total, count) = struct.unpack(fmt, current)
     return struct.pack(fmt, total + rec.bytes, count + 1)

 # return printable representation
 def avg_to_text(bin):
     (total, count) = struct.unpack(fmt, bin)
     return textformat % (float(total) / count)

 # merge two encoded values.
 def avg_merge(rec1, rec2):
     (total1, count1) = struct.unpack(fmt, rec1)
     (total2, count2) = struct.unpack(fmt, rec2)
     return struct.pack(fmt, total1 + total2, count1 + count2)

 # compare two encoded values
 def avg_compare(rec1, rec2):
     (total1, count1) = struct.unpack(fmt, rec1)
     (total2, count2) = struct.unpack(fmt, rec2)
     return cmp((float(total1) / count1), (float(total2) / count2))

 register_field("avg-bytes",
                column_width    = textsize,
                bin_bytes       = binsize,
                add_rec_to_bin  = avg_bytes,
                bin_to_text     = avg_to_text,
                bin_merge       = avg_merge,
                bin_compare     = avg_compare,
                initial_value   = initial)

To use this code, save it as avg-bytes.py, specify the name of the Python file in the --python-file switch, and list the field in the --values switch:

 rwuniq --python-file=avg-bytes.py --fields=sip  \
     --values=avg-bytes infile.rwf

This particular example will compute the average number of bytes per flow for each distinct source IP address in the file infile,rwf.

  rwcut, rwgroup, rwsort, rwstats, and rwuniq
The following example Python plug-in file defines two fields, "sport-service" and "dport-service". These fields convert the source port and destination port to the name of the "service" as defined in the file /etc/services; for example, port 80 is converted to "http". This plug-in can be used by any of rwcut, group, rwsort, rwstats, or rwuniq.

 import os,socket,struct

 # utility function to convert number to a service name,
 # or to a string if no service is defined
 def num_to_service(num):
     try:
         serv = socket.getservbyport(num)
     except socket.error:
         serv = "%d" % num
     return serv

 # rec-to-bin function: just return the port encoded as
 # a 16 bit unsigned integer
 def port_to_bin(num):
     return struct.pack("!H", num)

 # convert the encoded port to a service name
 def bin_to_service(bin):
     (port,) = struct.unpack("!H", bin)
     return num_to_service(port)

 # width of service columns can be specified with the
 # SERVICE_WIDTH environment variable; default is 12
 col_width = int(os.getenv("SERVICE_WIDTH", 12))

 register_field("sport-service", bin_bytes = 2,
                column_width = col_width,
                rec_to_text = lambda rec: num_to_service(rec.sport),
                rec_to_bin = lambda rec: port_to_bin(rec.sport),
                bin_to_text = bin_to_service)

 register_field("dport-service", bin_bytes = 2,
                column_width = col_width,
                rec_to_text = lambda rec: num_to_service(rec.dport),
                rec_to_bin = lambda rec: port_to_bin(rec.dport),
                bin_to_text = bin_to_service)

If this file is named service.py, it can be used by rwcut to print the source port and its service:

 rwcut --python-file service.py                        \
     --fields sport,sport-service flowrec.rwf

Although the plug-in can be used with rwsort, the records will be sorted in the same order as the numerical source port or destination port.

 rwsort --python-file service.py                             \
     --fields sport-service flowrec.rwf > outfile.rwf

When used with rwuniq, it can count flows, bytes, and packets indexed by the service of the destination port:

 rwuniq --python-file service.py --fields dport-service  \
     --values=flows,bytes,packets flowrec.rwf

LEGACY SUPPORT
Prior to SiLK 2.0, the register_field() function was called register_plugin_field(), and it had the following signature:

register_plugin_field(field_name, [bin_len=bin_len,] [bin_to_text=bin_to_text,] [text_len=text_len,] [rec_to_bin=rec_to_bin,] [rec_to_text=rec_to_text])

To convert from register_plugin_field to register_field, change text_len to column_width, and change bin_len to bin_bytes. (Even older code may use field_len; this should be changed to column_width as well.)

The register_filter() function is new. When rwfilter is invoked with --python-file and the named Python file does not call register_filter(), rwfilter will search the Python input for functions named rwfilter() and finalize(). If it finds the rwfilter() function, rwfilter will act as if the file contained:

 register_filter(rwfilter, finalize=finalize)

To update your pre-SiLK 2.0 rwfilter plug-ins, simply add the above line to your Python file.

ENVIRONMENT

PYTHONPATH
    The Python module that the SiLK Python plug-in uses (pysilk_nl.so) is installed under SiLK's installation tree. It may be necessary to set or modify the PYTHONPATH environment variable so Python can find this module. For information on using SiLK from Python, see pysilk(3).

SILK_PYTHON_TRACEBACK
    When set, Python plug-ins will output traceback information on Python errors to stderr.

SILK_PATH
    This environment variable gives the root of the install tree. As part of their search for the SiLK Python plug-in, silkpython.so, the SiLK applications check the directories $SILK_PATH/lib/silk, $SILK_PATH/share/lib and $SILK_PATH/lib.

SILK_PLUGIN_DEBUG
    When set to 1, the SiLK applications print status messages to the standard error as they attempt to find and open the silkpython.so plug-in.

FILES
SiLK applications look for the SiLK Python plug-in the the following locations. ($SILK_PATH is value of the SILK_PATH environment variable, if it is set; the use of /usr/local/ assumes the tool exists in the /usr/local/bin/ directory.)

  $SILK_PATH/lib/silk/silkpython.so
  $SILK_PATH/share/lib/silkpython.so
  $SILK_PATH/lib/silkpython.so
  /usr/local/lib/silk/silkpython.so
  /usr/local/share/lib/silkpython.so
  /usr/local/lib/silkpython.so

If the fields and/or switches are not available in an application, verify that silkpython.so is installed in the correct location. To aid in debugging, one may invoke:

  env SILK_PLUGIN_DEBUG=1 rwcut

to print the directory paths where rwcut is looking for silkpython.so.

SEE ALSO
pysilk(3), rwfilter(1), rwcut(1), rwgroup(1), rwsort(1), rwstats(1), rwuniq(1), python(1), <http://docs.python.org/>