Project: Falco (Provisioning / Security & Compliance)
File: 20210501-plugin-system.md

This is a proposal to create an infrastructure to extend the functionality of the Falco libraries via plugins. Plugins will allow users to easily extend the functionality of the libraries and, as a consequence, of Falco and any other tool based on the libraries. This proposal, in particular, focuses on two types of plugins: source plugins and extractor plugins. and provide a powerful data capture framework, with a rich set of features that includes: data capture trace files management enrichment filtering formatting and screen rendering Lua scripting (chisels) These features have been designed with one specific input in mind: system calls. However, they are generically adaptable to a broad set of inputs, such as cloud logs. With this proposal, we want to dramatically extend the scope of what the libraries, Falco and other tools can be applied to. We want to do it in a way that is easy, efficient and empowers anyone in the community to write a plugin. To design and implement a plugin framework that makes the libraries more modular and extensible To have a framework that is easy to use To support dynamic loading of plugins, so that the libraries can be extended without having to be recompiled and relinked To enable users to write plugins in any language, with a particular focus on Go, C and C++ To have an efficient plugin framework so that, performance-wise, writing a plugin is as close as possible as extending the libraries internal source code To make it possible to write plugins for Linux, MacOS and Windows To implement plugins other than source and extractor: to be approached as separate task To document the plugin framework and interface: to be approached as separate task Both source and extractor plugins have the following: A required api version, to ensure compatibility with the plugin framework. A name A description A version A contact field for the plugin authors (website, github repo, twitter, etc). Functions to initialize and destroy the plugin internal state. Initially, we will implement support for two types of plugins: source plugins and extractor plugins. A source plugin implements a new sinsp/scap event source. It has the ability to "open" and "close" a session that provides events. It also has the ability to return an event to the plugin framework via a next() method. Events returned by source plugins have an "event source", which describes the information in the event. This is distinct from the plugin name to allow for multiple kinds of plugins to generate the same kind of events. For example, there might be plugins gke-audit-bridge, eks-audit-bridge, ibmcloud-audit-bridge, etc. that all fetch information. The plugins would have different names but would have the same event source "k8s_audit". Source plugins also have the ability to extract information from events based on fields. For example, a field proc.name extracts a process name from a syscall event. The plugin returns a set of supported fields, and there are functions to extract a value given an event and field. The plugin framework can then build filtering expressions/Falco rule conditions based on these fields combined with relational and/or logical operators. For example, given an expression "ct.name=root and

the plugin framework handles parsing the expression, calling the plugin to extract values for a given event, and determining the result of the expression. In a Falco output string like "An EC2 Node was created (name=%ec2.name region=%ct.region)", the plugin framework handles parsing the output string, calling the plugin to extract values for a given event, and building the resolved string. Source plugins also provide an "id", which is globally unique and is used in capture files (see below). An extractor plugin focuses only on field extraction from events generated by other plugins, or by the core libraries. It does not provide an event source, but can extract fields from other event sources. An example is json field extraction, where a plugin might be able to extract fields from arbitrary json payloads. An extractor plugin provides an optional set of event sources. When the framework receives an event with an event source in the plugin's set of event sources, fields in expressions/Falco outputs will be extracted from events using the plugin. An extractor plugin can also not name a set of event sources. In this case, fields will be extracted from all events, regardless of source. In this case, the extractor plugin must detect the format of arbitrary payloads and be able to return NULL/no value when the payload is not supported. libscap will define a new event type called "pluginevent" that contains two fields: "plugin ID": This uniquely identifies the plugin that generated this event. "event_data": This is a variable-length data buffer containing the event data, as returned by the plugin. Defining an event for plugins allows creating capture files from plugins. These capture files can be saved, read, filtered, etc, like any other capture file, allowing for later analysis/display/etc. Plugins are dynamic libraries (.so files in Unix, .dll files in windows) that export a minimum set of functions that the libraries will recognize. Plugins are versioned using semantic versioning to minimize regressions and compatibility issues. Plugins can be written in any language, as long as they export the required functions. Go, however, is the preferred language to write plugins, followed by C/C++. The libraries will do everything possible to validate the data coming from the plugins and protect Falco and the other consumers from corrupted data. However, for performance reasons, plugins will be "trusted": they will run in the same thread and address space as Falco and they could crash the program. We assume that the user will be in control of plugin loading and will make sure only trusted plugins are loaded/packaged with Falco. Every source plugin requires its own, unique plugin ID to interoperate with Falco and the other plugins. The plugin ID will be used by the libs to properly process incoming events (for example, when saving events to file and loading them back), and by plugins to unambiguously recognize their dependencies. To facilitate the allocation and distribution of plugin IDs, we will require that plugin developers request IDs for their plugins to the Falco organization. The mechanism used for plugin allocation is not determined yet and will be discussed in the future. Similarly, plugin developers must register event sources with the Falco

