Modules
Introduction
A module is an independent piece of code that can be run during scan. Its job is merely to enrich the store. It is not fully independent as it may depend on previous modules (some module are likely to need data provided by others).
To develop a module, just init a new my_new_module.go
source file in the modules/
subdirectory. The structure of the module should look like the following snippet.
package modules
import (
// ...
)
func init() {
m := &MyNewModule{}
RegisterModule(m)
// SetDefault
SetDefault(m, "myparam", value, "the value of myparam")
// ...
}
type MyNewModule struct {}
// Name returns the name of the module
func (m * MyNewModule) Name() string {
// return the name of the module with a dash
return "my-new-module"
}
// Dependencies return the list of modules
// required to run this one
func (m * MyNewModule) Dependencies() []string {
// put the name of the modules you depend on here
return []string{"host-basic"}
}
// Run do the job. It returns error only if it really
// fails, i.e. it cannot be run (like privileges).
// In the other cases, just log the errors
func (m * MyNewModule) Run() error {
// you can grab your logger (from https://github.com/Sirupsen/logrus)
logger := GetLogger(m)
// you can grab config with GetConfig.
// :warning: generics are used to access the config
// GetConfig[<TYPE>](>MODULE>, <KEY>)
myParam := GetConfig[time.Duration](m, "myparam")
// ...
// do what you want
// ...
// but do not return error except if something
// prevents the module to be run, just log them:
// logger.Error(err)
// ...
//
// don't forget to put data into the store
// ...
return nil
}
Naming
You are free about the module naming, but obviously there are some constraints:
- the module name must be unique
- the name should describe what the module does (or the ecosystem, like "docker")
- If you wan to create a module called "awesome stuff":
- its name (output of
.Name()
) must beawesome-stuff
- the object that respects the
Module
interface must beAwesomeStuffModule
- the source file must be
awesome_stuff.go
Module interface
A module must implement the Module
interface described below.
// Module is the generic module interface to implement plugins to
// the agent
type Module interface {
Name() string
Dependencies() []string
Run() error
}
The Name()
outputs the [unique] name of the module.
The Dependencies()
returns the name of the modules required to start this module (prior information).
The Run()
function does the job. This functions is called during the scan. It may have several interactions:
- config (get extra configuration data)
- logging (output some information about the run)
- store (retrieve/store collected data)
Configuration
The configuration is only managed by the flags of urfave/cli.
The configuration of the modules are stored in the modules.module-name.*
namespace. To hide it to the developper, the modules
package expose two helpers:
// GetConfig is a generic function that returns a value
// associated to a key within the module namespace
func GetConfig[T any](m Module, key string) (T, error) {
// ...
}
// SetDefault is a helper that defines default module parameter.
// The provided values can be overwritten by CLI flags or config file.
func SetDefault(m Module, key string, value interface{}, usage string) {
// ...
}
Logging
The logging is managed by logrus. To log some information, the modules
package expose a GetLogger
function that returns a contextual logger (relative to the module).
func (m * MyModule) Run() error {
// ...
logger := GetLogger(m)
// now you can use the classical methods
logger.Debug("Debug message")
logger.Info("Info message")
logger.Warn("Warning message")
logger.Error("Error message")
logger.Fatal("Fatal error")
// you should avoid logger.Panic to prevent the agent from crashing
// ...
}
In addition, the module is likely to collect some information. You can log the collected data in a structured manner with the logger.WithField
method.
func (m * MyModule) Run() error {
// ...
logger := GetLogger(m)
// ...
// append the fields you want to show and call Debug/Info method
logger.WithField("hostname", hostname).Debug("Hostname found!")
}
Big module case
if your module is heavy you can store all the work (namely the material for the Run
function) inside a submodule and write a short interface in the modules
directory.
You may have the following layout:
modules/
heavy.go
heavy/
file1.go
file2.go
...
The heavy.go
file may look like the following:
import (
// ...
// load the submodule
"github.com/situation-sh/situation/modules/heavy"
)
type HeavyModule struct {}
func init() {
RegisterModule(&HeavyModule{})
}
func (m * HeavyModule) Name() string {
// return the name of the module with a dash
return "heavy"
}
func (m * HeavyModule) Dependencies() []string {
// put the name of the modules you depend on here
return []string{}
}
func (m * HeavyModule) Run() error {
// ...
// call heavy.Stuff
// ...
}
Documentation
Documenting a module is mandatory. There are two things to do. The first thing is to document the Module
object as follows:
// MyNewModule retrieves data from ...
//
// It mainly depends on the following external library:
// - ...
//
// On Windows, it collect data by calling...
// On Linux, it reads ...
type MyNewModule struct {}
One must have a synospis (first line) and then some details about the module. One may include how data is collected with regards to the platform and also other relevant things (edge cases, libraries, privileges, options etc.)
The second point is to fill some standard notes, as follows:
// LINUX(MyNewModule) ok
// WINDOWS(MyNewModule) ok
// MACOS(MyNewModule) ?
// ROOT(MyNewModule) no
package modules
The format of the note is given by the doc package. We use it as follows: <KEY>(<MODULE-NAME>) <VALUE>
Currently there are 4 attributes to provide: LINUX
, WINDOWS
, MACOS
and ROOT
. Their corresponding values must be
yes
/ok
(meaning "supported"), no
(meaning "not supported"), or ?
(meaning "don't know").
Warning
For ROOT
, yes
/ok
means that root privileges are required