This allows coordination between plugins that wish to provide compatible payloads, and to allow extractor plugins to know what data format is associated with a given event source. To facilitate the development of plugins written in go, an SDK has been developed. We intend this SDK (and future SDKs for other languages) to be part of the Falco organization. For this reason, we submitted the following incubation request: https://github.com/falcosecurity/evolution/issues/62 ```c // This struct represents an event returned by the plugin, and is used // below in next()/next_batch(). // - data: pointer to a memory buffer pointer. The plugin will set it // to point to the memory containing the next event. Once returned, // the memory is owned by the plugin framework and will be freed via // a call to free(). // - datalen: pointer to a 32bit integer. The plugin will set it the size of the // buffer pointed by data. // - ts: the event timestamp. Can be (uint64_t)-1, in which case the engine will // automatically fill the event time with the current time. typedef struct sspluginevent { uint8_t *data; uint32_t datalen; uint64_t ts; } sspluginevent; // // This is the opaque pointer to the state of a plugin. // It points to any data that might be needed plugin-wise. It is // allocated by init() and must be destroyed by destroy(). // It is defined as void because the engine doesn't care what it is // and it treats is as opaque. // typedef void ssplugint; // // This is the opaque pointer to the state of an open instance of the source // plugin. // It points to any data that is needed while a capture is running. It is // allocated by open() and must be destroyed by close(). // It is defined as void because the engine doesn't care what it is // and it treats is as opaque. // typedef void ssinstancet; // // Interface for a sinsp/scap source plugin // // // NOTE: For all functions below that return a char *, the memory // pointed to by the char * must be allocated by the plugin using // malloc() and should be freed by the caller using free(). // // For each function below, the exported symbol from the dynamic // library should have a prefix of "plugin_" // (e.g. plugingetrequiredapiversion, plugin_init, etc.) // typedef struct { // // Return the version of the plugin API used by this plugin. // Required: yes // Return value: the API version string, in the following format: // "<major>.<minor>.<patch>", e.g. "1.2.3". // NOTE: to ensure correct interoperability between the engine and the plugins, // we use a semver approach. Plugins are required to specify the version // of the API they run against, and the engine will take care of checking // and enforcing compatibility. // char (getrequiredapi_version)(); // // Return the plugin type. // Required: yes // Should return TYPESOURCEPLUGIN. It still makes sense to // have a function get_type() as the plugin interface will // often dlsym() functions from shared libraries, and can't // inspect any C struct type. // uint32t (*gettype)(); // // Initialize the plugin and, if needed, allocate its state. // Required: yes // Arguments: // - config: a string with the plugin

The format of the // string is chosen by the plugin itself. // - rc: pointer to an integer that will contain the initialization result, // as a SCAP* value (e.g. SCAPSUCCESS=0, SCAP_FAILURE=1) // Return value: pointer to the plugin state that will be treated as opaque // by the engine and passed to the other plugin functions. // If rc is SCAP_FAILURE, this function should return NULL. // ssplugint (init)(char config, int32_t rc); // // Destroy the plugin and, if plugin state was allocated, free it. // Required: yes // void (destroy)(ss_plugin_t s); // // Return a string with the error that was last generated by // the plugin. // Required: yes // // In cases where any other api function returns an error, the // plugin should be prepared to return a human-readable error // string with more context for the error. The plugin manager // calls getlasterror() to access that string. // char (getlasterror)(ssplugint* s); // // Return the unique ID of the plugin. // Required: yes // EVERY SOURCE PLUGIN (see get_type()) MUST OBTAIN AN OFFICIAL ID FROM THE // FALCOSECURITY ORGANIZATION, OTHERWISE IT WON'T PROPERLY COEXIST WITH OTHER PLUGINS. // uint32t (*getid)(); // // Return the name of the plugin, which will be printed when displaying // information about the plugin. // Required: yes // char (get_name)(); // // Return the descriptions of the plugin, which will be printed when displaying // information about the plugin or its events. // Required: yes // char (get_description)(); // // Return a string containing contact info (url, email, twitter, etc) for // the plugin authors. // Required: yes // char (get_contact)(); // // Return the version of this plugin itself // Required: yes // Return value: a string with a version identifier, in the following format: // "<major>.<minor>.<patch>", e.g. "1.2.3". // This differs from the api version in that this versions the // plugin itself, as compared to the plugin interface. When // reading capture files, the major version of the plugin that // generated events must match the major version of the plugin // used to read events. // char (get_version)(); // // Return a string describing the events generated by this source plugin. // Required: yes // Example event sources would be strings like "syscall", // "k8s_audit", etc. The source can be used by extractor // plugins to filter the events they receive. // char (geteventsource)(); // // Return the list of extractor fields exported by this plugin. Extractor // fields can be used in Falco rule conditions and sysdig filters. // Required: no // Return value: a string with the list of fields encoded as a json // array. // Each field entry is a json object with the following properties: // "type": one of "string", "uint64" // "name": a string with a name for the field // "desc": a string with a description of the field // Example return value: // [ // {"type": "string", "name": "field1", "desc": "Describing field 1"}, // {"type": "uint64", "name": "field2", "desc": "Describing field 2"} // ] char (get_fields)(); // // Open the source and start a capture. // Required: yes // Arguments: // - s: the plugin state returned by init() // - params: the open parameters, as a

The format is defined by the plugin // itself // - rc: pointer to an integer that will contain the open result, as a SCAP_* value // (e.g. SCAPSUCCESS=0, SCAPFAILURE=1) // Return value: a pointer to the open context that will be passed to next(), // close(), eventtostring() and extract_*. // ssinstancet (open)(ssplugint s, char params, int32_t* rc); // // Close a capture. // Required: yes // Arguments: // - s: the plugin context, returned by init(). Can be NULL. // - h: the capture context, returned by open(). Can be NULL. // void (close)(ss_plugin_t s, ssinstancet* h); // // Return the next event. // Required: yes // Arguments: // - s: the plugin context, returned by init(). Can be NULL. // - h: the capture context, returned by open(). Can be NULL. // // - evt: pointer to a sspluginevent pointer. The plugin should // allocate a sspluginevent struct using malloc(), as well as // allocate the data buffer within the sspluginevent struct. // Both the struct and data buffer are owned by the plugin framework // and will free them using free(). // // Return value: the status of the operation (e.g. SCAPSUCCESS=0, SCAPFAILURE=1, // SCAP_TIMEOUT=-1) // int32t (*next)(ssplugint* s, ssinstancet* h, ssplugin_event evt); // // Return the read progress. // Required: no // Arguments: // - progress_pct: the read progress, as a number between 0 (no data has been read) // and 10000 (100% of the data has been read). This encoding allows the engine to // print progress decimals without requiring to deal with floating point numbers // (which could cause incompatibility problems with some languages). // Return value: a string representation of the read // progress. This might include the progress percentage // combined with additional context added by the plugin. If // NULL, progress_pct should be used. // NOTE: reporting progress is optional and in some case could be impossible. However, // when possible, it's recommended as it provides valuable information to the // user. // char (getprogress)(ssplugint* s, ssinstancet* h, uint32t* progress_pct); // // Return a text representation of an event generated by this source plugin. // Required: yes // Arguments: // - data: the buffer from an event produced by next(). // - datalen: the length of the buffer from an event produced by next(). // Return value: the text representation of the event. This is used, for example, // by sysdig to print a line for the given event. // char (eventtostring)(ssplugint s, const uint8_t data, uint32_t datalen); // // Extract a filter field value from an event. // We offer multiple versions of extract(), differing from each other only in // the type of the value they return (string, integer...). // Required: no // Arguments: // - evtnum: the number of the event that is bein processed // - id: the numeric identifier of the field to extract. It corresponds to the // position of the field in the array returned by get_fields(). // - arg: the field argument, if an argument has been specified for the field, // otherwise it's NULL. For example: // * if the field specified by the user is

arg will be the // string "pippo" // * if the field specified by the user is foo.bar, arg will be NULL // - data: the buffer produced by next(). // - datalen: the length of the buffer produced by next(). // - field_present: nonzero if the field is present for the given event. // Return value: the produced value of the filter field. For extract_str(), a // NULL return value means that the field is missing for the given event. // char (extractstr)(ssplugint *s, uint64t evtnum, const char field, const char arg, uint8t *data, uint32t datalen); uint64t (*extractu64)(ssplugint s, uint64_t evtnum, const char field, const char arg, uint8_t data, uint32t datalen, uint32t *field_present); // // This is an optional, internal, function used to speed up event capture by // batching the calls to next(). // On success: // - nevts will be filled in with the number of events. // - evts: pointer to an sspluginevent pointer. The plugin should // allocate an array of contiguous sspluginevent structs using malloc(), // as well as allocate each data buffer within each sspluginevent // struct using malloc(). Both the array of structs and each data buffer are // owned by the plugin framework and will free them using free(). // Required: no // int32t (*nextbatch)(ssplugint s, ss_instance_t h, uint32t *nevts, ssplugin_event evts); // // This is an optional, internal, function used to speed up value extraction // Required: no // int32t (*registerasyncextractor)(ssplugint *s, asyncextractor_info *info); // // The following members are PRIVATE for the engine and should not be touched. // ssplugint* state; ssinstancet* handle; uint32_t id; char *name; } sourceplugininfo; // // Interface for a sinsp/scap extractor plugin // // // NOTE: For all functions below that return a char *, the memory // pointed to by the char * must be allocated by the plugin using // malloc() and should be freed by the caller using free(). // typedef struct { // // Return the version of the plugin API used by this plugin. // Required: yes // Return value: the API version string, in the following format: // "<major>.<minor>.<patch>", e.g. "1.2.3". // NOTE: to ensure correct interoperability between the engine and the plugins, // we use a semver approach. Plugins are required to specify the version // of the API they run against, and the engine will take care of checking // and enforcing compatibility. // char (getrequiredapi_version)(); // // Return the plugin type. // Required: yes // Should return TYPEEXTRACTORPLUGIN. It still makes sense to // have a function get_type() as the plugin interface will // often dlsym() functions from shared libraries, and can't // inspect any C struct type. // uint32t (*gettype)(); // // Initialize the plugin and, if needed, allocate its state. // Required: yes // Arguments: // - config: a string with the plugin configuration. The format of the // string is chosen by the plugin itself. // - rc: pointer to an integer that will contain the initialization result, // as a SCAP* value (e.g. SCAPSUCCESS=0, SCAP_FAILURE=1) // Return value: pointer to the plugin state that will be treated as opaque // by the engine and passed to the other plugin functions. // ssplugint (init)(char config, int32_t rc); // // Destroy the plugin and, if plugin state was allocated, free

// Required: yes // void (destroy)(ss_plugin_t s); // // Return a string with the error that was last generated by // the plugin. // Required: yes // // In cases where any other api function returns an error, the // plugin should be prepared to return a human-readable error // string with more context for the error. The plugin manager // calls getlasterror() to access that string. // char (getlasterror)(ssplugint* s); // // Return the name of the plugin, which will be printed when displaying // information about the plugin. // Required: yes // char (get_name)(); // // Return the descriptions of the plugin, which will be printed when displaying // information about the plugin or its events. // Required: yes // char (get_description)(); // // Return a string containing contact info (url, email, twitter, etc) for // the plugin author. // Required: yes // char (get_contact)(); // // Return the version of this plugin itself // Required: yes // Return value: a string with a version identifier, in the following format: // "<major>.<minor>.<patch>", e.g. "1.2.3". // This differs from the api version in that this versions the // plugin itself, as compared to the plugin interface. When // reading capture files, the major version of the plugin that // generated events must match the major version of the plugin // used to read events. // char (get_version)(); // // Return a string describing the event sources that this // extractor plugin can consume. // Required: no // Return value: a json array of strings containing event // sources returned by a source plugin's geteventsource() // function. // This function is optional--if NULL then the extractor // plugin will receive every event. // char (getextractevent_sources)(); // // Return the list of extractor fields exported by this plugin. Extractor // fields can be used in Falco rules and sysdig filters. // Required: yes // Return value: a string with the list of fields encoded as a json // array. // char (get_fields)(); // // Extract a filter field value from an event. // We offer multiple versions of extract(), differing from each other only in // the type of the value they return (string, integer...). // Required: for plugins of type TYPEEXTRACTORPLUGIN only // Arguments: // - evtnum: the number of the event that is being processed // - id: the numeric identifier of the field to extract. It corresponds to the // position of the field in the array returned by get_fields(). // - arg: the field argument, if an argument has been specified for the field, // otherwise it's NULL. For example: // * if the field specified by the user is foo.bar[pippo], arg will be the // string "pippo" // * if the field specified by the user is foo.bar, arg will be NULL // - data: the buffer produced by next(). // - datalen: the length of the buffer produced by next(). // - field_present: nonzero if the field is present for the given event. // Return value: the produced value of the filter field. For extract_str(), a // NULL return value means that the field is missing for the given

// char (extractstr)(ssplugint *s, uint64t evtnum, const char field, const char arg, uint8t *data, uint32t datalen); uint64t (*extractu64)(ssplugint s, uint64_t evtnum, const char field, const char arg, uint8_t data, uint32t datalen, uint32t *field_present); } extractorplugininfo; ``` Falco rules already have the notion of a "source", using the source property in rules objects, and there are currently two kinds of event sources: "syscall" and "k8s_audit". We will use the source property in Falco rules to map a given rule to the event source on which the rule runs. For example, given a plugin with source "awscloudtrail", and a Falco rule with source "awscloudtrail", the rule will be evaluated for any events generated by the plugin. Similarly, an extractor plugin that includes "awscloudtrail" in its set of event sources will have the opportunity to extract information from awscloudtrail events if a matching field is found in the rule's condition, exception, or output properties. This, combined with the restrictions below, allows a set of loaded rules files to contain a mix of rules for plugins as well as "core" syscall/k8s_audit events. We will also make a change to compile rules/macros/lists selectively based on the set of loaded plugins (specifically, their event sources), instead of unconditionally as Falco is started. This is especially important for macros, which do not contain a source property, but might contain fields that are only implemented by a given plugin. At an initial glance, adding plugins introduces the possibility of tens/hundreds of new filtercheck fields that could potentially overlap/conflict. For example, what happens if a plugin defines a "proc.name" field? However, the notion of "event source" makes these potential conflicts manageable. Remember that field extraction is always done in the context of an event, and each event can be mapped back to an event source. So we only need to ensure that filtercheck fields are distinct for a given event source. For example, it's perfectly valid for an AWS Cloudtrail plugin to define a proc.name field, as the events generated by that plugin are wholly separate from syscall events. For syscall events, the AWS Cloudtrail plugin is not involved and the core libraries extract the process name for the tid performing a syscall. For AWS Cloudtrail events, the core libraries are not involved in field extraction and is performed by the AWS Cloudtrail plugin instead. We only need to ensure the following: That only one plugin is loaded at a time that exports a given event source. For example, the libraries can load either a gke-audit-bridge plugin with event source k8saudit, or eks-audit-bridge with event source k8saudit, but not both. That for a mix of source and extractor plugins having the same event source, that the fields are distinct. For example, a source plugin with source k8s_audit can export ka. fields, and an extractor plugin with event source k8saudit can export a jevt.value[/...] field, and the appropriate plugin will be used to extract fields from k8saudit events as fields are parsed from condition expressions/output format strings. To allow rules files to document the plugin versions they are compatible with, we will add a new top-level field `requiredpluginversions` to the Falco rules file format. The field is optional, and if not provided no plugin compatibility checks will be performed. The syntax of `requiredpluginversions` will be the following: ```yaml requiredpluginversions: name: <plugin_name> version: x.y.z

``` Below requiredpluginversions is a list of objects, where each object has `name` and `version` properties. If a plugin is loaded, and if an entry in `requiredpluginversions` has a matching name, then the loaded plugin version must be semver compatible with the version property. Falco can load multiple rules files, and each file may contain its own `requiredpluginversions` property. In this case, name+version pairs across all files will be merged, and in the case of duplicate names all provided versions must be compatible. The mechanics of loading a plugin are implemented in the libraries and leverage the dynamic library functionality of the operating system (dlopen/dlsym in unix, LoadLibrary/GetProcAddress in Windows). The plugin loading code also ensures that: the plugin is valid, i.e. that it exports the set of expected symbols the plugin has an api version number that is compatible with the libraries instance that only one source plugin is loaded at a time for a given event source if a mix of source and extractor plugins are loaded for a given event source, that the exported fields have unique names that don't overlap across plugins At the libraries level, loading plugins is handled via the static method: ```c++ void sinspplugin::registerplugin(sinsp inspector, string filepath, char config, ...) ``` filepath points to a dynamic library containing code that exports plugin API functions. config contains arbitrary config content which is passed to init(). Note that the code using the libraries is responsible for determining the location of plugin libraries and their configuration. Falco will control/configure loading plugins via the new "plugins" property in falco.yaml. Here's an example: ```yaml plugins: name: aws_cloudtrail librarypath: awscloudtrail/plugin.so init_config: "..." open_params: "..." name: http_json librarypath: httpjson/plugin.so initconfigfile: http_json/config.txt openparamsfile: http_json/params.txt loadplugins: [awscloudtrail] ``` A new "plugins" property in falco.yaml will define the set of plugins that can be loaded by Falco. The property contains a list of objects, with the following properties: name: Only used for load_plugins, but by convention should be the same as the value returned by the name() api function. library_path: a path to the shared library. The path can be relative, in which case it is relative to Falco's "share" directory under a "plugins" subdirectory e.g. /usr/share/falco/plugins. init_config: If present, the exact configuration text that will be provided as an argument to the init() function. initconfigfile: If present, the provided file will be read and the contents will be provided as an argument to the init() function. open_params: If present, the exact params text that will be provided as an argument to the open() function. openparamsfile: If present, the provided file will be read and the contents will be provided as an argument to the open() function. For a given yaml object in the plugins list, only one of initconfig/initconfigfile and one of openparams/openparamsfile can be provided at a time. A new "load_plugins" property in falco.yaml will allow for loading a subset of the plugins defined in plugins. If present, only the plugins with the provided names will be loaded. We have an initial version working, consisting of: A version of falcosecurity/libs that supports the Support code and examples for A plugin that can generate events from cloudtrail logs and extract fields from those events. A version of Falco that uses all of the above to