sec(1)

πŸ“¦
version 2.9.4 Β· 5 versions
, in epel 10, epel 9, fedora 43, fedora 44, nixos 26.05, nixpkgs unstable, ubuntu resolute, ubuntu stonking

πŸ’– sec(1) β€” General Commands Manual πŸ’–

okay bestie, pull up a chair because we're about to talk about sec, aka the simple event correlator, and honestly? it's giving main character energy in the world of log-watching. this is the General Commands Manual page for it. no cap. πŸ¦‹


✨ NAME ✨

sec β€” simple event correlator

like... that's it, that's the whole name section. short, sweet, iconic. she's simple and she correlates events, periodt. πŸ’…


✨ SYNOPSIS ✨

here's the whole vibe, exactly as she comes, no edits no filter:

sec    [--conf=<file pattern> ...]
       [--input=<file pattern>[=<context>] ...]
       [--input-timeout=<input timeout>]
       [--timeout-script=<timeout script>]
       [--reopen-timeout=<reopen timeout>]
       [--check-timeout=<check timeout>]
       [--poll-timeout=<poll timeout>]
       [--socket-timeout=<socket timeout>]
       [--blocksize=<io block size>]
       [--bufsize=<input buffer size>]
       [--evstoresize=<event store size>]
       [--cleantime=<clean time>]
       [--log=<logfile>]
       [--syslog=<facility>]
       [--debug=<debuglevel>]
       [--pid=<pidfile>]
       [--dump=<dumpfile>]
       [--user=<username>]
       [--group=<groupname> ...]
       [--umask=<mode>]
       [--ruleperf | --noruleperf]
       [--dumpfts | --nodumpfts]
       [--dumpfjson | --nodumpfjson]
       [--quoting | --noquoting]
       [--tail | --notail]
       [--fromstart | --nofromstart]
       [--detach | --nodetach]
       [--jointbuf | --nojointbuf]
       [--keepopen | --nokeepopen]
       [--rwfifo | --norwfifo]
       [--childterm | --nochildterm]
       [--intevents | --nointevents]
       [--intcontexts | --nointcontexts]
       [--testonly | --notestonly]
       [--help] [-?]
       [--version]

lowkey a lot of brackets, highkey every single one of them matters. every <angle bracket> thingy is a placeholder you swap for your actual value, and every | means "pick one of these two, bestie, not both." the ... after some of them? that's the "you can stack these, go off" signal. 🌈

heads up: this is only part 1 of 26, so don't go anywhere β€” there's a whole rest of the page coming and it's so worth it. 🫢

πŸ’– DESCRIPTION πŸ’–

Okay bestie, listen up, because this is the moment. SEC is an event correlation tool for advanced event processing β€” like, the leveled-up kind. You can harness it for event log monitoring, for network and security management, for fraud detection, and honestly for any other task that involves event correlation. Periodt. ✨

And what is event correlation, you ask? It's a procedure where a stream of events gets processed, all so we can detect (and act on) certain event groups that happen inside predefined time windows. That's it, that's the whole vibe.

Unlike a bunch of other event correlation products out there that are these whole heavyweight situations, SEC is lightweight and platform-independent and runs as a single process. You can start it as a daemon, you can employ it in shell pipelines, you can execute it interactively in a terminal, you can run many SEC processes simultaneously for different tasks β€” and honestly the ways you can use it are a whole wide variety of other things too. 🌈

Here's how it moves: SEC reads lines from files, named pipes, or standard input, then matches those lines with patterns (regular expressions, Perl subroutines, etc.) to recognize input events, and then correlates the events according to the rules in its configuration file(s). Rules are matched against input in the order they are given in the configuration file. And if there are two or more configuration files, the rule sequence from every file is matched against input (unless explicitly specified otherwise, iykyk). SEC can produce output by executing external programs (e.g., snmptrap(1) or mail(1)), by writing to files, by sending data to TCP and UDP based servers, by calling precompiled Perl subroutines, etc. The output options? Endless. πŸ’…


SEC can be run in various ways. For example, the following command line starts it as a daemon, in order to monitor events appended to the /var/log/messages syslog file with rules from /etc/sec/syslog.rules:

/usr/bin/sec --detach --conf=/etc/sec/syslog.rules \
             --input=/var/log/messages

Each time /var/log/messages is rotated, a new instance of /var/log/messages is opened and processed from the beginning. πŸŒ€ The following command line runs SEC in a shell pipeline, configuring it to process lines from standard input, and to exit when the /usr/bin/nc tool closes its standard output and exits:

/usr/bin/nc -l 8080 | /usr/bin/sec --notail --input=- \
                                   --conf=/etc/sec/my.conf

Some SEC rules start event correlation operations, while other rules react immediately to input events or the system clock. So, like, suppose SEC has been started with this command line:

/usr/bin/sec --conf=/etc/sec/sshd.rules --input=/var/log/secure

...in order to monitor the /var/log/secure syslog file for sshd events. And also suppose that the /etc/sec/sshd.rules configuration file contains the following rule for correlating SSH failed login syslog events:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m for user $1
action=pipe '%s' /bin/mail -s 'SSH login alert' root@localhost
window=60
thresh=3

The pattern field of the rule defines the pattern for recognizing input events, while the ptype field defines its type (regular expression). Now picture this: user risto fails to log in over SSH, and this message gets logged to /var/log/secure:

Dec 16 16:24:59 myserver sshd[13685]: Failed password for risto from
10.12.2.5 port 41063 ssh2

This input message will match the regular expression pattern of the above rule, and the match variable $1 will be set to the string risto (see perlre(1) for details). After a match, SEC will evaluate the operation description string given with the desc field. This is done by substituting $1 with its current value, which yields Three SSH login failures within 1m for user risto. SEC will then check if there already exists an event correlation operation identified with this string and triggered by the same rule. If the operation is not found, SEC will create a new operation for the user name risto, and the occurrence time of the input event will be recorded into the operation.

⚠️ Note that for event occurrence time SEC always uses the current time as returned by the time(2) system call, not the timestamp extracted from the event. Fr fr. Don't get it twisted.

Okay so suppose that after 25 seconds, a similar SSH login failure event for the same user name is observed. In this case, a running operation will be found for the operation description string Three SSH login failures within 1m for user risto, and the occurrence time of the second event is recorded into the operation. Then if after 30 seconds a third event for the user name risto is observed, the operation has processed 3 events within 55 seconds. Since the threshold condition "3 events within 60 seconds" (as defined by the thresh and window fields) is now satisfied, SEC will execute the action defined with the action field β€” it will fork a command:

/bin/mail -s 'SSH login alert' root@localhost

...with a pipe connected to its standard input. Then, SEC writes the operation description string Three SSH login failures within 1m for user risto (held by the %s special variable) to the standard input of the command through the pipe. In other words, an e-mail warning is sent to the local root-user. πŸ“§βœ¨ And finally, since there are 5 seconds left until the end of the event correlation window, the operation will consume the following SSH login failure events for user risto without any further action, and finish after those 5 seconds. She ate and left no crumbs.


The above example illustrates that the desc field of a rule defines the scope of event correlation and influences the number of operations created by the rule. For example, if we set the desc field to Three SSH login failures within 1m, the root-user would be also alerted on 3 SSH login failure events for different users within 1 minute. πŸ™ˆ

In order to avoid clashes between operations started by different rules, the operation ID contains not only the value set by the desc field, but also the rule file name and the rule number inside the file. For example, if the rule file /etc/sec/sshd.rules contains one rule:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m for user $1
action=pipe '%s' /bin/mail -s 'SSH login alert' root@localhost
window=60
thresh=3

...and the event:

Dec 16 16:24:59 myserver sshd[13685]: Failed password for risto from
10.12.2.5 port 41063 ssh2

...is the first matching event for the above rule, this event will trigger a new event correlation operation with the ID:

/etc/sec/sshd.rules | 0 | Three SSH login failures within 1m for user
risto

(0 is the number assigned to the first rule in the file, see EVENT CORRELATION OPERATIONS section for more information). πŸŽ€


The following simple example demonstrates that event correlation schemes can be defined by combining several rules. In this example, two rules harness contexts and synthetic events for achieving their goal:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m for user $1
action=event 3_SSH_LOGIN_FAILURES_FOR_$1
window=60
thresh=3

type=EventGroup
ptype=RegExp
pattern=3_SSH_LOGIN_FAILURES_FOR_(\S+)
context=!USER_$1_COUNTED && !COUNTING_OFF
count=create USER_$1_COUNTED 60
desc=Repeated SSH login failures for 30 distinct users within 1m
action=pipe '%s' /bin/mail -s 'SSH login alert' root@localhost; \
       create COUNTING_OFF 3600
window=60
thresh=30

The first rule looks almost identical to the rule from the previous example, but its action field is different β€” after three SSH login failures have been observed for the same user name within one minute by an event correlation operation, the operation will emit the synthetic event 3_SSH_LOGIN_FAILURES_FOR_<username>. And here's the cute part: although synthetic events are created by SEC, they are treated like regular events received from input sources and are matched against rules. πŸ‘‘

The regular expression pattern of the second rule will match the 3_SSH_LOGIN_FAILURES_FOR_<username> event and start a new event correlation operation if no such events have been previously seen. Also, each time a synthetic event for some user name has matched the rule, a context with the lifetime of 1 minute for that user name is created (see the count field). Note that this prevents further matches for the same user name, since a synthetic event for <username> can match the rule only if the context USER_<username>_COUNTED does not exist (as requested by the boolean expression in the context field; see CONTEXTS AND CONTEXT EXPRESSIONS section for more information). πŸ’…

The operation started by the second rule sends an e-mail warning to the local root-user if 30 synthetic events have been observed within 1 minute (see the thresh and window fields). Note that due to the use of the USER_<username>_COUNTED contexts, all synthetic events concern different user names. After sending an e-mail warning, the operation will also create the context COUNTING_OFF with the lifetime of 1 hour, and will continue to run until the 1 minute event correlation window expires. After the operation has finished, the presence of the COUNTING_OFF context will keep the second rule disabled (as requested by the boolean expression in the context field). Therefore, at most one e-mail warning per 1 hour is issued by the above rules. No spam, only slay. πŸ¦‹


The above examples have presented the event correlation capabilities of SEC in a very brief fashion. The following sections will provide an in-depth discussion of SEC features. Buckle up bestie, we're going deep. 🎒

πŸŽ€ OPTIONS πŸŽ€

  • --conf=<file_pattern> β€” expand <file_pattern> to filenames (with the Perl glob() function) and read event correlation rules from every file. You can specify multiple --conf options at the command line. Each time SEC receives a signal that forces a configuration reload, <file_pattern> is re-evaluated. See also INPUT PROCESSING AND TIMING section for a discussion on rule processing order for multiple configuration files. 🌸
  • --input=<file_pattern>[=<context>] β€” okay so this one expands <file_pattern> to filenames using the Perl glob() function and uses those files as input sources. An input file can be a regular file, a named pipe, or standard input if you specified -. You can drop multiple --input options on the command line. Every time SEC catches a SIGHUP or SIGABRT signal, <file_pattern> gets re-evaluated. If SEC hits a system error while reading from an input file, it will close the file (use the --reopen-timeout option to reopen it). If <context> is given, SEC will set up the context <context> each time it reads a line from input files that match <file_pattern>. This helps you write rules that only match data from particular input source(s). When there's an --input option with <context> specified, it automatically enables the --intcontexts option. See the INTERNAL EVENTS AND CONTEXTS section for more info. πŸ’…
  • --input-timeout=<input_timeout>, --timeout-script=<timeout_script> β€” if SEC hasn't seen new data in an input file for <input_timeout> seconds (or the file was closed <input_timeout> seconds ago), <timeout_script> gets executed with command line parameters 1 and <the name of the input file>. If fresh data shows up again, <timeout_script> runs with command line parameters 0 and <the name of the input file>. Setting <input_timeout> to 0 disables this behavior (and that's also the default). Note that --input_timeout and --timeout_script can be used as synonyms for --input-timeout and --timeout-script, respectively. ✨
  • --reopen-timeout=<reopen_timeout> β€” if an input file is in the closed state (like, SEC fails to open it at startup because it hasn't been created yet), SEC will try to reopen the file every <reopen_timeout> seconds until it succeeds. Setting <reopen_timeout> to 0 disables this behavior (again, that's the default). This option has no meaning when the --notail option is also specified. Note that --reopen_timeout is a synonym for --reopen-timeout. πŸ¦‹
  • --check-timeout=<check_timeout> β€” if SEC hasn't observed new data in an input file, that file won't be polled (neither for status nor data) during the next <check_timeout> seconds. Setting <check_timeout> to 0 disables this behavior (yep, default). Note that --check_timeout is a synonym for --check-timeout. 🌸
  • --poll-timeout=<poll_timeout> β€” a real number that says how many seconds SEC will sleep when no new data was read from input files. Default is 0.1 seconds. Note that --poll_timeout is a synonym for --poll-timeout. ⭐
  • --socket-timeout=<socket_timeout> β€” if a network connection to a remote peer can't be established within <socket_timeout> seconds, just give up. Default is 60 seconds. Note that --socket_timeout is a synonym for --socket-timeout. πŸ“
  • --blocksize=<io_block_size> β€” the number of bytes SEC will try to read at once from an input file. Default is 8192 bytes (so, reading from input files in 8KB blocks). πŸ’œ
  • --bufsize=<input_buffer_size> β€” set all input buffers to hold <input_buffer_size> lines. The content of input buffers gets compared with patterns that are part of rule definitions (meaning no more than <input_buffer_size> lines can be matched by a pattern at a time). If <input_buffer_size> is set to 0, SEC will figure out the proper value by checking event matching patterns of all SEC rules. Default is 0 (so it determines the size of input buffers automatically). 🌈
  • --evstoresize=<event_store_size> β€” set an upper limit to the number of events in context event stores. Default is 0, which sets no limit. πŸ”₯
  • --cleantime=<clean_time> β€” time interval in seconds that specifies how often internal event correlation and context lists are processed, to accomplish time-related tasks and remove obsolete elements. See the INPUT PROCESSING AND TIMING section for more info. Default is 1 second. πŸ‘‘
  • --log=<logfile> β€” use <logfile> for logging SEC activities. Note that if SEC's standard error is connected to a terminal, messages will also be logged there, to make debugging easier. πŸ’–
  • --syslog=<facility> β€” use syslog for logging SEC activities. All messages will be logged with the facility <facility>, e.g., local0 (see syslog(3) for possible facility values). Warning: be careful with this option if SEC is monitoring syslog log files, because message loops might occur. πŸ’€
  • --debug=<debuglevel> β€” set logging verbosity for SEC. Setting debuglevel to <debuglevel> means all messages of level <debuglevel> and lower are logged (e.g., if <debuglevel> is 3, messages from levels 1-3 are logged). The following levels are recognized by SEC:
    • 1 - critical messages (severe faults that cause SEC to terminate, e.g., a failed system call)
    • 2 - error messages (faults that need attention, e.g., an incorrect rule definition in a configuration file)
    • 3 - warning messages (possible faults, e.g., a command forked from SEC terminated with a non-zero exit code)
    • 4 - notification messages (normal system level events and interrupts, e.g., the reception of a signal)
    • 5 - informative messages (information about external programs forked from SEC)
    • 6 - debug messages (detailed information about all SEC activities) Default <debuglevel> is 6 (so it logs everything). See the SIGNALS section for info on how to change <debuglevel> at runtime. 😭
  • --pid=<pidfile> β€” SEC will store its process ID to <pidfile> at startup. 🫢
  • --dump=<dumpfile> β€” SEC will use <dumpfile> as its dump file for writing performance and debug data. With the --dumpfts option, a timestamp suffix is appended to the dump file name. With the --dumpfjson option, the dump file is produced in JSON format. See the SIGNALS section for more info. Default is /tmp/sec.dump. πŸ’…
  • --user=<username>, --group=<groupname> β€” if SEC is started with effective user ID 0, it will drop root privileges by switching to user <username> and group <groupname>. The --group option can't be used without the --user option. If --user is given without --group, the primary group of user <username> is assumed for <groupname>. If several groups are provided with multiple --group options, SEC switches to the first group with the other groups as supplementary groups. πŸ‘‘
  • --umask=<mode> β€” set the file mode creation mask to <mode> at SEC startup, where <mode> is a value from the range 0..0777 (see also umask(2)). Octal, decimal, hexadecimal, and binary values can be specified for <mode> (e.g., octal mask 0027 can also be expressed as 23, 0x17, and 0b000010111). ✨
  • --ruleperf, --noruleperf β€” if --ruleperf is specified, performance data (e.g., total consumed CPU time) is collected for each rule and reported in the dump file. Default is --noruleperf. πŸ”₯
  • --dumpfts, --nodumpfts β€” if --dumpfts is specified, a timestamp suffix (seconds since Epoch) is appended to the dump file name that reflects the file creation time. Default is --nodumpfts. πŸ¦‹
  • --dumpfjson, --nodumpfjson β€” if --dumpfjson is specified, the dump file is produced in JSON format. Default is --nodumpfjson. 🌸
  • --quoting, --noquoting β€” if --quoting is specified, operation description strings that are supplied to command lines of shellcmd, spawn, and cspawn actions will be put inside single quotes. Each single quote (') that strings originally contain will be masked. This option prevents the shell from interpreting special symbols that operation description strings might contain. Default is --noquoting. πŸ’œ
  • --tail, --notail β€” if --notail is specified, SEC will process all data that is currently available in input files and exit after reaching all EOFs. If all input is received from a pipe and --notail is given, SEC terminates when the last writer closes the pipe (EOF condition). Please note that with named pipes --notail should be used with --norwfifo. With --tail, SEC will jump to the end of input files and wait for new lines to arrive. Each input file is tracked both by its name and i-node, and input file rotations are handled seamlessly. If the input file is recreated or truncated, SEC will reopen it and process its content from the beginning. If the input file is removed (i.e., there's just an i-node left without a name), SEC will keep the i-node open and wait for the input file recreation. Default is --tail. πŸ’€
  • --fromstart, --nofromstart β€” these flags have no meaning when --notail is also specified. When used in combination with --tail (or alone, since --tail is enabled by default), --fromstart will force SEC to read and process input files from the beginning to the end, before the 'tail' mode is entered. Default is --nofromstart. 🌈
  • --detach, --nodetach β€” if --detach is specified, SEC will disassociate itself from the controlling terminal and become a daemon at startup (note that SEC will close its standard input, standard output, and standard error, and change its working directory to the root directory). Default is --nodetach. πŸ‘‘
  • --jointbuf, --nojointbuf β€” okay so with --jointbuf, SEC uses one joint input buffer for ALL the input sources (size set by --bufsize, bestie πŸ’–). With --nojointbuf, every input file gets its own separate buffer, plus there's a separate buffer for all synthetic and internal events (and yeah, all of those sizes come from --bufsize too). The tea here is: --jointbuf lets multiline patterns match lines from several input sources, while --nojointbuf keeps the matching restricted to lines from just one input source. Head to the INPUT PROCESSING AND TIMING section for the whole story. And if the size of the input buffer(s) is 1 (either you set it with --bufsize=1 or it got auto-determined from the SEC rules), --jointbuf is enabled β€” otherwise the default is --nojointbuf.
  • --keepopen, --nokeepopen β€” with --keepopen, SEC keeps input files open across soft restarts. When the SIGABRT signal shows up, SEC will not reopen input files that were already opened β€” it only opens the ones sitting in the closed state. --nokeepopen forces SEC to close and (re)open every single input file during soft restarts. Default is --keepopen.
  • --rwfifo, --norwfifo β€” with --norwfifo, named pipe input files get opened in read-only mode. In that mode, the named pipe has to be reopened when the last writer closes the pipe, so the EOF condition on the pipe gets cleared. With --rwfifo, named pipe input files are opened in read-write mode β€” even though SEC literally never writes to the pipes. In this mode the pipe doesn't need reopening when an external writer closes it, because there's always at least one writer on the pipe and EOF will never appear. So like β€” if you've given --notail, you should also give --norwfifo. Default is --rwfifo.
  • --childterm, --nochildterm β€” with --childterm, SEC sends the SIGTERM signal to all its child processes when it terminates or goes through a full restart. Default is --childterm.
  • --intevents, --nointevents β€” SEC will generate internal events when it starts up, when it receives certain signals, and when it terminates gracefully. You can write specific rules to match those internal events so you can do special tasks at SEC startup, restart, and shutdown πŸ‘‘. See INTERNAL EVENTS AND CONTEXTS for more. Default is --nointevents.
  • --intcontexts, --nointcontexts β€” SEC will create an internal context when it reads a line from an input file. This helps you write rules that match data from one particular input source only. See INTERNAL EVENTS AND CONTEXTS for more. Default is --nointcontexts.
  • --testonly, --notestonly β€” with --testonly, SEC exits immediately after parsing the configuration file(s). If the configuration file(s) had no faulty rules, SEC exits with 0 β€” otherwise with 1. Default is --notestonly.
  • --help, -? β€” SEC outputs usage info and dips ✨.
  • --version β€” SEC outputs version info and dips ✨.
ngl this next bit is important: options can be introduced with either the single dash (-) or the double dash (--), and both the equal sign (=) and whitespace can separate the option name from the option value. For example, -conf=<file_pattern> and --conf <file_pattern> are equivalent. No cap. πŸ’…

πŸ“‚ CONFIGURATION FILES πŸ“‚

Every SEC configuration file is made of rule definitions, and they're separated by empty lines, whitespace lines and/or comment lines. Each rule definition is a bunch of keyword=value fields β€” one keyword and value per line. Values are case insensitive only where character case doesn't matter (like the values specifying rule types β€” e.g., 'Single' and 'single' are treated identically 🫢). The backslash character (\) can be used at the end of a line to continue the current rule field onto the next line. Lines starting with the number sign (#) are comments and get ignored (whitespace characters may come before the #). Any comment line, empty line, whitespace line, or end of file will terminate the preceding rule definition. And if you wanna drop comments inside rule definitions, the rem keyword is your girl. For example, these lines define two rules:

type=Single
rem=this rule matches any line which contains \
    three consecutive A characters and writes the string \
    "three A characters were observed" to standard output
ptype=SubStr
pattern=AAA
desc=Three A characters
action=write - three A characters were observed
# This comment line ends preceding rule definition.
# The following rule works like the previous rule,
# but looks for three consecutive B characters and
# writes the string "three B characters were observed"
# to standard output
type=Single
ptype=SubStr
pattern=BBB
desc=Three B characters
action=write - three B characters were observed

Apart from keywords that are part of rule definitions, label keywords may appear anywhere in the configuration file. The value of each label keyword gets treated as a label that can be referred to in rule definitions as a point-of-continue. This is the glow-up that lets event processing continue at a rule following the label, after the current rule has matched and processed the event. 🌈

The points-of-continue are defined with continue* fields. Accepted values for these fields are:

  • TakeNext β€” after an event has matched the rule, the search for matching rules in the configuration file continues from the next rule.
  • GoTo <label> β€” after an event has matched the rule, the search for matching rules continues from the location of <label> in the configuration file. Heads up bestie: <label> must be defined with the label keyword anywhere in the configuration file after the current rule definition.
  • DontCont (default value) β€” after an event has matched the rule, the search for matching rules ends in the current configuration file.
  • EndMatch β€” after an event has matched the rule, the search for matching rules ends for all configuration files.

SEC rules from the same configuration file are matched against input in the order they've been given in the file. For example, consider a configuration file containing this rule sequence:

type=Single
ptype=SubStr
pattern=AAA
rem=after this rule has matched, continue from last rule
continue=GoTo lastRule
desc=Three A characters
action=write - three A characters were observed

type=Single
ptype=SubStr
pattern=BBB
rem=after this rule has matched, don't consider following rules, \
    since 'continue' defaults to 'DontCont'
desc=Three B characters
action=write - three B characters were observed

type=Single
ptype=SubStr
pattern=CCC
rem=after this rule has matched, continue from next rule
continue=TakeNext
desc=Three C characters
action=write - three C characters were observed

label=lastRule

type=Single
ptype=SubStr
pattern=DDD
desc=Three D characters
action=write - three D characters were observed

For the input line "AAABBBCCCDDD", this ruleset writes the strings "three A characters were observed" and "three D characters were observed" to standard output. If the input line is "BBBCCCDDD", the string "three B characters were observed" is written to standard output. For the input line "CCCDDD", the strings "three C characters were observed" and "three D characters were observed" are sent to standard output, while the input line "DDD" produces the output string "three D characters were observed". Iconic behaviour honestly πŸ˜­πŸ’–

If there are two or more configuration files, the rule sequence from every file is matched against input (unless explicitly specified otherwise). For example, suppose SEC is started with the command line

/usr/bin/sec --input=- \
             --conf=/etc/sec/sec1.rules --conf=/etc/sec/sec2.rules

and the configuration file /etc/sec/sec1.rules has the following content:

type=Single
ptype=SubStr
pattern=AAA
desc=Three A characters
action=write - three A characters were observed

type=Single
ptype=SubStr
pattern=BBB
continue=EndMatch
desc=Three B characters
action=write - three B characters were observed

Also, suppose the configuration file /etc/sec/sec2.rules has the following content:

type=Single
ptype=SubStr
pattern=CCC
desc=Three C characters
action=write - three C characters were observed

If SEC receives the line "AAABBBCCC" from standard input, rules from both configuration files are tried, and as a result, the strings "three A characters were observed" and "three C characters were observed" are written to standard output. Note that rules from /etc/sec/sec1.rules are tried first against the input line, since the option --conf=/etc/sec/sec1.rules is given before --conf=/etc/sec/sec2.rules in the SEC command line (see also INPUT PROCESSING AND TIMING for a more detailed discussion). If SEC receives the line "BBBCCC" from standard input, the second rule from /etc/sec/sec1.rules produces a match, and the string "three B characters were observed" is written to standard output. Since the rule contains a continue=EndMatch statement, the search for matching rules ends for all configuration files, and rules from /etc/sec/sec2.rules will not be not tried. Without this statement, the search for matching rules would continue in /etc/sec/sec2.rules, and the first rule would write the string "three C characters were observed" to standard output.


πŸ¦‹ PATTERNS, PATTERN TYPES AND MATCH VARIABLES πŸ¦‹

Patterns and pattern types are defined with pattern* and ptype* rule fields. Many pattern types define the number of lines N which the pattern matches (if N is omitted, 1 is assumed). If N is greater than 1, the scope of matching is set with the --jointbuf and --nojointbuf options. With --jointbuf, the pattern is used for matching N last input lines taken from the joint input buffer (and those lines can come from different input sources πŸ’«). With --nojointbuf, the source of the last input line is identified, and the pattern is matched with N last input lines from the input buffer of that identified source.

SubStr[N] 🧡

  • pattern is a string that gets searched in the last N input lines L1, L2, ..., LN. If N is greater than 1, those input lines get joined into one string β€” "L1<NEWLINE>L2<NEWLINE>...<NEWLINE>LN" β€” and the pattern string is searched from that. If the pattern string is found in the input line(s), the pattern matches. πŸ’…
  • Backslash sequences \t, \n, \r, \s, and \0 can be used in the pattern to mean tabulation, newline, carriage return, space character, and empty string respectively, while \\ means backslash itself. Bestie, they're your little escape besties 🫢
  • Example pattern definition, for the visual learners:
ptype=substr
pattern=Backup done:\tsuccess
  • The pattern matches lines containing "Backup done:<TAB>success". ✨
Heads up: since the SubStr[N] pattern type was designed for fast matching, it does not support match variables. No $1 for you here, babe 😭

RegExp[N] 🌸

  • pattern is a Perl regular expression (peep perlre(1) for more info) for matching the last N input lines L1, L2, ..., LN. If N is greater than 1, the input lines get joined into "L1<NEWLINE>L2<NEWLINE>...<NEWLINE>LN" and the regular expression is matched against that string. If the regular expression matches, match variables get set, and you can use those match variables in other parts of the rule definition. πŸ‘‘

On top of numbered match variables ($1, $2, etc.), SEC supports named match variables $+{name} and the $0 variable. The $0 variable holds the entire string of the last N input lines that the regular expression matched. Named match variables can be created in newer versions of the Perl regular expression language β€” like, (?<myvar>AB|CD) sets $+{myvar} to AB or CD. Also, SEC creates special named match variables $+{_inputsrc} and $+{_intcontext}. The $+{_inputsrc} variable holds the input file name(s) where the matching line(s) came from. The $+{_intcontext} variable holds the name of the current internal context (go see the INTERNAL EVENTS AND CONTEXTS section for the tea β˜•). If no internal context has been set up for the current input source, that variable is set to the Perl undefined value.

For example, this pattern matches the SSH "Connection from" event, and sets $0 to the entire event line, both $1 and $+{ip} to the IP address of the remote node, and $2 to the port number at the remote node:

ptype=RegExp
pattern=sshd\[\d+\]: Connection from (?<ip>[\d.]+) port (\d+)

If the matching event comes from input file /var/log/messages with internal context MSGS, the $+{_inputsrc} and $+{_intcontext} variables are set to the strings "/var/log/messages" and "MSGS", respectively. πŸ’–

Also, SEC allows for match caching and for the creation of additional named match variables through variable maps, which are defined with the varmap* fields. A variable map is a list of name=number mappings separated by semicolons, where name is the name for the named variable and number identifies a numbered match variable that the regular expression set. Each name must begin with a letter and consist of letters, digits and underscores. After the regular expression has matched, named variables specified by the map are created from the corresponding numbered variables. If the same named variable gets set up both from the regular expression and the variable map, the map takes precedence. periodt.

If name is not followed by the equal sign and number in the varmap* field, it's regarded as a common name for all match variables and their values from a successful match. That name is used for caching a successful match by the pattern β€” match variables and their values get stored in the memory-based pattern match cache under name. Cached match results can be reused by Cached and NCached patterns. Note that before processing each new input line, the previous content of the pattern match cache is cleared. Also note that a successful pattern match is cached even if the subsequent context expression evaluation yields FALSE (see the INPUT PROCESSING AND TIMING section for more). 🧚

For example, consider this pattern definition:

ptype=regexp
pattern=(?i)(\S+\.mydomain).*printer: toner\/ink low
varmap=printer_toner_or_ink_low; message=0; hostname=1

The pattern matches "printer: toner/ink low" messages in a case insensitive manner from printers belonging to .mydomain. Note that the printer hostname is assigned to $1 and $+{hostname}, while the whole message line is assigned to $0 and $+{message}. If the message comes from file /var/log/test which doesn't have an internal context defined, the $+{_inputsrc} variable is set to the string "/var/log/test", while $+{_intcontext} is set to the Perl undefined value. Also, those variables and their values get stored to the pattern match cache under the name "printer_toner_or_ink_low". πŸ’…

The following pattern definition produces a match if the last two input lines are AAA and BBB:

ptype=regexp2
pattern=^AAA\nBBB$
varmap=aaa_bbb

Note that with the --nojointbuf option, the pattern only matches if the matching lines are coming from the same input file, while the --jointbuf option lifts that restriction. 🌈

In the case of a match, $0 is set to "AAA<NEWLINE>BBB", $+{_inputsrc} to the file name(s) for the matching lines, and $+{_intcontext} to the name of the current internal context. Also, these variable-value pairs are cached under the name "aaa_bbb". ✨


PerlFunc[N] πŸ“

  • pattern is a Perl function for matching the last N input lines L1, L2, ..., LN. The Perl function gets compiled at SEC startup with the Perl eval() function, and eval() must return a code reference for the pattern to be valid (see also the PERL INTEGRATION section). The function is called in Perl list context, and with the --jointbuf option, lines L1, L2, ..., LN and the names of the corresponding input files F1, F2, ..., FN are passed to the function as parameters:
function(L1, L2, ..., LN, F1, F2, ..., FN)

Note that with the --nojointbuf option, the function is called with a single file name parameter F, since lines L1, ..., LN are coming from the same input file:

function(L1, L2, ..., LN, F)

Also note that if the input line is a synthetic event, the input file name is the Perl undefined value. πŸ™ˆ

If the function returns several values, or a single value that's true in Perl boolean context, the pattern matches. If the function returns no values, or a single value that's false in Perl boolean context (0, empty string or undefined value), the pattern does not match. If the pattern matches, the return values get assigned to numbered match variables ($1, $2, etc.). Like with RegExp patterns, the $0 variable is set to the matching input line(s), the $+{_inputsrc} variable is set to the input file name(s), the $+{_intcontext} variable is set to the name of the current internal context, and named match variables can be created from variable maps. πŸ’œ

For example, consider this pattern definition:

ptype=perlfunc2
pattern=sub { return ($_[0] cmp $_[1]); }

The pattern compares the last two input lines in a stringwise manner ($_[1] holds the last line and $_[0] the preceding one), and matches if the lines are different. Note that the result of the comparison is assigned to $1, while the two matching lines are concatenated (with the newline character between them) and assigned to $0. If the matching lines come from input file /var/log/mylog with internal context TEST, the $+{_inputsrc} and $+{_intcontext} variables are set to the strings "/var/log/mylog" and "TEST", respectively. πŸ¦‹

The following pattern produces a match for any line, and sets the $1, $2 and $3 variables to the strings "abc", "def" and "ghi", respectively (also, $0 is set to the whole input line, $+{_inputsrc} to the input file name, and $+{_intcontext} to the name of the internal context associated with input file $+{_inputsrc}):

ptype=perlfunc
pattern=sub { return ("abc", "def", "ghi"); }

The following pattern definition produces a match if the input line is not a synthetic event and contains either the string "abc" or "def". The $0 variable is set to the matching line. If the matching line comes from /var/log/test without an internal context, $+{_intcontext} is set to the Perl undefined value, while $1, $+{file} and $+{_inputsrc} are set to the string "/var/log/test":

ptype=perlfunc
pattern=sub { if (defined($_[1]) && $_[0] =~ /abc|def/) \
{ return $_[1]; } return 0; }
varmap= file=1

Finally, if a function pattern returns a single value which is a reference to a Perl hash, named match variables get created from the key-value pairs in the hash. For example, the following pattern matches a line if it contains either the string "three" or "four". Apart from setting $0, $+{_inputsrc} and $+{_intcontext}, the pattern also creates the match variables $+{three} and $+{four}, and sets them to 3 and 4, respectively:

ptype=perlfunc
pattern=sub { my(%hash); \
if ($_[0] !~ /three|four/) { return 0; } \
$hash{"three"} = 3; $hash{"four"} = 4; return \%hash; }

Cached πŸŽ€

pattern is a name that is searched in the pattern match cache (entries are stored into the cache with the varmap* fields). If an entry with the given name is found in the cache, the pattern matches, and the match variables and values are retrieved from the cache. For example, if the input line matches the following pattern

ptype=perlfunc
pattern=sub { if (defined($_[1]) && $_[0] =~ /abc|def/) \
{ return $_[1]; } return 0; }
varmap=abc_or_def_found; file=1

then the entry "abc_or_def_found" is created in the pattern match cache. Therefore, the pattern

ptype=cached
pattern=abc_or_def_found

...will also produce a match for this input line, and set the $0, $1, $+{file}, $+{_inputsrc}, and $+{_intcontext} variables to values from the previous match. yes bestie it's literally recycling the old receipts πŸ’–


πŸŽ€ Pattern types, part two (the negated girlies + the truth-teller)

NSubStr[N]

Like SubStr[N], except the result of the match is negated. flip it, reverse it πŸ™ƒ Heads up though: this pattern type does not support match variables. None. Zero. Nada.

NRegExp[N]

Like RegExp[N], except the result of the match is negated and variable maps are not supported. And just so we're clear β€” the only match variables this pattern type supports are $0, $+{_inputsrc}, and $+{_intcontext}. That's the whole squad. πŸ’…

NPerlFunc[N]

Like PerlFunc[N], except the result of the match is negated and variable maps are not supported. Same deal as above: the only match variables supported are $0, $+{_inputsrc}, and $+{_intcontext}. 🫢

NCached

Like Cached, except the result of the match is negated. And once again: this pattern type does not support match variables. the pattern types that don't do variables are all hanging out together, it's giving clique 😭

TValue

Okay so this one is a truth value, with TRUE and FALSE being legitimate values. TRUE always matches an input line, while FALSE never matches anything. Periodt. Note that this pattern type does not support match variables. iconic minimalism honestly ⭐


πŸ’Έ Match variable substitution, the fine print (but make it cute)

When match variables are substituted, each $$ sequence is interpreted as a literal dollar sign ($) β€” which is how you mask match variables. For example, the string "Received $$1" becomes "Received $1" after substitution, while "Received $$$1" becomes "Received $<value_of_1st_var>". sneaky little dollar sign πŸ’€

In order to disambiguate numbered match variables from the following text, the variable number must be enclosed in braces. For example, the string "Received ${1}0" becomes "Received <value_of_1st_var>0" after substitution, while the string "Received $10" would become "Received <value_of_10th_var>". braces are the boundaries bestie, set them πŸŽ€

If the match variable was not set by the pattern, it is substituted with an empty string (i.e., a zero-width string). So the string "Received $10!" becomes "Received !" after substitution if the pattern did not set $10. (Note that prior to SEC-2.6, unset variables were not substituted.) throwback lore πŸ¦‹

In the current version of SEC, names of $+{name} match variables must comply with the following naming convention β€” the first character can be a letter or underscore, while remaining characters can be letters, digits, underscores and exclamation marks (!). However, when setting named match variables from a pattern, it is recommended to begin the variable name with a letter, since names of special automatically created variables begin with an underscore (e.g., $+{_inputsrc}). underscore names are reserved, don't be out here claiming them πŸ‘‘


πŸ—„οΈ Referring back to cached variables

After the pattern has matched an event and match variables have been set, it is also possible to refer to previously cached match variables with the syntax $:{entryname:varname}, where entryname is the name of the pattern match cache entry, and varname is the name of the variable stored under the entry. For example, if the variable $+{ip} has been previously cached under the entry "SSH", it can be referred as $:{SSH:ip}. receipts from the archive, immaculate 🧾

For the reasons of efficiency, the $:{entryname:varname} syntax is not supported for fast pattern types which do not set match variables (i.e., SubStr, NSubStr, NCached and TValue). sorry fast girls, no archive access for you 😭


πŸ‘― Pair / PairWithWindow shadowing (the two-pattern situation)

Note that since Pair and PairWithWindow rules have two patterns, match variables of the first pattern are shadowed for some rule fields when the second pattern matches and sets variables. In order to refer to shadowed variables, their names must begin with % instead of $ (e.g., %1 refers to match variable $1 set by the first pattern). the % is like the girl from the first era still showing up in the group chat πŸ’…

However, the use of the %-prefix is only valid under the following circumstances β€” both pattern types support match variables and in the given rule field match variables from both patterns can be used. both conditions or it doesn't count, no exceptions

The %-prefixed match variables are masked with the "%%" sequence (like regular match variables with "$$"). Similarly, the braces can be used for disambiguating the %-prefixed variables from the following text. πŸŽ€

Finally, note that the second pattern of Pair and PairWithWindow rules may contain match variables if the second pattern is of type SubStr, NSubStr, Regexp, or NRegExp. The variables are substituted at runtime with the values set by the first pattern. If the pattern is a regular expression, all special characters inside substituted values are masked with the Perl quotemeta() function and the final expression is checked for correctness. safety check, we love a validation moment βœ…


🌈 CONTEXTS AND CONTEXT EXPRESSIONS 🌈

A SEC context is a memory based entity which has one or more names, a lifetime, and an event store. Also, an action list can be set up for a context which is executed immediately before the context expires. it's like a little group chat with a timer, and a final message that sends right before it's archived πŸ’–

For example, the action

create MYCONTEXT 3600 (report MYCONTEXT /bin/mail root@localhost)

creates the context MYCONTEXT which has a lifetime of 3600 seconds and empty event store. Also, immediately before MYCONTEXT expires and is dropped from memory, the action report MYCONTEXT /bin/mail root@localhost is executed which mails the event store of MYCONTEXT to root@localhost. it literally mails you the receipts on the way out, obsessed πŸ“¬

Contexts can be used for event aggregation and reporting. Suppose the following actions are executed in this order:

create MYCONTEXT
add MYCONTEXT This is a test
alias MYCONTEXT MYALIAS
add MYALIAS This is another test
report MYCONTEXT /bin/mail root@localhost
delete MYALIAS

The first action creates the context MYCONTEXT with infinite lifetime and empty event store. The second action appends the string "This is a test" to the event store of MYCONTEXT. The third action sets up an alias name MYALIAS for the context (names MYCONTEXT and MYALIAS refer to the same context data structure). The fourth action appends the string "This is another test" to the event store of the context. The fifth action writes the lines

This is a test
This is another test

to the standard input of the /bin/mail root@localhost command. The sixth action deletes the context data structure from memory and drops its names MYCONTEXT and MYALIAS. one girl, two names, two strings, zero confusion πŸ’…

Since contexts are accessible from all rules and event correlation operations, they can be used for data sharing and joining several rules into one event correlation scheme. In order to check for the presence of contexts from rules, context expressions can be employed. shared state, the group project that actually works ✨


🧠 Context expressions (the vibe check for contexts)

Context expressions are boolean expressions that are defined with the context* rule fields. Context expressions can be used for restricting the matches produced by patterns, since if the expression evaluates FALSE, the rule will not match an input event. no context, no match, sorry babe πŸ™ˆ

The context expression accepts context names, Perl miniprograms, Perl functions, and pattern match cache lookups as operands. These operands can be combined with the following operators:

  • ! β€” logical NOT
  • && β€” short-circuit logical AND
  • || β€” short-circuit logical OR

In addition, parentheses can be used for grouping purposes. math class but make it event correlation πŸŽ€

If the operand does not contain any special operators (such as -> or :>, see below), it is treated as a context name. Context name operands may contain match variables, but may not contain whitespace. If the context name refers to an existing context, the operand evaluates TRUE, otherwise it evaluates FALSE. exists = true, doesn't = false, the simplest possible vibe check βœ…

For example, consider the following rule sequence:

type=Single
ptype=RegExp
pattern=Test: (\d+)
desc=test
action=create CONT_$1

type=Single
ptype=RegExp
pattern=Test2: (\d+) (\d+)
context=CONT_$1 && CONT_$2
desc=test
action=write - Both $1 and $2 have been seen in the past

If the following input lines appear in this order

Test: 19
Test: 261
Test2: 19 787
Test: 787
Test2: 787 261

the first input line matches the first rule which creates the context CONT_19, and similarly, the second input line triggers the creation of the context CONT_261. The third input line "Test2: 19 787" matches the regular expression

Test2: (\d+) (\d+)

but does not match the second rule, since the boolean expression

CONT_19 && CONT_787

evaluates FALSE (context CONT_19 exists, but context CONT_787 doesn't). one of them didn't show up to the function, it happens 😭

The fourth input line matches the first rule which creates the context CONT_787. The fifth input line "Test2: 787 261" matches the second rule, since the boolean expression

CONT_787 && CONT_261

evaluates TRUE (both context CONT_787 and context CONT_261 exist), and therefore the string "Both 787 and 261 have been seen in the past" is written to standard output. both girls present, the rule ate and left no crumbs πŸ”₯


➑️ The arrow operator -> (Perl function time)

If the context expression operand contains the arrow operator (->), the text following the arrow must be a valid Perl function definition that is compiled at SEC startup with the Perl eval() function. The eval() must return a code reference (see also PERL INTEGRATION section for more information). cross-reference moment, iykyk πŸ“–

If any text precedes the arrow, it is treated as a list of parameters for the function. Parameters must be separated by whitespace and may contain match variables. In order to evaluate the context expression operand, the Perl function is called in the Perl scalar context. If the return value of the function is true in the Perl boolean context, the operand evaluates TRUE, otherwise it evaluates FALSE. feed it args, it gives you a truth value, that's the whole transaction πŸ’…

For example, the following rule matches an SSH login failure event if the login attempt comes from a privileged port of the client host:

type=Single
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port (\d+) ssh2
context=$2 -> ( sub { $_[0] < 1024 } )
desc=SSH login failure for $1 priv port $2
action=write - SSH login failure for user $1 from a privileged port $2

When the following message from SSH daemon appears

Dec 16 16:24:59 myserver sshd[13685]: Failed password for risto from
10.12.2.5 port 41063 ssh2

the regular expression of the rule matches this message, and the value of the $2 match variable (41063) is passed to the Perl function

sub { $_[0] < 1024 }

This function returns true if its input parameter is less than 1024 and false otherwise, and therefore the above message will not match the rule. port 41063 said "not today", rejected πŸ™ˆ

However, the following message

Dec 16 16:25:17 myserver sshd[13689]: Failed password for risto from
10.12.2.5 port 1023 ssh2

matches the rule, and the string "SSH login failure for user risto from a privileged port 1023" is written to standard output. 1023 is under 1024, it passed the vibe check, slay πŸ”₯

As another example, the following context expression evaluates TRUE if the /var/log/messages file does not exist or was last modified more than 1 hour ago (note that the Perl function takes no parameters):

context= -> ( sub { my(@stat) = stat("/var/log/messages"); \
return (!scalar(@stat) || time() - $stat[9] > 3600); } )

no params, just vibes and a stat() call πŸ’œ

If your context expression operand has a :> operator in it, bestie, listen up πŸ‘‚ β€” everything after the :> must be a valid Perl function definition, and it gets compiled at SEC startup with the Perl eval() function. That eval() has to hand back a code reference (peep the PERL INTEGRATION section for the deets). If there's any text before the :>, that's treated as a list of parameters for the function. Parameters get separated by whitespace and can contain match variables. Each parameter is assumed to be the name of an entry in the pattern match cache. If no entry with that name exists, Perl's undefined value gets passed to the function πŸ«₯. If the entry does exist, a reference to the entry gets passed to the Perl function.

Internally, every pattern match cache entry is implemented as a Perl hash holding all the match variables for that entry. In that hash, each key-value pair is a variable name and its value β€” so if cached match variable $+{ip} is holding 10.1.1.1, the hash contains the value 10.1.1.1 under the key ip. ✨ To evaluate the context expression operand, the Perl function gets called in Perl scalar context. If the function's return value is true in Perl boolean context, the operand evaluates TRUE; otherwise it evaluates FALSE. Simple as πŸ’…

For example, consider this rule sequence:

type=Single
ptype=RegExp
pattern=sshd\[\d+\]: (?<status>Accepted|Failed) .+ \
for (?<invuser>invalid user )?(?<user>\S+) from (?<ip>[\d.]+) \
port (?<port>\d+) ssh2
varmap=SSH
continue=TakeNext
desc=parse SSH login events and pass them to following rules
action=none

type=Single
ptype=Cached
pattern=SSH
context=SSH :> ( sub { $_[0]->{"status"} eq "Failed" && \
                       $_[0]->{"port"} < 1024 && \
                       defined($_[0]->{"invuser"}) } )
desc=Probe of invalid user $+{user} from privileged port of $+{ip}
action=pipe '%t: %s' /bin/mail -s 'SSH alert' root@localhost

The first rule matches and parses SSH login messages, and stores the parsing results in the pattern match cache under the name SSH. The pattern of the second rule (defined with ptype=Cached and pattern=SSH) matches any input event for which the entry SSH has previously been created in the pattern match cache β€” in other words, the event has been recognized and parsed as an SSH login message πŸŽ€. For each matching event, the second rule passes the reference to the SSH cache entry to the Perl function

sub { $_[0]->{"status"} eq "Failed" && \
      $_[0]->{"port"} < 1024 && \
      defined($_[0]->{"invuser"}) }

The function checks the values of the $+{status}, $+{port}, and $+{invuser} match variables under the SSH entry, and returns true if $+{status} equals the string "Failed" (i.e., the login attempt failed), the value of $+{port} is less than 1024, and $+{invuser} holds a defined value (i.e., the user account doesn't exist). If the function (and therefore the context expression) evaluates TRUE, the rule sends a warning e-mail to root@localhost that a non-existing user account was probed from a privileged port of a client host. Iconic detective work, honestly πŸ”

If the context expression operand begins with the varset keyword, the following string is treated as the name of an entry in the pattern match cache. The operand evaluates TRUE if that entry exists, and FALSE otherwise.

So for example, this context expression definition evaluates TRUE if the pattern match cache entry SSH exists and, under that entry, the value of the match variable $+{user} equals the string "risto":

context=varset SSH && SSH :> ( sub { $_[0]->{"user"} eq "risto" } )

If the context expression operand begins with an equal sign (=), the text that follows must be a Perl miniprogram that's a valid parameter for the Perl eval() function. The miniprogram may contain match variables. To evaluate the Perl miniprogram operand, it gets compiled and executed by calling the Perl eval() function in Perl scalar context (again, check the PERL INTEGRATION section). If the return value from eval() is true in Perl boolean context, the operand evaluates TRUE; otherwise it evaluates FALSE. But heads up bestie β€” unlike the Perl functions of the -> and :> operators, which get compiled once at SEC startup, Perl miniprograms get compiled before every single execution, so evaluating them is considerably more expensive. πŸ’Έ

For example, this context expression evaluates TRUE when neither the context C1 nor the context C2 exists and the value of the $1 variable equals the string "myhost.mydomain":

context=!(C1 || C2) && =("$1" eq "myhost.mydomain")

And since && is a short-circuiting operator, the Perl code

"$1" eq "myhost.mydomain"

is not evaluated if either C1 or C2 exists. Efficient queen πŸ‘‘

Now, a lil PSA: since Perl functions and miniprograms can contain strings that clash with context expression operators (like '!'), it's recommended to wrap them in parentheses, like so:

context=$1 $2 -> ( sub { $_[0] != $_[1] } )

context= =({my($temp) = 0; !$temp;})

Also, if function parameter lists contain strings like that, they should be wrapped in parentheses in a similar way:

context=($1! $2) -> ( sub { $_[0] eq $_[1] } )

If the whole context expression is enclosed in square brackets [], like [MYCONTEXT1 && !MYCONTEXT2], then SEC evaluates the expression before pattern matching (normally, the pattern gets matched against the input line(s) first, so that match variables are initialized and substituted before the expression is evaluated). But β€” and this is the serve β€” if the expression contains no match variables, and lots of input events are known to match the pattern but not the expression, the []-operator could save you a substantial amount of CPU time. πŸ”₯


🌈 ACTIONS, ACTION LISTS AND ACTION LIST VARIABLES 🌈

Action lists get defined with the action* rule fields. An action list is made of action definitions separated by semicolons. Each action definition kicks off with a keyword that says what type of action it is. Depending on the action type, parameters may follow, and non-constant parameters may contain match variables. For instance β€” say the $1 and $2 match variables hold the values "test1" and "the second test" β€” then the action create MYCONT_$1 60 creates the context MYCONT_test1 with a lifetime of 60 seconds, while the action write - The names of tests: $1, $2 writes the string "The names of tests: test1, the second test" to standard output. πŸ¦‹

Apart from a few exceptions that get explicitly noted, match variables are substituted at the earliest possible opportunity in action lists. For example, take this rule definition:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m
action=pipe 'Three SSH login failures, first user is $1' \
            /bin/mail -s 'SSH login alert' root@localhost
window=60
thresh=3

When this rule matches an SSH login failure event that starts an event correlation operation, the operation substitutes the $1 match variable in the action list definition with the user name from the matching event, and user names from any further events processed by this same event correlation operation are not considered for $1. So if these events come through

Dec 16 16:24:52 myserver sshd[13671]: Failed password for root from
10.12.2.5 port 29736 ssh2
Dec 16 16:24:59 myserver sshd[13685]: Failed password for risto from
10.12.2.5 port 41063 ssh2
Dec 16 16:25:01 myserver sshd[13689]: Failed password for oracle from
10.12.2.5 port 11204 ssh2

then all the events get processed by the same operation, and the message "Three SSH login failures, first user is root" gets mailed to root@localhost. πŸ“¬

In order to use semicolons inside a non-constant parameter, the parameter must be enclosed in parentheses (the outermost set of parentheses gets removed by SEC during configuration file parsing). For example, this action list is made of the delete and shellcmd actions:

action=delete MYCONTEXT; shellcmd (rm /tmp/sec1.tmp; rm /tmp/sec2.tmp)

The delete action deletes the context MYCONTEXT, while the shellcmd action executes the command line rm /tmp/sec1.tmp; rm /tmp/sec2.tmp. Since the command line contains a semicolon, it's been wrapped in parentheses β€” because otherwise that semicolon would've been mistakenly read as a separator between two actions. πŸ™ˆ


Apart from match variables, SEC supports action list variables in action lists, which make data sharing between actions and Perl integration way easier. Every action list variable has a name that must begin with a letter and consist of letters, digits, and underscores. Names of built-in variables usually start with a dot character (.), so they can be told apart from user-defined variables. To refer to an action list variable, you prefix its name with a percent sign (%). Unlike match variables, action list variables can only be used in action lists, and they get substituted with their values immediately before the action list executes. Also β€” and this is the glow-up β€” action list variables keep existing after the current action list has been executed, so they can be used in action lists of other rules. πŸ’–

These action list variables are predefined by SEC:

  • %s β€” operation description string (the value of the desc field after match variables have been substituted with their values). Note that for the action2 field of Pair and PairWithWindow rules, the %s variable gets set by evaluating the desc2 field of the rule.
  • %t β€” the time in human-readable format, as returned by the Perl localtime() function in Perl scalar context (e.g., Fri Feb 19 17:54:18 2016).
  • %u β€” the time in seconds since Epoch, as returned by the time(2) system call.
  • %.sec β€” number of seconds after the minute, in the range 00-59 (the value consists of two digits and is zero padded on the left).
  • %.min β€” number of minutes after the hour, in the range 00-59 (the value consists of two digits and is zero padded on the left).
  • %.hour β€” number of hours past midnight, in the range 00-23 (the value consists of two digits and is zero padded on the left).
  • %.hmsstr β€” the time in HH:MM:SS format (hours, minutes and seconds separated by colons, e.g., 09:32:04 or 18:06:02).
  • %.mday β€” day of the month, in the range 01-31 (the value consists of two digits and is zero padded on the left).
  • %.mdaystr β€” day of the month as a string (the value consists of two characters and is space padded on the left, e.g., " 1", " 4", " 9", or "25").
  • %.mon β€” month, in the range 01-12 (the value consists of two digits and is zero padded on the left).
  • %.monstr β€” the abbreviated name of the month, according to your current locale, exactly like the %b spec of the strftime(3) library call gives it (so Jan, May, or Sep) πŸ—“οΈ
  • %.year β€” the year (like 1998 or 2016) ✨
  • %.wday β€” day of the week, in the range 0-6 (0 = Sunday) πŸ’«
  • %.wdaystr β€” abbreviated name of the day of the week per the current locale, straight from the %a spec of strftime(3) (like Mon, Wed, or Sat)
  • %.tzname β€” name of the timezone according to the current locale, as returned by the %Z specification of the strftime(3) library call (like UTC or EET) 🌍
  • %.tzoff β€” timezone offset from UTC, as returned by the %z specification of the strftime(3) library call (like -0500 or +0200)
  • %.tzoff2 β€” timezone offset from UTC in +hh:mm/-hh:mm format (like -05:00 or +02:00), and this only works if the %z spec of the strftime(3) call returns the value in +hhmm/-hhmm format β€” if the value doesn't follow that format, %.tzoff2 is just an empty string, no cap 😭
  • %.nl β€” newline character
  • %.cr β€” carriage return character
  • %.tab β€” tabulation character
  • %.sp β€” space character
  • %.chr0, ..., %.chr255 β€” ASCII 0..255 characters (like %.chr7 is bell and %.chr12 is the form feed character, whereas the sequence %.chr195%.chr132 is the A-umlaut character in UTF-8 encoding) πŸ”₯

ok so for example, this action list below grabs the current time in human-readable format, glues the string "This is a test event" onto it, shoves the whole thing into the %text action list variable, and then mails the value of %text to root@localhost:

action=assign %text %t: This is a test event; \
       pipe '%text' /bin/mail root@localhost

If this action list runs at Nov 19 10:58:51 2015, the assign action sets the %text action list variable to the string "Thu Nov 19 10:58:51 2015: This is a test event", and then the pipe action mails that string to root@localhost. And here's the tea bestie β€” unlike match variables, action list variables have global scope, so if you access %text from the action lists of other rules, you'll get that same string "Thu Nov 19 10:58:51 2015: This is a test event" back (at least until something assigns a new value to %text) πŸ‘‘

To disambiguate the variable from whatever text comes right after it, you gotta wrap the variable name in braces. Like, this action:

action=write - %{.year}-%{.mon}-%{.mday}T%{.hmsstr}%{.tzoff2}

writes a timestamp in ISO 8601 format to standard output, e.g. 2016-02-24T07:34:01+02:00 (and yes, if you swapped %{.mday} for %.mday in that action you'd accidentally be referencing a %.mdayT variable, which is not the vibe πŸ™ˆ).

When action list variables get substituted with their values, every sequence "%%" is read as a literal percent sign (%), which is how you mask the variables. So for example, the string "s%%t" becomes "s%t" after substitution β€” not "s%<timestamp>". Periodt.

But heads up: if %-prefixed match variables are supported for the action2 field of the Pair or PairWithWindow rule, then you have to use the sequence "%%%" in action2 to mask a variable, because that string goes through two variable substitution rounds (first for the %-prefixed match variables, then for the action list variables β€” so like, "s%%%t" first becomes "s%%t" and finally "s%t") πŸŒ€

And whenever a rule field goes through several substitution rounds, the $ or % characters are masked inside values substituted during earlier rounds, so you don't get unwanted side effects during later rounds. Slay.

If an action list variable has not been set, it gets substituted with an empty string (i.e. a zero-width string). So the string "Value of A is: %a" becomes "Value of A is: " after substitution if the variable %a is unset. (And just so you know β€” prior to SEC-2.6, unset variables were not substituted. Ancient history fr fr.)

Finally, values are substituted as strings, so values of other types (e.g. references) lose their original meaning, unless it's explicitly noted otherwise (like, if a Perl function reference is stored to an action list variable, that function can later be invoked through this variable with the call action β€” that's the exception, iykyk).


SEC supports the following actions (optional parameters are enclosed in square brackets, don't forget):

  • none β€” No action. Literally nothing. It's giving main character who stays silent πŸ’…
  • logonly [<string>] β€” the message <string> is logged to the destinations given with the --log and --syslog options. The level of the log message is set to 4 (peep the --debug option if you want the whole lore on log message levels). The default value for <string> is %s. For example, check out this action list definition:
action=logonly This is a test

The logonly action above logs the message "This is a test" with level 4.

  • write <filename> [<string>] β€” the string <string> plus a terminating newline gets written to the file <filename> (<filename> may not contain whitespace). The file can be a regular file, a named pipe, or standard output (which you denote with -). If it's a regular file, <string> is appended to the end of the file. If the file doesn't exist yet, it gets created as a regular file before the write happens. Note that the file will not be closed once the action completes, so following write actions will access an already open file. However, several signals cause the file to be closed and reopened, and for rotating files created with the write action, you can use the SIGUSR2 signal (see the SIGNALS section for more on that). The default value for <string> is %s. For example, consider this action list definition:
action=write /var/log/test.log %t $0

The write action above prepends a human-readable timestamp and a separating space character to the value of the $0 match variable, and the resulting string is appended to the file /var/log/test.log with a terminating newline.

  • writen <filename> [<string>] β€” same energy as the write action, except the string <string> is written without a terminating newline. Note that write and writen actions share the same filehandle for accessing the file. For example, consider this action list definition:
action=writen - ab; writen - c; writen - %.nl

The action list above writes the string "abc<NEWLINE>" to standard output, so it's identical to write - abc (and also identical to writen - abc%.nl). Same thing, different font ✨

  • closef <filename> β€” closes the file <filename> that was previously opened by the write or writen action (<filename> may not contain whitespace).
  • owritecl <filename> [<string>] β€” similar to the write action, except the file <filename> is opened and closed at each write. Also, the string <string> is written without a terminating newline. If the file has already been opened by a previous write action, owritecl does not use the existing filehandle β€” it opens and closes the file separately. For example, consider this action list definition:
action=owritecl /var/log/test-%{.year}%{.mon}%{.mday} $0%{.nl}

The owritecl action above appends the value of the $0 match variable with a terminating newline to the file /var/log/test-YYYYMMDD, where YYYYMMDD reflects the current date (so if the current date is April 1 2018, the file is /var/log/test-20180401). And since the file gets closed after every write, the old file will not be left open when the date changes. Clever, right? 🧠

  • udgram <filename> [<string>] β€” the string <string> is written to the UNIX datagram socket <filename> (<filename> may not contain whitespace). Note that the socket will not be closed once the action completes, and following udgram actions will access an already open socket. However, several signals cause the socket to be closed and reopened (see the SIGNALS section for more). The default value for <string> is %s. For example, consider this action list definition:
action=udgram /dev/log <30>%.monstr %.mdaystr %.hmsstr sec: This is a test

The udgram action above sends a syslog message to the local syslog daemon via the /dev/log socket, where the message priority is 30 (that corresponds to the "daemon" facility and "info" level), the syslog tag is "sec", and the message text is "This is a test". Note that the message substring "%.monstr %.mdaystr %.hmsstr" evaluates to a timestamp in BSD syslog format (like Mar 31 15:36:07).

  • closeudgr <filename> β€” closes the UNIX datagram socket <filename> that was previously opened by the udgram action (<filename> may not contain whitespace).
  • ustream <filename> [<string>] β€” the string <string> is written to the UNIX stream socket <filename> (<filename> may not contain whitespace). Note that the socket will not be closed once the action completes, and following ustream actions will access an already open socket. However, several signals cause the socket to be closed and reopened (see the SIGNALS section for more). The default value for <string> is %s.
  • closeustr <filename> β€” closes the UNIX stream socket <filename> that was previously opened by the ustream action (<filename> may not contain whitespace).
  • udpsock <host>:<port> [<string>] β€” the string <string> is sent to the UDP port <port> of the host <host>. Note that the UDP socket used for communication will not be closed once the action completes, and following udpsock actions for the same remote peer will use an already existing socket. However, several signals cause the socket to be closed and recreated (see the SIGNALS section for more). The default value for <string> is %s. For example, consider this action list definition:
action=udpsock mysrv:514 <13>%.monstr %.mdaystr %.hmsstr myhost test: $0

The udpsock action above sends a BSD syslog message to port 514/udp of the remote syslog server mysrv, where the message priority is 13 (that corresponds to the "user" facility and "notice" level), the name of the local host is "myhost", the syslog tag is "test", and the message text is the value of the $0 match variable. Sending the whole vibe over the network, iconic πŸ“‘

  • closeudp <host>:<port> β€” closes the UDP socket for the peer <host>:<port> that was previously opened by the udpsock action.
  • tcpsock <host>:<port> [<string>] β€” yeets your string straight into a TCP port πŸ’Œ

So <string> gets sent to TCP port <port> on the host <host>. How long SEC will try to establish a connection to the remote peer is totally up to the timeout value you gave with the --socket-timeout option. And if the connection establishment doesn't succeed immediately, tcpsock just buffers <string> in memory for later sending β€” no biggie, it's patient like that.

Heads up though: the relevant TCP socket does not get closed after <string> has been transmitted, so any following tcpsock actions for the same peer will reuse an already-existing socket. That said, several signals cause the socket to be closed and recreated (go peek at the SIGNALS section for the deets). Default value for <string> is %s.

Okay so picture this action list definition:

action=tcpsock grsrv:2003 ssh.login.failures %{num} %{u}%{.nl}

That tcpsock action sends the value of the action list variable %{num} to port 2003/tcp of the Graphite server grsrv, so the value gets recorded under the metric path ssh.login.failures. And not to be that girl but note that the %{u} action list variable evaluates to current time in seconds since Epoch, and it's what sets the timestamp for the recorded value. slay.

  • closetcp <host>:<port> β€” bye bye socket 🫢

Closes the TCP socket for peer <host>:<port> that was previously opened by the tcpsock action. closure, periodt.

  • shellcmd <cmdline> β€” fork a process, let the shell do its thing πŸ“

Forks a process for executing command line <cmdline>. If <cmdline> contains shell metacharacters, <cmdline> is parsed by shell. If the --quoting option was specified and <cmdline> contains %s variables, the value of %s is quoted with single quotes before substituting it into <cmdline>; and if the value of %s contains single quotes, they're masked with backslashes (e.g., abc is converted to 'abc' and aa'bb is converted to 'aa'\''bb'). For additional information, see the CHILD PROCESSES section.

Example time, bestie:

action=shellcmd (cat /tmp/report | mail root; rm -f /tmp/report); \
       logonly Report sent to user root

The shellcmd action of this action list executes the command line

cat /tmp/report | mail root; rm -f /tmp/report

and the logonly action logs the message "Report sent to user root". And since the command line contains a semicolon β€” which is what separates shellcmd and logonly actions β€” the command line is enclosed in parentheses. iconic behavior tbh.

  • spawn <cmdline> β€” every stdout line becomes a synthetic event ✨

Similar to the shellcmd action, except that each line from the standard output of <cmdline> becomes a synthetic event and will be treated like a line from input file (see the event action for more information). If the --intcontexts command line option is given, internal context _INTERNAL_EVENT is set up before each synthetic event is processed (see the INTERNAL EVENTS AND CONTEXTS section for more information).

For example, consider the following action list definition:

action=spawn (cat /tmp/events; rm -f /tmp/events)

The above spawn action will generate synthetic events from all lines in file /tmp/events and remove the file. Since the command line contains a semicolon which is used for separating actions, the command line is enclosed in parentheses.

  • cspawn <name> <cmdline> β€” same vibe, custom context name πŸ¦‹

Similar to the spawn action, except that if the --intcontexts command line option is given, internal context <name> is set up for each synthetic event.

  • cmdexec <cmdline> β€” no shell, just raw execvp energy πŸ”₯

Forks a process for executing command line <cmdline>. Unlike the shellcmd action, <cmdline> is not parsed by shell, but split into arguments by using whitespace as a separator, and passed to execvp(3) for execution. Note that splitting into arguments is done when the cmdexec action is loaded from the configuration file and parsed, not at runtime (e.g., if <cmdline> is /usr/local/bin/mytool $1 $2, the values of $1 and $2 variables are regarded as single arguments even if the values contain whitespace). For additional information, see the CHILD PROCESSES section.

Example:

action=cmdexec rm /tmp/report*

The above cmdexec action will remove the file /tmp/report* without treating * as a file pattern character that matches any string. no globbing, no games πŸ’…

  • spawnexec <cmdline> β€” cmdexec but make it synthetic events ⭐

Similar to the cmdexec action, except that each line from the standard output of <cmdline> becomes a synthetic event and will be treated like a line from input file (see the event action for more information). If the --intcontexts command line option is given, internal context _INTERNAL_EVENT is set up before each synthetic event is processed (see the INTERNAL EVENTS AND CONTEXTS section for more information).

  • cspawnexec <name> <cmdline> β€” the named-context remix πŸŽ€

Similar to the spawnexec action, except that if the --intcontexts command line option is given, internal context <name> is set up for each synthetic event.

  • pipe '[<string>]' [<cmdline>] β€” write a string into some poor process's stdin πŸ’§

Forks a process for executing command line <cmdline>. If <cmdline> contains shell metacharacters, <cmdline> is parsed by shell. The string <string> with a terminating newline is written to the standard input of <cmdline> (single quotes are used for disambiguating <string> from <cmdline>). If <string> contains semicolons, <string> must be enclosed in parentheses (e.g., pipe '($1;$2)' /bin/cat). Default value for <string> is %s. If <cmdline> is omitted, <string> is written to standard output. For additional information, see the CHILD PROCESSES section.

Example, bestie:

action=pipe 'Offending activities from host $1' /bin/mail root@localhost

The above pipe action writes the line "Offending activities from host <hostname>" to the standard input of the /bin/mail root@localhost command which sends this line to root@localhost via e-mail (<hostname> is the value of the $1 match variable).

  • pipeexec '[<string>]' [<cmdline>] β€” pipe, but execvp style, no shell involved 🌈

Similar to the pipe action, except <cmdline> is not parsed by shell, but split into arguments by using whitespace as a separator, and passed to execvp(3) for execution. Note that splitting into arguments is done when the pipeexec action is loaded from the configuration file and parsed, not at runtime (e.g., if <cmdline> is /usr/local/bin/mytool $1 $2, the values of $1 and $2 variables are regarded as single arguments even if the values contain whitespace).

Example:

action=pipeexec 'Offending activities from host $1' \
                /bin/mail -s SEC%{.sp}alert $2

The above pipeexec action writes the line "Offending activities from host <hostname>" to the standard input of the /bin/mail -s <subject> <user> command which sends this line to <user> via e-mail with subject <subject> (<hostname> is the value of the $1 match variable, while <user> is the value of the $2 match variable).

Note that since <subject> is defined as SEC%{.sp}alert and does not contain whitespace, it is treated as a single argument for the -s flag of the /bin/mail command. However, since <subject> contains the %.sp action list variable, the string "SEC alert" will be used for the e-mail subject at runtime. Also, if the value of the $2 match variable contains shell metacharacters, they will not be interpreted by the shell. protective queen πŸ›‘οΈ

  • create [<name> [<time> [<action list>] ] ] β€” birth a context, give it a lifetime πŸ‘‘

Create a context with the name <name>, lifetime of <time> seconds, and empty event store. The <name> parameter may not contain whitespace and defaults to %s. The <time> parameter must evaluate to an unsigned integer at runtime. Specifying 0 for <time> or omitting the value means infinite lifetime. If <action list> is given, it will be executed when the context expires. If <action list> contains several actions, the list must be enclosed in parentheses. In <action list>, the internal context name _THIS may be used for referring to the current context (see the INTERNAL EVENTS AND CONTEXTS section for a detailed discussion).

And if an already existing context is recreated with create, its remaining lifetime is set to <time> seconds, its action list is reinitialized, and its event store is emptied. fresh start, fr fr.

Example:

action=write /var/log/test.log $0; create TIMER 3600 \
       ( logonly Closing /var/log/test.log; closef /var/log/test.log )

The write action from the above action list appends the value of the $0 match variable to file /var/log/test.log, while the create action creates the context TIMER which will exist for 3600 seconds. Since this context is recreated at each write, the context can expire only if the action list has not been executed for more than 3600 seconds (i.e., the action list has last updated the file more than 1 hour ago). If that is the case, the action list

logonly Closing /var/log/test.log; closef /var/log/test.log

is executed which logs the message "Closing /var/log/test.log" with the logonly action and closes /var/log/test.log with the closef action. When the execution of this action list is complete, the TIMER context is deleted. clean exit, we love to see it πŸ’–

  • delete [<name>] β€” yeet that context into the void πŸ’€

Delete the context <name>. The <name> parameter may not contain whitespace and defaults to %s.

  • obsolete [<name>] β€” delete, but say your goodbyes first πŸ™ˆ

Similar to the delete action, except that the action list of the context <name> (if present) is executed before deletion.

set <name> <time> [<action list>]

Bestie, this one is the glow-up button for a context. It changes the settings for the context called name. The creation time of the context gets set to the current time, and its lifetime gets set to time seconds. If you hand over the action list parameter, the context action list becomes that action list β€” but if you don't, the context action list is not changed, it just stays as it was. πŸ’…

The name parameter may not contain whitespace and defaults to %s. The time parameter has to evaluate to an unsigned integer or a hyphen (-) at runtime. Specifying 0 for time means infinite lifetime (main character energy, never expiring ✨). If time equals to -, then the creation time and the lifetime of the context are not changed. If the action list contains several actions, the list must be enclosed in parentheses. Inside action list, the internal context name _THIS may be used for referring to the current context (see the INTERNAL EVENTS AND CONTEXTS section for the full tea).

For example, consider this action list definition:

action=set C_$1 30 ( logonly Context C_$1 has expired )

The above set action sets the context C_<suffix> to expire after 30 seconds with a log message about expiration (suffix is the value of the $1 match variable). πŸŽ€


alias <name> [<alias>]

Making a nickname for your context, basically. Create an alias name alias for the context name. After creation, both alias and name will point to the same context data structure, and can thus be used interchangeably for referring to the context. πŸ’–

The name and alias parameters may not contain whitespace, and alias defaults to %s. If the context name does not exist, the alias name is not created. And if the delete action is called for one of the context names, the context data structure is destroyed, and all context names (which are now pointers to unallocated memory 😭) cease to exist. Also note that when the context expires, its action list is executed only once, no matter how many names the context has. One glow, one party. πŸ¦‹


unalias [<alias>]

Drop an existing context name alias, so that it can no longer be used for referring to the given context. The alias parameter may not contain whitespace and defaults to %s. If the name alias is the last reference to the context, the unalias action is identical to delete. Last one out, lights off, periodt. πŸ’€


add <name> [<string>]

String string is appended to the end of the event store of the context name. The name parameter may not contain whitespace, and the string parameter defaults to %s. If the context name does not exist, the context is created with an infinite lifetime, empty action list and empty event store (as with create <name>) before adding the string to the event store. So it just makes itself comfy. πŸ›‹οΈ

If string is a multi-line string (i.e., it contains newlines), it is split into lines, and each line is appended to the event store separately. For example, consider this action list definition:

action=add EVENTS This is a test; add EVENTS This is a test2

After the execution of this action list, the last two strings in the event store of the EVENTS context are "This is a test" and "This is a test2" (in that order). Slay. ✨


prepend <name> [<string>]

Similar to the add action, except that the string string is prepended to the beginning of the event store of context name. Front-of-the-line privileges, fr fr. For example, consider this action list definition:

action=prepend EVENTS This is a test; prepend EVENTS This is a
test2

After the execution of this action list, the first two strings in the event store of the EVENTS context are "This is a test2" and "This is a test" (in that order). Note the flip β€” that's the whole point. πŸ‘‘


fill <name> [<string>]

Similar to the add action, except that the event store of the context name is emptied before string is added. It's giving "clear the group chat, start a new topic". 🧹✨


report <name> [<cmdline>]

Okay so this one actually spawns a whole process, bestie. Fork a process for executing command line cmdline. If cmdline contains shell metacharacters, cmdline is parsed by shell. Also, write strings from the event store of the context name to the standard input of cmdline. Strings are written in the order they appear in the event store, with a terminating newline appended to each string.

If the context name does not exist or its event store is empty, cmdline is not executed. The name parameter may not contain whitespace, and if cmdline is omitted, strings are written to standard output. For additional info, see the CHILD PROCESSES section. For example, consider this action list definition:

action=create PID_$1 60 ( report PID_$1 /bin/mail root@localhost
); \
       add PID_$1 Beginning of the report

The above action list creates the context PID_<suffix> with the lifetime of 60 seconds and sets the first string in the context event store to "Beginning of the report" (suffix is the value of the $1 match variable). When the context expires, all strings from the event store will be mailed to root@localhost. Iconic. πŸ’Œ


reportexec <name> [<cmdline>]

Similar to the report action, except cmdline is not parsed by shell, but split into arguments by using whitespace as a separator, and passed to execvp(3) for execution. 🫢

Note that splitting into arguments is done when the reportexec action is loaded from the configuration file and parsed, not at runtime (e.g., if cmdline is /usr/local/bin/mytool $1 $2, the values of $1 and $2 variables are regarded as single arguments even if the values contain whitespace).

copy <name> %<var>

Strings s1,...,sn from the event store of the context name are joined into a multi-line string "s1<NEWLINE>...<NEWLINE>sn", and this string is assigned to the action list variable %<var>. If the context name does not exist, the value of %<var> does not change. No context, no touchy. πŸ™ˆ


empty <name> [%<var>]

Similar to the copy action, except that the event store of the context name will be emptied after the assignment. If %<var> is omitted, the content of the event store is dropped without an assignment. Poof. πŸ’¨


pop <name> %<var>

Remove the last string from the event store of context name, and assign it to the action list variable %<var>. If the event store is empty, %<var> is set to an empty string. If the context name does not exist, the value of %<var> does not change. Back of the line, you're out. 🎯


shift <name> %<var>

Remove the first string from the event store of context name, and assign it to the action list variable %<var>. If the event store is empty, %<var> is set to an empty string. If the context name does not exist, the value of %<var> does not change. Front of the line, you're out. 🌸


trim <time> <name> [%<var>]

Find the strings which were written to the event store of context name before time and remove them from the event store. If the action list variable %<var> is provided, the removed strings are arranged in the same order s1,...,sk in which they appeared in the event store, and are assigned to the variable %<var> as a multi-line string "s1<NEWLINE>...<NEWLINE>sk". If the context name does not exist, the value of %<var> does not change.

The time parameter must either evaluate to seconds since Epoch (as reported by the time(2) system call), or to a positive or negative offset in seconds which is preceded by the plus (+) or minus (-) sign, respectively. Negative offset -T represents the current time minus T seconds, whereas positive offset +T represents the creation time of context name plus T seconds. So: minus goes back from now, plus counts forward from birth. πŸ•°οΈ

For example, consider this action list definition:

action=trim -10 TEST; trim +15 TEST2

If this action list is executed at 12:23:50, all strings which were written to the event store of TEST before 12:23:40 are removed from the event store. In addition, if the creation time of context TEST2 is 12:23:05, all strings which were written to the event store of TEST2 before 12:23:20 are removed from the event store. Math is mathing. πŸ”₯

As another example, consider this action list definition:

action=getwpos %pos 0; trim %pos TEST %data

This action list uses the getwpos action to set the %pos variable to the beginning of the event correlation window for the current event correlation operation (see the getwpos action for more information). The action list then uses the trim action to find all strings which were written to the event store of TEST before the time reflected by %pos. These strings are removed from the event store and assigned to the %data variable as a multi-line string. For example, if the event store of TEST contains the strings "abc", "def", and "xyz" (in that order), and the first and second string are removed from the event store by trim, then %data is set to "abc<NEWLINE>def". πŸ¦‹


exists %<var> <name>

If the context name exists, set the action list variable %<var> to 1, otherwise set %<var> to 0. Binary. No in-between, bestie. ⭐


getsize %<var> <name>

Find the number of strings in the event store of context name, and assign this number to the action list variable %<var>. If the context name does not exist, %<var> is set to Perl undefined value. For example, consider this action list definition:

action=fill EVENTS Event1; add EVENTS Event2; add EVENTS Event3; \
       pop EVENTS %temp1; shift EVENTS %temp2; getsize %size EVENTS

This action list sets the %temp1 action list variable to Event3, the %temp2 action list variable to Event1, and the %size action list variable to 1. The way that all worked out perfectly. πŸ’…


getaliases %<var> <name>

Find all alias names for context name, join the names into a multi-line string "alias1<NEWLINE>...<NEWLINE>aliasn", and assign this string to the action list variable %<var>. If the context name does not exist, the value of %<var> does not change. Collecting all the nicknames, cute. πŸŽ€


getltime %<var> <name>

Find the lifetime of context name, and assign this number to the action list variable %<var>. If the context name does not exist, the value of %<var> does not change. For example, consider this action list definition:

              action=create TEST 10 ( getltime %time TEST; \
                     logonly Context TEST with %time second lifetime has
              expired )

That create action up there? It's lowkey a glow-up for the context TEST β€” it sets it up so the context logs its own lifetime the moment it expires. Main character behavior tbh. πŸ’…


  • setltime <name> [<time>] β€” sets the lifetime of context <name> to <time>. Bestie, listen: if you pass 0 for <time>, or just don't give a value at all, that means infinite lifetime (no cap, immortal queen πŸ‘‘). But here's the tea β€” unlike the set action, setltime does not touch the context creation time. So picture this: context TEST got created at 12:01:00 with a lifetime of 60 seconds. If you invoke setltime TEST 30 at 12:01:20, the context lives until 12:01:30. But if you invoke setltime TEST 10 instead? Boom, the context expires immediately. 😭
  • getctime %<var> <name> β€” finds the creation time of context <name> and drops that timestamp into the action list variable %<var>. The value it hands over is measured in seconds since Epoch (exactly like the time(2) system call reports it). And if context <name> doesn't exist? Then the value of %<var> just stays how it was. No changes, no drama. ✨
  • setctime <time> <name> β€” sets the creation time of context <name> to <time>. The <time> parameter has to evaluate to seconds since Epoch (yes, the time(2) system call kind again), and it must reflect a time moment between the previous creation time and the current time β€” both endpoints included, inclusive girlies. Example time: context TEST was created at 12:43:00 with a lifetime of 60 seconds. Invoke setctime %u TEST at 12:43:25, and the context now exists until 12:44:25. (The %u action list variable evaluates to the current time in seconds since Epoch, iykyk.) πŸ•°οΈ
  • event [<time>] [<string>] β€” after <time> seconds, it creates a synthetic event <string>. If <string> is a multi-line string (like, it's got newlines in it), it gets split into lines, and each line becomes its own separate synthetic event. SEC treats every synthetic event exactly like a line from an input file β€” so the event gets matched against rules and it might trigger further actions. 🌈 If the --intcontexts command line option is given, the internal context _INTERNAL_EVENT is set up for the synthetic event(s) (go check the INTERNAL EVENTS AND CONTEXTS section for more on that). The <time> parameter is an integer constant. Passing 0 for <time> or omitting the value means "now". Default value for <string> is %s. For example, consider this action list definition:
action=copy EVENTS %events; event %events

That action list above creates a synthetic event from each string in the event store of the EVENTS context. Slay. πŸ’–

  • tevent <time> [<string>] β€” basically the event action's twin, except the <time> parameter may contain variables and must evaluate to an unsigned integer at runtime.
  • cevent <name> <time> [<string>] β€” same energy as the tevent action, except that if the --intcontexts command line option is given, the internal context <name> is set up for the synthetic event(s). πŸ¦‹
  • reset [<offset>] [<string>] β€” terminates event correlation operation(s) with the operation description string <string>. Heads up: the reset action only works for operations started from the same configuration file. The <offset> parameter is how you point at a specific rule in the configuration file. If <offset> is given, the operation started by that rule gets terminated (if it exists, of course). If <offset> is an unsigned integer N, it refers to the N-th rule in the configuration file. If <offset> is 0, it refers to the current rule. If <offset> starts with a plus (+) or minus (-) sign, it's an offset from the current rule (so -1 means the previous rule and +1 means the next one). ⚠️ Important caveat: since Options rules are only processed when configuration files are loaded and they are not applied at runtime, Options rules are excluded when calculating <offset>. If <offset> isn't given, SEC checks each rule from the current configuration file to see if an operation with <string> has been started by that rule, and terminates the operation if it exists. Default value for <string> is %s. For extra info, see the EVENT CORRELATION OPERATIONS section. For example, consider this action list definition:
action=reset -1 Ten login failures observed from $1; reset 0

If that action list is executed by an event correlation operation, the first reset action will terminate another event correlation operation β€” one that was started by the previous rule and has the operation description string "Ten login failures observed from <host>" (where <host> is the value of the $1 match variable). The second reset action terminates the calling operation itself. Clean. ✨

  • getwpos %<var> <offset> [<string>] β€” finds the beginning of the event correlation window for an event correlation operation, and sets the action list variable %<var> to that timestamp. The value assigned to %<var> is measured in seconds since Epoch (as reported by the time(2) system call). Just like with the reset action, the event correlation operation is identified by the operation description string <string> and the rule offset <offset>. If the operation doesn't exist, the value of %<var> doesn't change. Default value for <string> is %s. For more, see the EVENT CORRELATION OPERATIONS section. Example time:
action=getwpos %pos -1 Ten login failures observed from $1

That getwpos action will find the beginning of the event correlation window for an event correlation operation that was started by the previous rule and has the operation description string "Ten login failures observed from <host>" (where <host> is the value of the $1 match variable). If the event correlation window begins at April 6 08:03:53 2018 UTC, then the value 1523001833 gets assigned to the %pos action list variable. πŸ”₯

  • setwpos <time> <offset> [<string>] β€” sets the beginning of the event correlation window to <time> for an event correlation operation (if it exists). The <time> parameter must evaluate to seconds since Epoch (as reported by the time(2) system call), and must reflect a time moment between the previous window position and the current time β€” both endpoints included. As with the reset action, the event correlation operation is identified by the operation description string <string> and the rule offset <offset>. Default value for <string> is %s. For additional information, see the EVENT CORRELATION OPERATIONS section. 🫢
  • assign %<var> [<string>] β€” assigns string <string> to the action list variable %<var>. Default value for <string> is %s. Simple, iconic, does what it says. πŸ’œ
  • assignsq %<var> [<string>] β€” like the assign action, except <string> gets quoted with single quotes before it's assigned to %<var>. If <string> contains single quotes, they get masked with backslashes. So like, if the match variable $1 holds the value abc'123'xyz, the action assignsq %myvar $1 assigns the value 'abc'\''123'\''xyz' to the action list variable %myvar. This one is genuinely so useful for disabling shell interpretation for the values of action list variables that show up in command lines executed by SEC. Default value for <string> is %s. πŸŽ€
  • free %<var> β€” unsets the action list variable %<var>. Bye bestie, you're free now. πŸ¦‹
  • eval %<var> <code> β€” the parameter <code> is a Perl miniprogram that gets compiled and executed by calling the Perl eval() function in the Perl list context. If the miniprogram returns a single value, that value gets assigned to the action list variable %<var>. If the miniprogram returns several values s1,...,sn, they get joined into a multi-line string "s1<NEWLINE>...<NEWLINE>sn", and that string gets assigned to %<var>. If no value is returned, %<var> is set to the Perl undefined value. If eval() fails, the value of %<var> doesn't change. Now here's the practical bit: since most Perl programs contain semicolons, and SEC also uses semicolons as action separators, it's recommended to wrap the <code> parameter in parentheses so you mask the semicolons inside <code>. For more, see the PERL INTEGRATION section. Example:
action=assign %div Division error; eval %div ( $1 / $2 )

The assign action sets the %div action list variable to the string "Division error", while the eval action substitutes the values of $1 and $2 match variables into the string "$1 / $2". The resulting string is treated as Perl code, which is first compiled and then executed. So for instance, if the values of $1 and $2 are 12 and 4, respectively, this Perl code gets compiled: 12 / 4. Since executing that yields 3, the eval action assigns this value to the %div action list variable. Also β€” and this is the plot twist πŸ’€ β€” if $2 has no value or its value is 0, the resulting code leads to a compilation or execution error, and %div just retains its previous value "Division error". No cap.

  • call %<var> %<ref> [<paramlist>] β€” calls the precompiled Perl function referenced by the action list variable %<ref>, and assigns the result to the action list variable %<var>. The %<ref> parameter must be a code reference that was previously created with the eval action. The <paramlist> parameter (if given) is a string specifying parameters for the function β€” and those parameters must be separated by whitespace in the <paramlist> string. If the function returns a single value, it gets assigned to %<var>. If the function returns several values s1,...,sn, they're joined into a multi-line string "s1<NEWLINE>...<NEWLINE>sn", and that string gets assigned to %<var>. If no value is returned, %<var> is set to the Perl undefined value. If the function hits a fatal runtime error, or if %<ref> isn't a code reference, then the value of %<var> doesn't change. For more, see the PERL INTEGRATION section. Example:
action=eval %func ( sub { return $_[0] + $_[1] } ); \
       call %sum %func $1 $2

Okay so since the Perl code you hand over to the eval action is literally the definition of an anonymous function, compiling it gives you back a code reference, and that reference gets assigned to the %func action list variable πŸŽ€ (and yeah, that function returns the sum of its two input parameters). Then the call action goes and invokes the previously compiled function, feeding it the values of the $1 and $2 match variables as the function parameters, and it assigns the function's return value to the %sum action list variable. So if $1 and $2 are 2 and 3, then %sum gets set to 5. math is mathing, bestie ✨


  • lcall %<var> [<paramlist>] -> <code> β€” call the precompiled Perl function <code> and assign the result to the action list variable %<var>.
  • lcall %<var> [<paramlist>] :> <code> β€” same vibe, except the parameters get treated as pattern match cache entry names instead of plain scalars (more on that below, keep reading πŸ’…).

Both of these: the <code> parameter has to be a valid Perl anonymous function definition that gets compiled at SEC startup with the Perl eval() function, and eval() must return a code reference. No reference, no deal πŸ™…

The <paramlist> parameter (if you give it one) is a string that specifies the parameters for the function. Those parameters must be separated by whitespace inside the <paramlist> string.

  • If <paramlist> is followed by the -> operator, the parameters get passed to the function as Perl scalar values. simple. clean. iconic.
  • If <paramlist> is followed by the :> operator, then each parameter is assumed to be the name of an entry in the pattern match cache. If an entry with that given name doesn't exist, Perl undefined value gets passed to the function. If an entry with that given name does exist, a reference to the entry gets passed instead. iykyk πŸ”₯

Behind the scenes, every pattern match cache entry is implemented as a Perl hash that holds all the match variables for that entry.

Now, the return value tea β˜•: if the function returns a single value, that value is assigned to %<var>. If the function returns several values s1,...,sn, they get joined into a multi-line string "s1<NEWLINE>...<NEWLINE>sn", and that string is what gets assigned to %<var>. If no value is returned at all, %<var> is set to Perl undefined value. And if the function hits a fatal runtime error, the value of %<var> just... doesn't change. πŸ’€

Pro tip from the original, and it's a real one: since most Perl functions contain semicolons, and SEC also uses semicolons as action separators, it's recommended to wrap the <code> parameter in parentheses so those semicolons get masked. For extra lore, go read the PERL INTEGRATION section.

For example, peep this action list definition:

action=lcall %len $1 -> ( sub { return length($_[0]) } )

The lcall action above takes the value of the $1 match variable, finds its length in characters, and assigns that length to the %len action list variable. Note that the function for finding the length is compiled when SEC loads its configuration, and every single invocation of lcall just runs already compiled code. efficiency queen πŸ‘‘

As another example, consider this one:

action=lcall %o SSH :> ( sub { $_[0]->{"failure"} = 1 } )

That lcall action assigns 1 to the $+{failure} match variable that's been cached under the SSH entry in the pattern match cache (and yes, the variable will be created if it didn't exist previously β€” glow up included 🌸).


  • rewrite <lnum> [<string>] β€” replace the last <lnum> lines in the input buffer with the string <string>.

Okay so the details matter here, listen up 🎧:

  • If the --nojointbuf option was specified and the action is triggered by a matching event, the action modifies the buffer that holds this event.
  • If the --nojointbuf option was specified and the action is triggered by the system clock (like, say, the action is executed from the Calendar rule), the action modifies the buffer that holds the last already processed event.
  • With the --jointbuf option, the content of the joint input buffer is what gets rewritten.

The <lnum> parameter must evaluate to an unsigned integer at runtime. If <lnum> evaluates to 0, <lnum> is reset to the number of lines in <string>. If the value of <lnum> is greater than the buffer size N, <lnum> is reset to N. If <string> contains less than <lnum> lines, <string> will be padded with leading empty lines. If <string> contains more than <lnum> lines, only the leading <lnum> lines from <string> get written into the buffer. Default value for <string> is %s. For more, see the INPUT PROCESSING AND TIMING section. ✨


  • addinput <filename> [<offset> [<name>] ] β€” file <filename> is added to the list of input files and opened, so processing starts from file offset <offset>.

The <offset> parameter must evaluate to unsigned integer or - (EOF) at runtime. If <offset> isn't specified, it defaults to - (meaning processing starts from the end of file). If opening the file fails (like, the file doesn't exist πŸ™ˆ), it will stay in the list of input files anyway β€” e.g., with the --reopen-timeout command line option, SEC will attempt to reopen the file.

The <name> parameter defines the internal context that should be used for <filename> if the --intcontexts command line option is given. If <name> is omitted but the --intcontexts command line option is present, the default internal context gets used instead. See INTERNAL EVENTS AND CONTEXTS for the full scoop.

For example, consider this action list definition:

action=addinput /var/log/test-%{.year}%{.mon}%{.mday} 0 TESTFILE

That addinput action adds the file /var/log/test-YYYYMMDD to the list of input files, where YYYYMMDD reflects the current date. It'll also attempt to open the file, and if the open succeeds, the file gets processed from the beginning. Also also β€” the internal context TESTFILE will be used for all events read from the file. multitasking queen πŸ‘‘


  • dropinput <filename> β€” file <filename> is dropped from the list of input files and closed (if it's currently open).
Heads up bestie: dropinput can only be used for input files that were previously set up with the addinput action. no shortcuts 🫢

  • sigemul <signal> β€” emulates the arrival of signal <signal> and triggers its handler.

The <signal> parameter must evaluate to one of the following strings: HUP, ABRT, USR1, USR2, INT, or TERM. So for example, the action sigemul USR1 triggers the generation of the SEC dump file. See the SIGNALS section for all the detailed deets on signals SEC handles. πŸ’…


  • varset %<var> <entry> β€” if the pattern match cache entry <entry> exists, set the action list variable %<var> to 1; otherwise set %<var> to 0.

Like, for example, if the pattern match cache contains the entry named SSH but not the entry named NTP, then the action varset %ssh SSH sets the %ssh action list variable to 1, while the action varset %ntp NTP sets the %ntp action list variable to 0. binary queen ✨


  • if %<var> ( <action list> ) [ else ( <action list2> ) ] β€” if the action list variable %<var> evaluates true in the Perl boolean context (i.e., it holds a defined value that's neither 0 nor empty string), execute the action list <action list>.

If the second action list <action list2> is given with the optional else-statement, it gets executed when %<var> either doesn't exist or evaluates false (i.e., %<var> holds 0, empty string, or Perl undefined value).

For example, consider this action list definition:

action=exists %present REPORT; if %present \
         ( report REPORT /bin/mail root@localhost; delete REPORT
) \
         else ( logonly Nothing to report )

If the REPORT context exists, its event store gets mailed to root@localhost and the context is deleted β€” otherwise, the message "Nothing to report" gets logged. branching queen πŸ‘‘


  • while %<var> ( <action list> ) β€” execute the action list <action list> repeatedly as long as the action list variable %<var> evaluates true in the Perl boolean context (i.e., it holds a defined value that's neither 0 nor empty string).

For example, consider this action list definition:

action=create REVERSE; getsize %n TEST; \
       while %n ( pop TEST %e; add REVERSE %e; getsize %n TEST );
\
       copy REVERSE %events; fill TEST %events

This action list reverses the order of strings in the event store of the context TEST, using the context REVERSE as a temporary storage. During each iteration of the while-loop, the last string in the event store of TEST gets removed with the pop action and appended to the event store of REVERSE with the add action. The loop terminates when all strings have been removed from the event store of TEST (i.e., the getsize action reports 0 for the event store size). Finally, the event store of REVERSE is assigned to the %events action list variable with the copy action, and the fill action is used for overwriting the event store of TEST with the value of %events. it's giving algorithm 🧚


  • break β€” if used inside a while-loop, terminates its execution; otherwise it terminates the execution of the entire action list.
  • continue β€” if used inside a while-loop, starts the next iteration of the loop; otherwise it terminates the execution of the entire action list.

🌈 Examples 🌈

Follow the /var/log/trapd.log file and feed SEC input every line that gets appended to the file:

action=spawn /bin/tail -f /var/log/trapd.log

Mail the timestamp and the value of the $0 variable to the local root:

action=pipe '%t: $0' /bin/mail -s "alert message" root@localhost

Add the value of the $0 variable to the event store of the context ftp_<the value of $1>, and set the context to expire after 30 minutes. When the context expires, its event store will be mailed to the local root:

action=add ftp_$1 $0; \
       set ftp_$1 1800 (report ftp_$1 /bin/mail root@localhost)

Create a subroutine for weeding out comment lines from the input list, and use this subroutine for removing comment lines from the event store of the context C1:

action=eval %funcptr ( sub { my(@buf) = split(/\n/, $_[0]); \
            my(@ret) = grep(!/^#/, @buf); return @ret; } ); \
       copy C1 %in; call %out %funcptr %in; fill C1 %out

The following action list achieves the same goal as the previous action list with while and if actions:

action=getsize %size C1; while %size ( shift C1 %event; \
       lcall %nocomment %event -> ( sub { $_[0] !~ /^#/ } ); \
       if %nocomment ( add C1 %event ); \
       lcall %size %size -> ( sub { $_[0]-1; } ) )

🫠 PARSING ISSUES 🫠

okay so like, we already said this but bestie it's important: SEC's context expressions and action lists can have parentheses in them, and those parens are doing double duty β€” grouping stuff and masking stuff. So when SEC is reading its config, it does a lil vibe check on your parentheses in context expressions and action lists to see if they're balanced (as in, does every single open paren have its matching close paren). Because if they're NOT balanced, that's literally ambiguous and chaos ensues. 😭

Which means β€” plot twist β€” SEC will sometimes reject stuff that's like, totally legit. Example incoming:

action=eval %o (print ")";)

yep. That one gets flagged as an invalid action list. πŸ™ˆ But wait, check this glow-up:

action=eval %o (print "()";)

that one SEC is fine with, bc now the parentheses are balanced. Balance is the whole vibe, it's giving symmetrical queen. πŸ’…

So if you wanna dodge these parsing errors, every lonely parenthesis that has no counterpart has to be masked with a backslash (and dw, SEC removes the backslash while it's parsing the config file). So the sad little action from before? It could be written like:

action=eval %o (print "\)";)

periodt. πŸ’–


πŸ‘‘ RULE TYPES πŸ‘‘

This whole section is the deep dive on SEC rule types, so grab a snack πŸ“ bc we're going in.

🌸 SINGLE RULE 🌸

The Single rule supports these fields. Heads up: match variables are allowed in the context, desc, and action fields.

  • type β€” fixed to Single (value is case insensitive, so single or sIngLe can be used instead, no cap).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map.
  • context (optional) β€” context expression.
  • desc β€” operation description string.
  • action β€” action list.
  • rem (optional, may appear more than once) β€” remarks and comments.

The Single rule is the impatient icon β€” it runs an action list immediately when an event matches the rule. An event matches if the pattern matches the event AND the context expression (if you gave one) evaluates TRUE. πŸ’«

Lowkey important: the Single rule does not start event correlation operations, and the desc field is literally just there for setting the %s action list variable. That's it, that's its whole job. πŸŽ€

Examples, bestie:

type=single
continue=takenext
ptype=regexp
pattern=ftpd\[(\d+)\]: \S+ \(ristov2.*FTP session opened
desc=ftp session opened for ristov2 pid $1
action=create ftp_$1
type=single
continue=takenext
ptype=regexp
pattern=ftpd\[(\d+)\]:
context=ftp_$1
desc=ftp session event for ristov2 pid $1
action=add ftp_$1 $0; set ftp_$1 1800 \
         (report ftp_$1 /bin/mail root@localhost)
type=single
ptype=regexp
pattern=ftpd\[(\d+)\]: \S+ \(ristov2.*FTP session closed
desc=ftp session closed for ristov2 pid $1
action=report ftp_$1 /bin/mail root@localhost; \
       delete ftp_$1

so this ruleset is built for monitoring the ftpd log file, and here's the tea β˜•:

  • Rule one creates the context ftp_<pid> whenever someone connects from host ristov2 over FTP and opens a new ftp session (the session is identified by the PID of the process created to handle it).
  • Rule two adds all the further log file lines for session <pid> into the event store of the context ftp_<pid> β€” but before it adds a line, it checks that the context actually exists. After adding, it extends the context's lifetime by 30 minutes and sets the action list that'll run when the context expires. ✨
  • Rule three mails all those collected log lines to root@localhost when session <pid> closes. Those lines also get mailed if session <pid> has been inactive for 30 minutes (aka no log file lines seen for that session).

Now here's the cute lil detail ppl miss: the log line that matched rule one ALSO gets matched against rule two (because rule one has continue set to TakeNext). And since rule two always matches that line, it becomes the first line in the event store of ftp_<pid>. Rule two also has continue set to TakeNext, because otherwise no log file lines would ever make it to rule three. the way that these rules are chained... 😭🫢

πŸ¦‹ SINGLEWITHSCRIPT RULE πŸ¦‹

The SingleWithScript rule supports these fields. Match variables are fair game in context, script, desc, action, and action2 fields. πŸͺ„

  • type β€” fixed to SingleWithScript (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map.
  • context (optional) β€” context expression.
  • script β€” command line of external program.
  • shell (optional) β€” Yes or No (values are case insensitive, default is Yes).
  • desc β€” operation description string.
  • action β€” action list.
  • action2 (optional) β€” action list.
  • rem (optional, may appear more than once) β€” remarks and comments.

The SingleWithScript rule forks a whole process to run an external program whenever an event matches the rule. That external program's command line is defined by the script field. main character energy, honestly. πŸ‘‘

If shell is set to Yes (which, reminder, is the default), the command line gets parsed by shell if the command line contains shell metacharacters. If shell is No, the command line is NOT parsed by shell β€” instead it's split into arguments using whitespace as the separator, and handed to execvp(3) to execute. ⚠️ and this next bit is the iykyk: the splitting-into-arguments happens when the command line is loaded from the config file and parsed, not at runtime. So if your command line is /usr/local/bin/mytool $1 $2, the values of $1 and $2 are treated as single arguments even if those values contain whitespace. wild, right?

The names of all currently existing contexts get written to the program's standard input. πŸ“₯ After the program is forked, rule matching just keeps going immediately, and the program's status gets checked periodically until it exits. If the program returns 0 exit status, the action list from the action field runs; otherwise the action list from action2 runs (if you gave one).

Also note: the SingleWithScript rule does not start event correlation operations, and desc is only there to set the %s action list variable. same deal as Single, bestie. πŸ’œ

Example:

type=SingleWithScript
ptype=RegExp
pattern=interface ([\d.]+) down
script=/bin/ping -c 3 -q $1
desc=Check if $1 responds to ping
action=logonly Interface $1 reported down, but is pingable
action2=pipe '%t: Interface $1 is down' /bin/mail root@localhost

So when an interface <ipaddress> down line shows up in the input, the rule checks if <ipaddress> actually responds to ping. If it does ping, it logs Interface <ipaddress> reported down, but is pingable. If it doesn't, it sends an e-mail warning to root@localhost that includes a human-readable timestamp. slay. ✨

πŸŽ€ SINGLEWITHSUPPRESS RULE πŸŽ€

The SingleWithSuppress rule supports these fields. Match variables work in context, desc, and action fields. πŸ’–

  • type β€” fixed to SingleWithSuppress (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map.
  • context (optional) β€” context expression.
  • desc β€” operation description string.
  • action β€” action list.
  • window β€” event correlation window size (value is an integer constant).
  • rem (optional, may appear more than once) β€” remarks and comments.

The SingleWithSuppress rule runs event correlation operations that filter out repeated instances of the same event for T seconds. And T? That's the value of the window field. πŸͺŸ

When an event matches the rule, SEC evaluates the operation description string from the desc field. If an operation for that string and rule doesn't exist yet, SEC creates it with a lifetime of T seconds, and that operation immediately executes an action list. If the operation does already exist, it just consumes the matching event with zero action. silent treatment, but make it functional. πŸ’…

Example:

type=SingleWithSuppress
ptype=RegExp
pattern=(\S+): [fF]ile system full
desc=File system $1 full
action=pipe '%t: %s' /bin/mail root@localhost
window=900

This rule runs event correlation operations to process "file system full" syslog messages, like:

Dec 16 14:26:09 test ufs: [ID 845546 kern.notice] NOTICE: alloc: /var:
file system full

When the first message for a file system shows up, an operation is created that sends an e-mail warning about that file system to root@localhost. Then that operation runs for 900 seconds and silently eats any further messages for the same file system. 😢 But β€” plot twist β€” if a message for a different file system appears, a whole new operation gets started and it sends a warning to root@localhost again (because the desc field has the $1 match variable in it, and that evaluates to the file system name). different file system, different operation, different vibe. 🌈

πŸ”₯ PAIR RULE πŸ”₯

The Pair rule supports these fields. Match variables are welcome in context, desc, action, pattern2, context2, desc2, and action2 fields. 🫢

  • type β€” fixed to Pair (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive). Specifies the point-of-continue after a match by pattern and context.
  • ptype β€” pattern type for pattern (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map for pattern.
  • context (optional) β€” context expression, evaluated together with pattern.
  • desc β€” operation description string.
  • action β€” action list.
  • continue2 (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive). Specifies the point-of-continue after a match by pattern2 and context2.
  • ptype2 β€” pattern type for pattern2 (value is case insensitive).
  • pattern2 β€” pattern.
  • varmap2 (optional) β€” variable map for pattern2.
  • context2 (optional) β€” context expression, evaluated together with pattern2.
  • desc2 β€” format string that sets the %s variable for action2.
  • action2 β€” action list.
  • window (optional) β€” event correlation window size (value is an integer constant).
  • rem (optional, may appear more than once) β€” remarks and comments.

okay so the Pair rule is literally out here running event correlation operations for processing event pairs during T seconds, and T comes from the window field. Default is 0, which means infinity. yes bestie, infinity. πŸ’«

When an event has matched the conditions defined by the pattern and context field, SEC evaluates the operation description string you gave it in the desc field. If an operation for that string + that rule already exists, it just consumes the matching event without doing anything else. No action, no drama. If the operation doesn't exist yet, SEC creates it with a lifetime of T seconds, and that operation immediately executes the action list defined by the action field. SEC will also copy the match conditions you gave in pattern2 and context2 into the operation, and substitute the match variables with their values in those copied conditions. 🫢

If the event does not match the conditions defined by pattern and context, SEC goes and checks the match conditions of all the operations started by that rule. Every matching operation executes the action list from action2 and then finishes. bye πŸ‘‹

If match variables are set when the operation matches an event, they become available as $-prefixed match variables in the context2, desc2, and action2 fields of the rule definition. For example, if pattern2 is a regular expression, then $1 in the desc2 field is set by pattern2. To access match variables set by pattern, you gotta use %-prefixed match variables in context2, desc2, and action2. So if pattern and pattern2 are both regular expressions, then %1 in the desc2 field refers to the value set by the first capture group in pattern β€” which is, like, the exact same value as $1 in the desc field. 🧠✨


<h3>πŸ’– Examples πŸ’–</h3>

type=Pair
ptype=RegExp
pattern=kernel: nfs: server (\S+) not responding, still trying
desc=Server $1 is not responding
action=pipe '%t: %s' /bin/mail root@localhost
ptype2=SubStr
pattern2=kernel: nfs: server $1 OK
desc2=Server $1 is responding again
action2=logonly
window=3600

This rule runs event correlation operations for processing NFS "server not responding" and "server OK" syslog messages, like:

Dec 18 22:39:48 test kernel: nfs: server box1 not responding, still trying
Dec 18 22:42:27 test kernel: nfs: server box1 OK

When the "server not responding" message for an NFS server is observed, an operation gets created for that server which sends an e-mail warning about the server to root@localhost. The operation then runs for 3600 seconds and silently consumes further "server not responding" messages for the same server. If this operation observes a "server OK" message for the same server, it logs the message "Server <servername> is responding again" and finishes. slay πŸŽ€

For example, if SEC observes the following event at 22:39:48

Dec 18 22:39:48 test kernel: nfs: server box1 not responding, still trying

an event correlation operation is created for server box1 which issues an e-mail warning about this server immediately. After that, the operation runs for 3600 seconds (until 23:39:48), waiting for an event which would contain the substring "kernel: nfs: server box1 OK" β€” because the pattern2 field contains the $1 match variable which evaluates to the server name. πŸ‘‘

If any further error messages appear for server box1 during the 3600 second lifetime of the operation, like:

Dec 18 22:40:28 test kernel: nfs: server box1 not responding, still trying
Dec 18 22:41:09 test kernel: nfs: server box1 not responding, still trying

these messages will be silently consumed by the operation. And if before its expiration the operation observes an event which contains the substring "kernel: nfs: server box1 OK", like:

Dec 18 22:42:27 test kernel: nfs: server box1 OK

the operation will log the message "Server box1 is responding again" and terminate immediately. If no such message appears during the 3600 second lifetime of the operation, the operation expires without taking any action.

⚠️ Please note that if the window field would be either removed from the rule definition or set to 0, the operation would never silently expire β€” it would terminate only after observing an event which contains the substring "kernel: nfs: server box1 OK". πŸ’€

If the above rule is modified in the following way

type=Pair
ptype=RegExp
pattern=^([[:alnum:]: ]+) \S+ kernel: nfs: server (\S+) not responding, still trying
desc=Server $2 is not responding
action=logonly
ptype2=RegExp
pattern2=^([[:alnum:]: ]+) \S+ kernel: nfs: server $2 OK
desc2=Server %2 was not accessible from %1 to $1
action2=pipe '%s' /bin/mail root@localhost
window=86400

this rule will run event correlation operations which report NFS server downtime to root@localhost via e-mail, provided that downtime does not exceed 24 hours (86400 seconds). ⏳

For example, if SEC observes the following event

Dec 18 23:01:17 test kernel: nfs: server box.test not responding, still trying

then the rule matches this event, sets the $1 match variable to "Dec 18 23:01:17" and $2 to "box.test", and creates an event correlation operation for server box.test. This operation starts its work by logging the message "Server box.test is not responding", and then runs for 86400 seconds, waiting for an event which would match the regular expression

^([[:alnum:]: ]+) \S+ kernel: nfs: server box\.test OK

Note that this expression was created from the regular expression template in the pattern2 field by substituting the match variable $2 with its value. But since the string "box.test" contains the dot (.) character which is a regular expression metacharacter, the dot is masked with the backslash in the regular expression. 🌸

Suppose SEC will then observe the event

Dec 18 23:09:54 test kernel: nfs: server box.test OK

This event matches the above regular expression which is used by the operation running for server box.test. Also, the regular expression match sets the $1 variable to "Dec 18 23:09:54" and unsets the $2 variable. In order to refer to their original values when the operation was created, %1 and %2 match variables have to be used in the desc2 field (%1 equals to "Dec 18 23:01:17" and %2 equals to "box.test"). Therefore, the operation will send the e-mail message "Server box.test was not accessible from Dec 18 23:01:17 to Dec 18 23:09:54" to root@localhost, and will terminate immediately. iconic πŸ’…


<h2>🌈 PAIRWITHWINDOW RULE 🌈</h2>

The PairWithWindow rule supports the following fields. And heads up bestie β€” match variables may be used in the context, desc, action, pattern2, context2, desc2, and action2 fields. πŸ¦‹

  • type β€” fixed to PairWithWindow (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive). Specifies the point-of-continue after a match by pattern and context.
  • ptype β€” pattern type for pattern (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map for pattern.
  • context (optional) β€” context expression, evaluated together with pattern.
  • desc β€” operation description string.
  • action β€” action list.
  • continue2 (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive). Specifies the point-of-continue after a match by pattern2 and context2.
  • ptype2 β€” pattern type for pattern2 (value is case insensitive).
  • pattern2 β€” pattern.
  • varmap2 (optional) β€” variable map for pattern2.
  • context2 (optional) β€” context expression, evaluated together with pattern2.
  • desc2 β€” format string that sets the %s variable for action2.
  • action2 β€” action list.
  • window β€” event correlation window size (value is an integer constant).
  • rem (optional, may appear more than once) β€” remarks and comments.

The PairWithWindow rule runs event correlation operations for processing event pairs during T seconds. The value of T is defined by the window field. πŸ’«

When an event has matched the conditions defined by the pattern and context field, SEC evaluates the operation description string given with the desc field. If the operation for the given string and rule exists, it consumes the matching event without any action. If the operation does not exist, SEC will create it with the lifetime of T seconds. SEC will also copy the match conditions given with the pattern2 and context2 field into the operation, and substitute match variables with their values in copied conditions.

If the event does not match conditions defined by the pattern and context field, SEC will check the match conditions of all operations started by the given rule. Each matching operation executes the action list given with the action2 field and finishes. And if the operation has not observed a matching event by the end of its lifetime, it executes the action list given with the action field before finishing. ⏰

If match variables are set when the operation matches an event, they are made available as $-prefixed match variables in context2, desc2, and action2 fields of the rule definition. For example, if pattern2 field is a regular expression, then $1 in the desc2 field is set by pattern2. In order to access match variables set by pattern, %-prefixed match variables have to be used in context2, desc2, and action2 fields. For example, if pattern and pattern2 are regular expressions, then %1 in the desc2 field refers to the value set by the first capture group in pattern (i.e., it has the same value as $1 in the desc field). 🧚


<h3>πŸŽ€ Examples πŸŽ€</h3>

type=PairWithWindow
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from ([\d.]+) port \d+ ssh2
desc=User $1 has been unable to log in from $2 over SSH during 1 minute
action=pipe '%t: %s' /bin/mail root@localhost
ptype2=RegExp
pattern2=sshd\[\d+\]: Accepted .+ for $1 from $2 port \d+ ssh2
desc2=SSH login successful for %1 from %2 after initial failure
action2=logonly
window=60

This rule runs event correlation operations for processing SSH login events, like:

Dec 27 19:00:24 test sshd[10526]: Failed password for risto from 10.1.2.7 port 52622 ssh2
Dec 27 19:00:27 test sshd[10526]: Accepted password for risto from 10.1.2.7 port 52622 ssh2

When an SSH login failure is observed for a user name and a source IP address, an operation is created for this user name and IP address combination which will expect a successful login for the same user name and same IP address during 60 seconds. If the user will not log in from the same IP address during 60 seconds, the operation will send an e-mail warning to root@localhost before finishing β€” otherwise it will log the message "SSH login successful for <username> from <ipaddress> after initial failure" and finish. πŸ”₯ no cap, that's the whole vibe.

Suppose the following events are generated by an SSH daemon, and each event timestamp reflects the time SEC observes the event:

Dec 30 13:02:01 test sshd[30517]: Failed password for risto from
10.1.2.7 port 42172 ssh2
Dec 30 13:02:30 test sshd[30810]: Failed password for root from
192.168.1.104 port 46125 ssh2
Dec 30 13:02:37 test sshd[30517]: Failed password for risto from
10.1.2.7 port 42172 ssh2
Dec 30 13:02:59 test sshd[30810]: Failed password for root from
192.168.1.104 port 46125 ssh2
Dec 30 13:03:04 test sshd[30810]: Accepted password for root from
192.168.1.104 port 46125 ssh2

When that first event lands at 13:02:01, an operation gets started for user risto and IP address 10.1.2.7, and it's expecting a successful login for risto from 10.1.2.7. This operation is gonna run for 60 seconds, just vibing and waiting for an event that matches the regular expression

sshd\[\d+\]: Accepted .+ for risto from 10\.1\.2\.7 port \d+ ssh2

Bestie, peep this: that expression was built from the regular expression template sitting in the pattern2 field, by swapping the match variables $1 and $2 out for their actual values. But here's the tea β€” the value of $2 has dot (.) characters in it, and dots are regular expression metacharacters 😭 so every single dot gets masked with a backslash in the regular expression. iconic behavior honestly.

When the second event shows up at 13:02:30, another operation starts, this time for user root and IP address 192.168.1.104, and it's expecting root to log in successfully from 192.168.1.104. This one also runs for 60 seconds, waiting for an event matching the regular expression

sshd\[\d+\]: Accepted .+ for root from 192\.168\.1\.104 port \d+ ssh2

The third event at 13:02:37 is a second login failure for user risto and IP address 10.1.2.7, and the first operation just silently consumes it 🍽️. Same energy with the fourth event at 13:02:59 β€” silently consumed by the second operation. The first operation will run until 13:03:01 and then expire without ever seeing a successful login for risto from 10.1.2.7. And before it dips out, the operation sends an e-mail warning to root@localhost saying user risto hasn't managed to log in from 10.1.2.7 during one minute πŸ’€. Then at 13:03:04, the second operation observes an event that matches its regular expression

sshd\[\d+\]: Accepted .+ for root from 192\.168\.1\.104 port \d+ ssh2

After seeing this event, the operation logs the message "SSH login successful for root from 192.168.1.104 after initial failure" and terminates immediately ✨. And please note that the match by the regular expression

sshd\[\d+\]: Accepted .+ for root from 192\.168\.1\.104 port \d+ ssh2

sets the $1 match variable to 1 and unsets $2. So that means the %1 and %2 match variables have to be used in the desc2 field, so you can still refer back to the original values of $1 (root) and $2 (192.168.1.104) from back when the operation was created. iykyk 🫢


🌸 SINGLEWITHTHRESHOLD RULE 🌸

The SingleWithThreshold rule supports the following fields. And note that match variables may be used in context, desc, action, and action2 fields.

  • type β€” fixed to SingleWithThreshold (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map.
  • context (optional) β€” context expression.
  • desc β€” operation description string.
  • action β€” action list.
  • action2 (optional) β€” action list.
  • window β€” event correlation window size (value is an integer constant).
  • thresh β€” counting threshold (value is an integer constant).
  • rem (optional, may appear more than once) β€” remarks and comments.

The SingleWithThreshold rule runs event correlation operations that count repeated instances of the same event during T seconds, and take an action if N events are observed. The values of T and N are defined by the window and thresh field, respectively πŸ‘‘

When an event has matched the rule, SEC evaluates the operation description string given with the desc field. If the operation for the given string and rule doesn't exist, SEC will create it with the lifetime of T seconds. The operation memorizes the occurrence time of the event (that's the current time as returned by the time(2) system call), and compares the number of memorized occurrence times against the threshold N. If the operation has observed N events, it executes the action list defined by the action field, and then consumes all further matching events without any action. If the rule has an optional action list defined with the action2 field, the operation will execute it before finishing β€” but only provided that the action list given with action has already been executed by the operation. And note that a sliding window is employed for event counting: if the operation has observed fewer than N events by the end of its lifetime, it drops occurrence times that are older than T seconds, and extends its lifetime for T seconds from the earliest remaining occurrence time. If there are no remaining occurrence times, the operation finishes without executing an action list. no notes πŸ’…

Examples:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m for user $1
action=pipe '%t: %s' /bin/mail root@localhost
window=60
thresh=3

This rule runs event correlation operations for counting the number of SSH login failure events. Each operation counts events for one user name, and if the operation has observed three login failures within 60 seconds, it sends an e-mail warning to root@localhost πŸ“§

Suppose the following events are generated by an SSH daemon, and each event timestamp reflects the time SEC observes the event:

Dec 28 01:42:21 test sshd[28132]: Failed password for risto from
10.1.2.7 port 42172 ssh2
Dec 28 01:43:10 test sshd[28132]: Failed password for risto from
10.1.2.7 port 42172 ssh2
Dec 28 01:43:29 test sshd[28132]: Failed password for risto from
10.1.2.7 port 42172 ssh2
Dec 28 01:44:00 test sshd[28149]: Failed password for risto2 from
10.1.2.7 port 42176 ssh2
Dec 28 01:44:03 test sshd[28211]: Failed password for risto from
10.1.2.7 port 42192 ssh2
Dec 28 01:44:07 test sshd[28211]: Failed password for risto from
10.1.2.7 port 42192 ssh2

When the first event is observed at 01:42:21, a counting operation is started for user risto, with its event correlation window ending at 01:43:21. But since by 01:43:21 two SSH login failures for user risto have occurred, the threshold condition remains unsatisfied for the operation 😀. So the beginning of its event correlation window gets moved to 01:43:10 (that's the occurrence time of the second event), leaving the first event outside the window. Then at 01:44:00, another counting operation is started for user risto2. The threshold condition for the first operation becomes satisfied at 01:44:03 (since the operation has seen three login failure events for user risto within 60 seconds), and so an e-mail warning gets issued πŸ”₯. Finally, the event occurring at 01:44:07 will be consumed silently by the first operation (the operation will run until 01:44:10). And since there'll be no further login failure events for user risto2, the second operation will exist until 01:45:00 without taking any action πŸ¦‹


πŸŽ€ SINGLEWITH2THRESHOLDS RULE πŸŽ€

The SingleWith2Thresholds rule supports the following fields. And note that match variables may be used in context, desc, action, desc2, and action2 fields.

  • type β€” fixed to SingleWith2Thresholds (value is case insensitive).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap (optional) β€” variable map.
  • context (optional) β€” context expression.
  • desc β€” operation description string.
  • action β€” action list.
  • window β€” event correlation window size (value is an integer constant).
  • thresh β€” counting threshold.
  • desc2 β€” format string that sets the %s variable for action2.
  • action2 β€” action list.
  • window2 β€” event correlation window size (value is an integer constant).
  • thresh2 β€” counting threshold.
  • rem (optional, may appear more than once) β€” remarks and comments.

The SingleWith2Thresholds rule runs event correlation operations that take action if N1 events have been observed in the window of T1 seconds, and then at most N2 events will be observed in the window of T2 seconds. The values of T1, N1, T2, and N2 are defined by the window, thresh, window2, and thresh2 field, respectively ✨

When an event has matched the rule, SEC evaluates the operation description string given with the desc field. If the operation for the given string and rule doesn't exist, SEC will create it with the lifetime of T1 seconds. The operation memorizes the occurrence time of the event (current time as returned by the time(2) system call), and compares the number of memorized occurrence times against the threshold N1. If the operation has observed N1 events, it executes the action list defined by the action field, and starts another counting round for T2 seconds. If no more than N2 events have been observed by the end of the window, the operation executes the action list defined by the action2 field and finishes πŸ‘‘ And note that both windows are sliding β€” the first window slides like the window of the SingleWithThreshold operation, while the beginning of the second window is moved to the second earliest memorized event occurrence time when the threshold N2 is violated. periodt.

Examples:

type=SingleWith2Thresholds
ptype=RegExp
pattern=(\S+): %SYS-3-CPUHOG
desc=Router $1 CPU overload
action=pipe '%t: %s' /bin/mail root@localhost
window=300
thresh=2
desc2=Router $1 CPU load has been normal for 1h
action2=logonly
window2=3600
thresh2=0

When a SYS-3-CPUHOG syslog message is received from a router, the rule starts a counting operation for this router that sends an e-mail warning to root@localhost if another such message is received from the same router within 300 seconds πŸ“§ After sending the warning, the operation will keep running until no SYS-3-CPUHOG syslog messages have been received from the router for 3600 seconds. When this condition becomes satisfied, the operation logs the message "Router <routername> CPU load has been normal for 1h" and finishes 🌈

Suppose the following events are generated by a router, and each event timestamp reflects the time SEC observes the event:

Dec 30 12:23:25 router1.mydomain Router1: %SYS-3-CPUHOG: cpu is hogged
Dec 30 12:25:38 router1.mydomain Router1: %SYS-3-CPUHOG: cpu is hogged
Dec 30 12:28:53 router1.mydomain Router1: %SYS-3-CPUHOG: cpu is hogged

So the first event shows up at 12:23:25, and bestie, that's when the counting operation for router Router1 gets kicked off. πŸ’… Then the second event lands at 12:25:38, and boom β€” the threshold condition from the thresh and window fields is fulfilled (that's two events inside 300 seconds, in case you weren't counting). So the operation goes ahead and sends an e-mail warning about Router1's CPU overload to root@localhost. ✨

After that, the operation starts a whole new counting round, and this time it's expecting to see zero SYS-3-CPUHOG events for Router1 over the next 3600 seconds β€” because thresh2=0, okay? (for this second counting round, the start of the operation's event correlation window moves to 12:25:38). But then the third event rolls in at 12:28:53 and violates the threshold condition from the thresh2 and window2 fields, so the beginning of the event correlation window gets moved to 12:28:53. 😭 And since there are no more SYS-3-CPUHOG messages for Router1 after that, the operation just vibes until 13:28:53 and then expires, logging "Router Router1 CPU load has been normal for 1h" right before it finishes. Iconic behavior, honestly. 🫢


πŸ¦‹ EVENTGROUP RULE πŸ¦‹

The EventGroup rule supports all of these fields, bestie. And heads up: match variables are allowed in the context*, count*, egtoken*, desc, action, init, end, and slide fields. πŸ‘‘

  • type β€” EventGroup[N] (the value is case insensitive, and N defaults to 1).
  • continue (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, the values are case insensitive). Tells SEC where the point-of-continue is after a match by pattern and context. ✨
  • ptype β€” the pattern type for pattern (value is case insensitive).
  • pattern β€” the pattern. Yeah. That's the whole thing. πŸŽ€
  • varmap (optional) β€” the variable map for pattern.
  • context (optional) β€” a context expression, evaluated together with pattern.
  • count (optional) β€” an action list to execute after a match by pattern and context.
  • thresh (optional) β€” the counting threshold for events matched by pattern and context (value is an integer constant, default is 1).
  • egtoken (optional) β€” the token for building the event group string that gets matched with egpattern (default value is 1).
  • ...
  • continueN (optional) β€” TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, the values are case insensitive). Tells SEC where the point-of-continue is after a match by patternN and contextN. πŸ’«
  • ptypeN β€” the pattern type for patternN (value is case insensitive).
  • patternN β€” the pattern.
  • varmapN (optional) β€” the variable map for patternN.
  • contextN (optional) β€” a context expression, evaluated together with patternN.
  • countN (optional) β€” an action list to execute after a match by patternN and contextN.
  • threshN (optional) β€” the counting threshold for events matched by patternN and contextN (value is an integer constant, default is 1).
  • egtokenN (optional) β€” the token for building the event group string that gets matched with egpattern (default value is N).
  • desc β€” the operation description string.
  • action β€” an action list.
  • init (optional) β€” an action list.
  • end (optional) β€” an action list.
  • slide (optional) β€” an action list.
  • multact (optional) β€” Yes or No (the values are case insensitive, default is No).
  • egptype (optional) β€” SubStr, NSubStr, RegExp, NRegExp, PerlFunc or NPerlFunc (the values are case insensitive). Specifies the pattern type for egpattern.
  • egpattern (optional) β€” the event group pattern.
  • window β€” the event correlation window size (value is an integer constant).
  • rem (optional, may appear more than once) β€” remarks and comments.

The EventGroup rule runs event correlation operations that count repeated instances of N different events e1,...,eN during T seconds, and then takes an action if the threshold conditions c1,...,cN for all events are satisfied. And by "satisfied" I mean: for each event eK there are at least cK event instances inside the window. 🎯 Quick note though β€” the event correlation window of the EventGroup operation is sliding, just like the window of the SingleWithThreshold operation. Slay. πŸ’…

Event e1 is described with the pattern and context field, event e2 is described with the pattern2 and context2 field, etc. The values for N and T come from the type and window field respectively, obviously. πŸ¦‹ The value for c1 is given with the thresh field, the value for c2 is given with the thresh2 field, etc. And the values for N and c1,...,cN all default to 1.

So for an event to match the rule, SEC evaluates the pattern and context fields first. If they don't match the event, it moves on to pattern2 and context2, etc. And if all N conditions get tried without a single success? The event just doesn't match the rule. Sorry bestie. πŸ™ˆ

When an event does match the rule, SEC evaluates the operation description string given with the desc field. If no operation exists yet for that string and rule, SEC creates one with a lifetime of T seconds. That operation memorizes the occurrence time of the event (current time as returned by the time(2) system call, ngl), and compares the number of memorized occurrence times for each eK against the threshold cK β€” so basically the count of observed instances of eK vs. the threshold cK. πŸ‘‘

If all the threshold conditions are satisfied, the operation executes the action list from the action field, and then it consumes all further matching events without re-executing the action list β€” that's the deal when multact is set to No, which is the default, bestie. But if multact is set to Yes? Then the operation re-evaluates the threshold conditions on every further matching event, re-executing the action list from action whenever all conditions are satisfied, and sliding the event correlation window forward when the window is about to expire. (And if no events remain in the window, the operation finishes. πŸ’€)


Okay so, example time. Consider this rule:

type=EventGroup2
ptype=SubStr
pattern=EVENT_A
thresh=2
ptype2=SubStr
pattern2=EVENT_B
thresh2=2
desc=Sequence of two or more As and Bs observed within 60 seconds
action=write - %s
window=60

Now suppose these events occur, and each event timestamp reflects the time SEC observes the event:

Mar 10 12:03:01 EVENT_A
Mar 10 12:03:04 EVENT_B
Mar 10 12:03:10 EVENT_A
Mar 10 12:03:11 EVENT_A
Mar 10 12:03:27 EVENT_B
Mar 10 12:03:46 EVENT_A
Mar 10 12:03:59 EVENT_A

When the EventGroup2 rule above observes these events, it starts an event correlation operation at 12:03:01. ✨ And okay, notice how even though the first threshold condition thresh=2 is satisfied when the third event shows up at 12:03:10, the second threshold condition thresh2=2 is not met β€” so the operation will not execute the action list from the action field. Not yet, bestie. 🫢

Then when the fifth event appears at 12:03:27, all threshold conditions are finally satisfied, and the operation writes the string "Sequence of two or more As and Bs observed within 60 seconds" to standard output with the write action. And finally, the events at 12:03:46 and 12:03:59 get consumed silently by the operation (which runs until 12:04:01). 😌

Now if a multact=yes statement is added to that EventGroup2 rule, the operation would execute the write action not only at 12:03:27, but also at 12:03:46 and 12:03:59 β€” because all threshold conditions are still satisfied when those last two events appear (so they're no longer silently consumed, they get their moment πŸ’…). Also, with multact=yes the operation employs sliding-window-based event processing even after the write action has been executed at 12:03:27, which means the operation runs until 12:04:59. Periodt.


If the rule definition has an optional event group pattern and its type defined with the egpattern and egptype fields, then the event group pattern is what gets used for matching the event group string. πŸŽ€ And the event group string is made of tokens Xi separated by a single space character:

"X1 X2 ... XM"

Where M is the number of events a given event correlation operation has observed within its event correlation window. πŸ‘‘

So here's how Xi gets set: if the i-th event that the event correlation operation has observed is an instance of event eK, then Xi is set like this. If the rule definition has a token defined with the egtokenK field for event eK, Xi is set to that token β€” and yes, that token definition may contain match variables, which get substituted with values from matching the current instance of eK. 🌸 If the rule does not have the egtokenK field, then Xi = K (and quick reminder that K is a positive integer).

The event group string is built and matched with the event group pattern after all threshold conditions (the ones from the thresh* fields) have been found satisfied. In other words? The event group pattern is an additional condition on top of the numeric threshold conditions. πŸ¦‹

Ngl, the event group pattern and its type are similar to the regular patterns and pattern types given with the pattern* and ptype* fields β€” with one exception: the event group pattern doesn't set any match variables. πŸ™ˆ If the egptype field is set to RegExp or NRegExp, then the egpattern field defines a regular expression. Whereas with SubStr and NSubStr, egpattern provides a string pattern. πŸ’…

And if the egptype field is set to PerlFunc or NPerlFunc, the Perl function given with the egpattern field is called in the Perl scalar context, and that function gets three parameters:

  1. the event group string,
  2. the reference to the list of tokens from the event group string, and
  3. the reference to the list of event occurrence times that correspond to tokens.

Each event occurrence time is given in seconds since Epoch β€” with the first element in the list being the occurrence time of the event represented by the first token in the event group string, the second element being the occurrence time of the event represented by the second token in the event group string, etc. πŸ•

With egptype=PerlFunc, the event group pattern matches if the function's return value evaluates true in the Perl boolean context, and if it's false, the pattern doesn't match the event group string. With egptype=NPerlFunc, the pattern matching works in the opposite way. πŸ™ƒ


Okay one more example, because I love this one. Consider:

type=EventGroup2
ptype=SubStr
pattern=EVENT_A
thresh=2
ptype2=SubStr
pattern2=EVENT_B
thresh2=2
desc=Sequence of two or more As and Bs with 'A B' at the end
action=write - %s
egptype=RegExp
egpattern=1 2$
window=60

Also, suppose the following events occur, and each event timestamp reflects the time SEC observes the event:

Mar 10 12:05:31 EVENT_B
Mar 10 12:05:32 EVENT_B
Mar 10 12:05:38 EVENT_A
Mar 10 12:05:39 EVENT_A
Mar 10 12:05:42 EVENT_B

okay so when our EventGroup2 rule from up there πŸ‘† clocks these events, it starts an event correlation operation right at 12:05:31. and then when the fourth event drops in at 12:05:39, literally all the threshold conditions (thresh=2 and thresh2=2) get satisfied, which means the operation builds this event group string out of the first four events:

2 2 1 1

but bestie 😭 this string does not match the regular expression

1 2$

that got handed over with the egpattern field, so the operation is not gonna execute the action list from the action field. nope, not happening.

then the fifth event shows up at 12:05:42, all threshold conditions are satisfied again, and now every single observed event gives us this event group string:

2 2 1 1 2

and this one does match the regex from egpattern, so the operation writes the string "Sequence of two or more As and Bs with 'A B' at the end" to standard output with the write action. slay πŸ’…


so if the rule definition has an optional action list set with the countK field for event eK, the operation runs it every single time an instance of eK is observed β€” and yes, that's even if multact is set to No and the operation has already executed the action list given with action. no cap. and if that action list has match variables in it, they get substituted before each execution with values from matching the current instance of eK. every time, fresh. πŸ”₯

if the rule definition has an optional action list set with the init field, the operation executes it right away β€” like, immediately after the operation has been created. newborn action list energy πŸ‘‘

if the rule definition has an optional action list set with the end field, the operation executes it immediately before the operation finishes. but heads up 🚨 β€” this action list is not executed when the operation is terminated with the reset action. so like, reset is the one exception, don't come for me.

if the rule definition has an optional action list set with the slide field, the operation executes it immediately after the event correlation window has slidden forward. however πŸ‘€ β€” moving the window with the setwpos action will not trigger the execution. sneakily different, ik.

also note, bestie: when the event correlation window slides forward, the event group pattern (the one given with the egpattern field) and the numeric threshold conditions (given with thresh* fields) will not be evaluated for checking whether the action list given with the action field should be executed. so the slide is like, its own separate lil universe. 🌸

✨ Examples ✨

okay the following example rule cross-correlates iptables events, Apache web server access log messages with 4xx response codes, and SSH login failure events. it's giving detective energy πŸ•΅οΈβ€β™€οΈ:

type=EventGroup3
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (?:invalid user )?\S+ from ([\d.]+)
port \d+ ssh2
thresh=2
ptype2=RegExp
pattern2=^([\d.]+) \S+ \S+ \[.+?\] ".+? HTTP\/[\d.]+" 4\d+
thresh2=3
ptype3=RegExp
pattern3=kernel: iptables:.* SRC=([\d.]+)
thresh3=5
desc=Repeated probing from host $1
action=pipe '%t: %s' /bin/mail root@localhost
window=120

so this rule starts an event correlation operation for an IP address if an SSH login failure event, an iptables event, or an Apache 4xx event is observed for that IP address. and the operation sends an email warning to root@localhost if within 120 seconds three threshold conditions are satisfied for the IP address it tracks:

  1. at least two SSH login failure events have occurred for this client IP
  2. at least three Apache 4xx events have occurred for this client IP
  3. at least five iptables events have been observed for this source IP

so picture this β€” suppose the following events occur, and every event timestamp reflects the time SEC observes the event:

192.168.1.104 - - [05/Jan/2014:01:11:22 +0200] "GET /test.html HTTP/1.1"
404 286 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
Jan  5 01:12:52 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=48422 DF PROTO=TCP
SPT=46351 DPT=21 WINDOW=29200 RES=0x00 SYN URGP=0
Jan  5 01:12:53 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=48423 DF PROTO=TCP
SPT=46351 DPT=21 WINDOW=29200 RES=0x00 SYN URGP=0
Jan  5 01:13:01 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=20048 DF PROTO=TCP
SPT=44963 DPT=23 WINDOW=29200 RES=0x00 SYN URGP=0
Jan  5 01:13:02 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=20049 DF PROTO=TCP
SPT=44963 DPT=23 WINDOW=29200 RES=0x00 SYN URGP=0
Jan  5 01:13:08 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=36362 DF PROTO=TCP
SPT=56918 DPT=25 WINDOW=29200 RES=0x00 SYN URGP=0
Jan  5 01:13:09 localhost kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:8e:a1:3a:00:1d:e0:7e:89:b1:08:00 SRC=192.168.1.104
DST=192.168.1.107 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=36363 DF PROTO=TCP
SPT=56918 DPT=25 WINDOW=29200 RES=0x00 SYN URGP=0
192.168.1.104 - - [05/Jan/2014:01:13:51 +0200] "GET /test.html HTTP/1.1"
404 286 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
192.168.1.104 - - [05/Jan/2014:01:13:54 +0200] "GET /test.html HTTP/1.1"
404 286 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
192.168.1.104 - - [05/Jan/2014:01:14:00 +0200] "GET /login.html
HTTP/1.1" 404 287 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
192.168.1.104 - - [05/Jan/2014:01:14:03 +0200] "GET /login.html
HTTP/1.1" 404 287 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
192.168.1.104 - - [05/Jan/2014:01:14:03 +0200] "GET /login.html
HTTP/1.1" 404 287 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"
Jan  5 01:14:11 localhost sshd[1810]: Failed password for root from
192.168.1.104 port 46125 ssh2
Jan  5 01:14:12 localhost sshd[1810]: Failed password for root from
192.168.1.104 port 46125 ssh2
Jan  5 01:14:18 localhost sshd[1822]: Failed password for root from
192.168.1.104 port 46126 ssh2
Jan  5 01:14:19 localhost sshd[1822]: Failed password for root from
192.168.1.104 port 46126 ssh2
192.168.1.104 - - [05/Jan/2014:01:14:34 +0200] "GET /test.html HTTP/1.1"
404 286 "-" "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:26.0)
Gecko/20100101 Firefox/26.0"

so the Apache 4xx event at 01:11:22 starts an event correlation operation for 192.168.1.104, which has the event correlation window of 120 seconds β€” so like, it's ending at 01:13:22. between 01:12:52 and 01:13:09, six iptables events appear for 192.168.1.104, and the appearance of the fifth event at 01:13:08 fulfills the third threshold condition (within 120 seconds, at least five iptables events have been observed). βœ…

but since by 01:13:22 (the end of the event correlation window) no additional events have occurred, the first and second threshold conditions stay unsatisfied. so the beginning of the event correlation window gets moved to 01:12:52 (the occurrence time of the earliest event which is at most 120 seconds old). as a result, the end of the window moves from 01:13:22 to 01:14:52. the only event left outside the window is the Apache 4xx event at 01:11:22, and so the threshold condition for iptables events stays satisfied. ✨

between 01:13:51 and 01:14:03, five Apache 4xx events occur, and the appearance of the third event at 01:14:00 fulfills the second threshold condition (within 120 seconds, at least three Apache 4xx events have been observed). these events are followed by four SSH login failure events which occur between 01:14:11 and 01:14:19. the appearance of the second event at 01:14:12 fulfills the first threshold condition (within 120 seconds, at least two SSH login failure events have been observed). and since at this particular moment (01:14:12) the other two conditions are also fulfilled, the operation sends an email warning about 192.168.1.104 to root@localhost. πŸŽ‰ after that, the operation silently consumes all further matching events for 192.168.1.104 until 01:14:52, and then terminates. so chill of it honestly.

please note πŸ‘€ that if the above rule definition had a multact=yes statement, the operation would keep sending emails at each matching event after 01:14:12, as long as all threshold conditions are satisfied. so the operation would send three additional emails at 01:14:18, 01:14:19, and 01:14:34. and also, the operation would not terminate after its window ends at 01:14:52 β€” it would rather slide the window forward and expect new events. at the occurrence of any iptables, SSH login failure or Apache 4xx event for 192.168.1.104, the operation would produce a warning email if all threshold conditions are fulfilled. it simply does not stop. icon behavior. πŸ’…

the following example rule cross-correlates iptables events and SSH login events:

type=EventGroup3
ptype=regexp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from ([\d.]+) port \d+ ssh2
varmap= user=1; ip=2
count=alias OPER_$+{ip} LOGIN_FAILED_$+{user}_$+{ip}
ptype2=regexp
pattern2=sshd\[\d+\]: Accepted .+ for (\S+) from ([\d.]+) port \d+ ssh2
varmap2= user=1; ip=2
context2=LOGIN_FAILED_$+{user}_$+{ip}
ptype3=regexp
pattern3=kernel: iptables:.* SRC=([\d.]+)
varmap3= ip=1
desc=Client $+{ip} accessed a firewalled port and had difficulties with
logging in
action=pipe '%t: %s' /bin/mail root@localhost
init=create OPER_$+{ip}
slide=delete OPER_$+{ip}; reset 0
end=delete OPER_$+{ip}
window=120

so the rule starts an event correlation operation for an IP address if an SSH login failure or an iptables event was observed for that IP address. the operation exists for 120 seconds (since when the event correlation window slides forward, the operation terminates itself with the reset action as specified with the slide field). and the operation sends an email warning to root@localhost if within 120 seconds three threshold conditions are satisfied for the IP address it tracks:

  1. at least one iptables event has been observed for this source IP
  2. at least one SSH login failure has been observed for this client IP
  3. at least one successful SSH login has been observed for this client IP and for some user, provided that the operation has previously observed an SSH login failure for the same user and same client IP

so basically it's like, "you fumbled the login and then you got in? sus." 🧐

now suppose the following events occur, and each event timestamp reflects the time SEC observes the event:

...and it's giving "let me show you with actual receipts" ✨ so here's the log that goes with it:

Dec 27 19:00:06 test kernel: iptables: IN=eth0 OUT=
MAC=00:13:72:8a:83:d2:00:1b:25:07:e2:1b:08:00 SRC=10.1.2.7 DST=10.2.5.5
LEN=60 TOS=0x00 PREC=0x00 TTL=62 ID=1881 DF PROTO=TCP SPT=34342 DPT=23
WINDOW=5840 RES=0x00 SYN URGP=0
Dec 27 19:00:14 test sshd[10520]: Accepted password for root from
10.1.2.7 port 52609 ssh2
Dec 27 19:00:24 test sshd[10526]: Failed password for risto from
10.1.2.7 port 52622 ssh2
Dec 27 19:00:27 test sshd[10526]: Accepted password for risto from
10.1.2.7 port 52622 ssh2

okay so walk with me πŸšΆβ€β™€οΈ the iptables event at 19:00:06 kicks off an event correlation operation for 10.1.2.7, and that operation has the event correlation window of 120 seconds. Immediately after the operation has been started, it creates the context OPER_10.1.2.7. The second event at 19:00:14 does not match the rule, since the context LOGIN_FAILED_root_10.1.2.7 does not exist. The third event at 19:00:24 matches the rule, and the operation which is running for 10.1.2.7 sets up the alias name LOGIN_FAILED_risto_10.1.2.7 for the context OPER_10.1.2.7. Finally, the fourth event at 19:00:27 matches the rule, since the context LOGIN_FAILED_risto_10.1.2.7 exists, and the event is therefore processed by the operation (the presence of the context indicates that the operation has previously observed a login failure for user risto from 10.1.2.7). At this particular moment (19:00:27), all three threshold conditions for the operation are fulfilled, and therefore it sends an e-mail warning about 10.1.2.7 to root@localhost. After that, the operation silently consumes all further matching events for 10.1.2.7 until 19:02:06, and then terminates. Immediately before termination, the operation deletes the context OPER_10.1.2.7 which also drops its alias name LOGIN_FAILED_risto_10.1.2.7. πŸ’…

The following example rule correlates SSH login failure events:

type=EventGroup
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from ([\d.]+) port \d+ ssh2
desc=SSH login failures from three different hosts within 1m for user $1
egtoken=$2
egptype=PerlFunc
egpattern=sub { my(%hosts) = map { $_ => 1 } @{$_[1]}; \
                return scalar(keys %hosts) >= 3; }
action=pipe '%t: %s' /bin/mail root@localhost
window=60
thresh=3

The rule runs event correlation operations for counting the number of SSH login failure events. Each operation counts events for one user name, and if the operation has observed login failures from three different hosts within 60 seconds, it sends an e-mail warning to root@localhost. For verifying that hosts are different, the egtoken field configures the use of host IP addresses as tokens for building the event group string. Also, the Perl function provided with the egpattern field checks if the list of tokens contains at least three unique IP addresses. Note that the list of tokens is referenced by the second input parameter ($_[1]) of the function, while the first input parameter ($_[0]) holds the event group string (the above Perl function only processes the list of tokens). 🫢

Suppose the following events are generated by an SSH daemon, and each event timestamp reflects the time SEC observes the event:

Apr  4 23:04:00 test sshd[21137]: Failed password for risto from
10.1.1.7 port 32182 ssh2
Apr  4 23:04:03 test sshd[21145]: Failed password for risto from
10.1.1.9 port 42176 ssh2
Apr  4 23:04:04 test sshd[21212]: Failed password for risto from
10.1.1.7 port 34191 ssh2
Apr  4 23:04:07 test sshd[21226]: Failed password for risto from
10.1.1.2 port 18999 ssh2

When the first event is observed at 23:04:00, a counting operation is started for user risto. Since at 23:04:04 the operation observes the third event, the threshold condition given with the thresh field becomes satisfied, and thus the event group pattern given with the egpattern field is evaluated. However, the list of tokens (10.1.1.7, 10.1.1.9, 10.1.1.7) contains only two unique elements, and therefore the event group pattern does not match. When the fourth event occurs at 23:04:07, event group pattern is evaluated again, and since the list of tokens (10.1.1.7, 10.1.1.9, 10.1.1.7, 10.1.1.2) contains three unique elements now, the event group pattern matches and the operation will send an e-mail warning to root@localhost. πŸ”₯


πŸ›‘οΈ SUPPRESS RULE

The Suppress rule supports the following fields. Note that match variables may be used in the context field. ✨

  • type β€” fixed to Suppress (value is case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap β€” (optional) variable map.
  • context β€” (optional) context expression.
  • desc β€” (optional) string for describing the rule.
  • rem β€” (optional, may appear more than once) remarks and comments.

The Suppress rule takes no action when an event has matched the rule, and keeps matching events from being processed by later rules in the configuration file. 🚫

Note that the Suppress rule does not start event correlation operations, and the optional desc field is merely used for describing the rule. Also, in order to end event processing, so that no further rules from any of the configuration files would be tried, use the Jump rule. πŸ’…

Examples:

type=Suppress
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for \S+ from ([\d.]+) port \d+ ssh2
context=SUPPRESS_IP_$1

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from ([\d.]+) port \d+ ssh2
desc=Three SSH login failures within 1m for user $1 from $2
action=pipe '%t: %s' /bin/mail root@localhost; \
       create SUPPRESS_IP_$2 3600
window=60
thresh=3

The first rule filters out SSH login failure events for an already reported source IP address, so that they will not be matched against the second rule during 3600 seconds after sending an e-mail warning. πŸŽ€


πŸ—“οΈ CALENDAR RULE

The Calendar rule supports the following fields.

  • type β€” fixed to Calendar (value is case insensitive).
  • time β€” time specification.
  • context β€” (optional) context expression.
  • desc β€” operation description string.
  • action β€” action list.
  • rem β€” (optional, may appear more than once) remarks and comments.

The Calendar rule was designed for executing actions at specific times. Unlike all other rules, this rule reacts only to the system clock, ignoring other input. ⏰ The Calendar rule executes the action list given with the action field if the current time matches all conditions of the time specification given with the time field. The action list is executed only once for any matching minute.

The rule employs a time specification which closely resembles the crontab(1) style, but there are some subtle differences. πŸ‘€ The time specification consists of five or six conditions separated by whitespace. The first condition matches minutes (allowed values are 0-59), the second condition matches hours (allowed values are 0-23), the third condition days (allowed values are 0-31, with 0 denoting the last day of the month), the fourth condition months (allowed values are 1-12), and the fifth condition weekdays (allowed values are 0-7, with 0 and 7 denoting Sunday). The sixth condition is optional and matches years (allowed values are 0-99 which denote the last two digits of the year).

Asterisks (*), ranges of numbers (e.g., 8-11), and lists (e.g., 2,5,7-9) are allowed as conditions. Asterisks and ranges may be augmented with step values (e.g., 47-55/2 means 47,49,51,53,55). πŸ’«

Note that unlike crontab(1) time specification, the day and weekday conditions are not joined with logical OR, but rather with logical AND. Therefore, 0 1 25-31 10 7 means 1AM on last Sunday in October. On the other hand, with crontab(1) the same specification means 1AM in every last seven days or every Sunday in October.

Also, unlike some versions of cron(8), SEC is not restricted to take action only during the first second of the current minute. For example, if SEC is started at the 22th second of a minute, the wildcard condition produces a match for this minute. As another example, if the time specification matches the current minute but the context expression evaluates FALSE during the first half of the minute, the Calendar rule will execute the action list in the middle of this minute when the expression value becomes TRUE. πŸ•°οΈ

Note that the Calendar rule does not start event correlation operations, and the desc field is merely used for setting the %s action list variable.

Examples:

type=Calendar
time=0 2 25-31 3,12 6
desc=Check if backup is done on last Saturday of Q1 and Q4
action=event WAITING_FOR_BACKUP

type=Calendar
time=0 2 24-30 6,9 6
desc=Check if backup is done on last Saturday of Q2 and Q3
action=event WAITING_FOR_BACKUP

type=PairWithWindow
ptype=SubStr
pattern=WAITING_FOR_BACKUP
desc=Quarterly backup not completed on time!
action=pipe '%t: %s' /bin/mail root@localhost
ptype2=SubStr
pattern2=BACKUP READY
desc2=Quarterly backup successfully completed
action2=none
window=1800

The first two rules create a synthetic event WAITING_FOR_BACKUP at 2AM on last Saturday of March, June, September and December. The third rule matches this event and starts an event correlation operation which waits for the BACKUP READY event for 1800 seconds. If this event has not arrived by 2:30AM, the operation sends an e-mail warning to root@localhost. πŸ˜­πŸ‘‘


🦘 JUMP RULE

The Jump rule supports the following fields. Note that match variables may be used in the context field. They may also be used in the cfset field, provided that the constset field is set to No. ✨

  • type β€” fixed to Jump (value is case insensitive).
  • continue β€” (optional) TakeNext, DontCont, EndMatch or GoTo <label> (apart from <label>, values are case insensitive).
  • ptype β€” pattern type (value is case insensitive).
  • pattern β€” pattern.
  • varmap β€” (optional) variable map.
  • context β€” (optional) context expression.
  • cfset β€” (optional) configuration file set names that are separated by whitespace.
  • constset β€” (optional) Yes or No (values are case insensitive, default is Yes).
  • desc β€” (optional) string for describing the rule.
  • rem β€” (optional, may appear more than once) remarks and comments.

So like, the Jump rule grabs matching events and yeets them over to specific ruleset(s) for further processing πŸ’« If the event matches the rule, SEC keeps on hunting for matching rules inside the configuration file set(s) you gave it with the cfset field. Rules from every file get tried in the order they show up in the file, no skipping, no reordering, fr fr. And where do those configuration file sets even come from? From Options rules with the joincfset field, bestie β€” each set holding at least one configuration file. If you hand over more than one set name with cfset, the sets get processed left to right; a matching rule in one set does not stop SEC from processing the sets that come after. If the constset field is set to Yes, set names are treated as constants and won't be searched for match variables at runtime.

Now the spicy part 🌢️ If cfset isn't there and continue is set to GoTo, you can use the Jump rule to skip rules inside the current configuration file. If both cfset and continue are missing (or continue is set to DontCont), Jump is literally identical to a Suppress rule. And finally, if cfset is missing and continue is set to EndMatch, processing of the matching event just ends β€” like, no further rules from any of the configuration files will be tried. Door closed, lights off, periodt πŸ’…

Heads up bestie: the Jump rule does not start event correlation operations, and that optional desc field is purely for describing the rule. That's it, that's all it does.

Examples, come look πŸ“:

type=Jump
ptype=RegExp
pattern=sshd\[\d+\]:
cfset=sshd-rules auth-rules

So when an sshd syslog message pops up in the input, rules from configuration files of the set sshd-rules are used first for matching the message, and then rules from the configuration file set auth-rules get tried. Two rounds, no cap.


πŸŽ€ OPTIONS RULE πŸŽ€

The Options rule supports the following fields, keep up 🫢

  • type β€” fixed to Options (the value is case insensitive, so don't stress about your shift key).
  • joincfset (optional) β€” configuration file set names, separated by whitespace.
  • procallin (optional) β€” Yes or No (values are case insensitive, default is Yes).
  • rem (optional, and it may appear more than once) β€” remarks and comments.

The Options rule sets processing options for the ruleset in the current configuration file. If more than one Options rule is hanging out in the configuration file, the last instance overrides all the previous ones β€” last one wins, that's the law πŸ‘‘ Note that the Options rule is only processed when SEC (re)starts and reads in the configuration file. And since this rule isn't applied at runtime, it can never match events, react to the system clock, or start event correlation operations. It's giving "behind the scenes only" energy ✨

The joincfset field lists the names of one or more configuration file sets, and the current configuration file gets added to each set. If a set doesn't exist yet, it will be created and the current configuration file becomes its first member β€” founder status πŸ’– If the procallin field is set to No, the rules from the configuration file will be used for matching input from Jump rules only.

Examples, because we love a demo 🌈:

The following rule adds the current configuration file to the set sshd-rules which is used for matching input from Jump rules only:

type=Options
joincfset=sshd-rules
procallin=no

The following rule adds the current configuration file to sets linux and solaris which are used for matching all input:

type=Options
joincfset=linux solaris

✨ EVENT CORRELATION OPERATIONS ✨

okay bestie this is the part where sec gets actually magical πŸ’« Event correlation operations are these dynamic little entities that get created by rules. Like, a rule makes an operation, and then the rule keeps feeding that operation events it needs to correlate. And since one single rule can spawn and feed so many operations all running at the same time, every operation needs its own unique ID. no sharing. no clashing. πŸ’…

So how does SEC tell them apart? SEC assigns an ID to every operation, and that ID is built from three things: the configuration file name, the rule number, and the operation description string (which comes from the desc field of the rule). If there are N rules in the config file (not counting Options rules), then the rule numbers live in the range 0..N-1, and the number of the k-th rule is k-1. Why are Options rules excluded? Because each Options rule only gets processed when SEC reads in the configuration file and is never applied at runtime, so the Options rules never receive rule numbers. 🫢 And since the config file name and the rule number are both part of the operation ID, different rules can totally have identical desc fields with zero danger of a clash between operations. iconic behavior honestly.


πŸŽ€ a lil example, because vibes

Say the configuration file /etc/sec/my.conf contains only one rule:

type=SingleWithThreshold
ptype=RegExp
pattern=user (\S+) login failure on (\S+)
desc=Repeated login failures for user $1 on $2
action=pipe '%t: %s' /bin/mail root@localhost
window=60
thresh=3

Then the number of this rule is 0. Now when this rule matches an input event user admin login failure on tty1, the desc field yields an operation description string of Repeated login failures for user admin on tty1, and the event gets directed for further processing to the operation with the following ID:

/etc/sec/my.conf | 0 | Repeated login failures for user admin on tty1

If the operation for this ID doesn't exist yet, the rule will create it. And the newly created operation? Its event counter is initialized to 1, and it's expecting to receive two more user admin login failure on tty1 events from the rule within the following 60 seconds. Every time the operation receives such an event, its event counter gets incremented, and if that counter reaches the value of 3, a warning e-mail is sent to root@localhost. πŸŽ‰


🌸 tuning the desc changes everything

By tuning the desc field of the rule, you can change the scope of individual event correlation operations. For instance, if the following events occur within 10 seconds:

user admin login failure on tty1
user admin login failure on tty5
user admin login failure on tty2

then the above rule starts three event correlation operations. But if you change the desc field of the rule to Repeated login failures for user $1, then those events get processed by the same event correlation operation instead (and that operation sends a warning e-mail to root@localhost when it receives the third event). one operation, three events, slay.


πŸ¦‹ rule ordering matters, lowkey

Since rules from the same configuration file are matched against input in the order they're given, rule ordering influences the creation and feeding of event correlation operations. Suppose the configuration file /etc/sec/my.conf contains these rules:

type=Suppress
ptype=TValue
pattern=TRUE
context=MYCONTEXT

type=SingleWithThreshold
ptype=RegExp
pattern=user (\S+) login failure on (\S+)
desc=Repeated login failures for user $1 on $2
action=pipe '%t: %s' /bin/mail root@localhost
window=60
thresh=3

The second rule is able to create and feed event correlation operations as long as the context MYCONTEXT does not exist. But once MYCONTEXT has been created, no input event will reach the second rule, and so the rule is unable to create new operations and unable to feed existing ones with events. rip. 😭


πŸ’œ Pair and PairWithWindow can feed several operations

Not me forgetting to mention this β€” Pair and PairWithWindow rules can feed the same event to several operations. Suppose the configuration file /etc/sec/my2.conf contains the following rules:

type=Suppress
ptype=SubStr
pattern=test

type=Pair
ptype=RegExp
pattern=database (\S+) down
desc=Database $1 is down
action=pipe '%t: %s' /bin/mail root@localhost
ptype2=RegExp
pattern2=database $1 up|all databases up
desc2=Database %1 is up
action2=pipe '%t: %s' /bin/mail root@localhost
window=86400

Since the following input events don't contain the substring "test":

database mydb1 down
database mydb2 down
database mydb3 down

they get matched by the second rule of type Pair, which creates three event correlation operations. Each operation runs for one particular database name, and the operations have these IDs:

/etc/sec/my2.conf | 1 | Database mydb1 is down
/etc/sec/my2.conf | 1 | Database mydb2 is down
/etc/sec/my2.conf | 1 | Database mydb3 is down

Each newly created operation sends an e-mail notification to root@localhost about the "database down" condition, and will then wait for 86400 seconds (24 hours) for either of the following messages:

  1. a "database up" message for the given database
  2. an "all databases up" message

The operation with the ID

/etc/sec/my2.conf | 1 | Database mydb1 is down

uses this regular expression for matching expected messages:

database mydb1 up|all databases up

The operation with the ID

/etc/sec/my2.conf | 1 | Database mydb2 is down

employs this regular expression for matching expected messages:

database mydb2 up|all databases up

And finally, the operation with the ID

/etc/sec/my2.conf | 1 | Database mydb3 is down

uses this regular expression:

database mydb3 up|all databases up

🧚 what happens next, bestie

If the following input events appear after 10 minutes:

database test up
admin logged in
database mydb3 up
all databases up

then the first event "database test up" matches the first rule (Suppress), which does not pass the event further to the second rule (Pair). But all the following events do reach the Pair rule. And since those messages don't match the pattern field of the rule, the rule feeds them to all currently existing operations it has created, so the operations can match these events with their own regular expressions. Because the regular expressions of all three operations don't match the event "admin logged in", the operations just keep running. πŸ’…

Now for the "database mydb3 up" event β€” the regular expression of the operation

/etc/sec/my2.conf | 1 | Database mydb3 is down

produces a match. So that operation sends the e-mail notification "Database mydb3 is up" to root@localhost and terminates. But then the following event "all databases up" matches the regular expressions of two remaining operations. As a result, those operations send e-mail notifications "Database mydb1 is up" and "Database mydb2 is up" to root@localhost and terminate. two birds, one event, we love to see it. 🐦🐦


⭐ windows, moving them, and killing operations

Every operation has an event correlation window, and that window defines its scope in time. The size of the window is defined by the window field, and you can grab the beginning of the window with the getwpos action. SingleWithThreshold, SingleWith2Thresholds and EventGroup operations can slide their window forward during event processing, while for all operations the window can also be moved explicitly with the setwpos action. Also, with the reset action, event correlation operations can be terminated. πŸ“Œ Note that getwpos, setwpos, and reset actions only work for operations started by the rules from the same configuration file. iykyk.


πŸ‘‘ the sshd example (a classic)

For example, consider the configuration file /etc/sec/sshd.rules that contains the following rules:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=Three SSH login failures within 1m for user $1
action=pipe '%t: %s' /bin/mail root@localhost
window=60
thresh=3

type=Single
ptype=RegExp
pattern=sshd\[\d+\]: Accepted .+ for (\S+) from [\d.]+ port \d+ ssh2
desc=SSH login successful for user $1
action=reset -1 Three SSH login failures within 1m for user $1

Suppose the following events are generated by an SSH daemon, and each event timestamp reflects the time SEC observes the event:

Dec 29 15:00:03 test sshd[14129]: Failed password for risto from 10.1.2.7 port 31312 ssh2
Dec 29 15:00:08 test sshd[14129]: Failed password for risto from 10.1.2.7 port 31312 ssh2
Dec 29 15:00:17 test sshd[14129]: Accepted password for risto from 10.1.2.7 port 31312 ssh2
Dec 29 15:00:52 test sshd[14142]: Failed password for risto from 10.1.1.2 port 17721 ssh2

The first event at 15:00:03 starts an event correlation operation with the ID

/etc/sec/sshd.rules | 0 | Three SSH login failures within 1m for user risto

But when the third event occurs at 15:00:17, the second rule matches it and terminates the operation with the action

reset -1 Three SSH login failures within 1m for user risto

The -1 parameter of reset restricts the action to operations started by the previous rule (i.e., the first rule that has a number 0), while the Three SSH login failures within 1m for user risto parameter refers to the operation description string. Together with the current configuration file name (/etc/sec/sshd.rules), the parameters yield the operation ID

/etc/sec/sshd.rules | 0 | Three SSH login failures within 1m for user risto
πŸ™ˆ If the operation with the given ID would not exist, reset would perform no operation.

As a consequence, the fourth event at 15:00:52 starts another operation with the same ID as the terminated operation had. And without the second rule? The operation that was started at 15:00:03 would not be terminated, and the appearance of the fourth event would trigger a warning e-mail from that operation. so yeah, that second rule is doing a lot of heavy lifting. πŸ’ͺ


✨ INPUT PROCESSING AND TIMING ✨

okay bestie, this is the part where we talk about the actual heartbeat of SEC, like how it eats input and when it does stuff πŸ’–

SEC processes input data iteratively β€” read one line per iteration, write that line into the relevant input buffer, then match the content of the updated buffer against rules from the configuration files. that's the loop, that's the vibe πŸ”

and here's a lil detail that matters: if during the matching process an action list gets executed that creates new input events (say, through the event action), those events are not written to buffer(s) immediately. nope. they get consumed at the following iterations. queued up, not skipped ahead πŸŽ€

also note: when both synthetic events and regular input are available for processing, synthetic events are always consumed first. when all synthetic events have been consumed iteratively, SEC will start processing new data from input files. line, meet queue πŸ’…


🫢 the joint buffer situation

With the --jointbuf option, SEC employs a joint input buffer for all input sources which holds N last input lines (N is whatever you set with --bufsize). updating the input buffer means the new line becomes the first element of the buffer, while the last element (the oldest line) gets yeeted from the end of the buffer 🌸

With the --nojointbuf option, SEC maintains a buffer of N lines for each input file, and if the input line comes from file F, the buffer of F is updated as described previously. there's also a separate buffer for synthetic and internal events. everyone gets their own lil container πŸ«™

Suppose SEC is started with the following command line:

/usr/bin/sec --conf=/etc/sec/test-multiline.conf --jointbuf \
             --input=/var/log/prog1.log --input=/var/log/prog2.log

and the configuration file /etc/sec/test-multiline.conf has the following content:

type=Single
rem=this rule matches two consecutive lines where the first \
    line contains "test1" and the second line "test2", and \
    writes the matching lines to standard output
ptype=RegExp2
pattern=test1.*\n.*test2
desc=two consecutive test lines
action=write - $0

When the following lines appear in input files /var/log/prog1.log and /var/log/prog2.log:

Dec 31 12:33:12 test prog1: test1 (file /var/log/prog1.log)
Dec 31 12:34:09 test prog2: test1 (file /var/log/prog2.log)
Dec 31 12:39:35 test prog1: test2 (file /var/log/prog1.log)
Dec 31 12:41:53 test prog2: test2 (file /var/log/prog2.log)

they are stored in a common input buffer. therefore, the rule fires after the third event has appeared, and writes the following lines to standard output:

Dec 31 12:34:09 test prog2: test1 (file /var/log/prog2.log)
Dec 31 12:39:35 test prog1: test2 (file /var/log/prog1.log)

However, if SEC is started with the --nojointbuf option, separate input buffers are set up for /var/log/prog1.log and /var/log/prog2.log. therefore, the rule fires after the third event has occurred, and writes the following lines to standard output:

Dec 31 12:33:12 test prog1: test1 (file /var/log/prog1.log)
Dec 31 12:39:35 test prog1: test2 (file /var/log/prog1.log)

The rule also fires after the fourth event has occurred, producing the following output:

Dec 31 12:34:09 test prog2: test1 (file /var/log/prog2.log)
Dec 31 12:41:53 test prog2: test2 (file /var/log/prog2.log)

see how the same rules just behave different depending on the buffer sitch? iconic honestly πŸ‘‘


πŸ’… rewriting buffers mid-iteration (yes really)

The content of input buffers can be modified with the rewrite action, and modifications become visible immediately during the ongoing event processing iteration. no waiting around. instant glow-up ✨

Suppose SEC is started with the following command line:

/usr/bin/sec --conf=/etc/sec/test-rewrite.conf \
             --input=- --nojointbuf

and the configuration file /etc/sec/test-rewrite.conf has the following content:

type=Single
rem=this rule matches two consecutive lines where the first \
    line contains "test1" and the second line "test2", and \
    joins these lines in the input buffer
ptype=RegExp2
pattern=^(.*test1.*)\n(.*test2.*)$
continue=TakeNext
desc=join two test lines
action=rewrite 2 Joined $1 and $2

type=Single
rem=this rule matches a line which begins with "Joined", \
    and writes this line to standard output
ptype=RegExp
pattern=^Joined
desc=output joined lines
action=write - $0

When the following two lines appear in standard input:

This is a test1
This is a test2

they are matched by the first rule which uses the rewrite action for replacing those two lines in the input buffer with new content. the last line in the input buffer (This is a test2) is replaced with Joined This is a test1 and This is a test2, while the previous line in the input buffer (This is a test1) is replaced with an empty string 🫧

since the rule contains continue=TakeNext statement, the matching process will continue from the following rule. this rule matches the last line in the input buffer if it begins with Joined, and writes the line to standard output, producing:

Joined This is a test1 and This is a test2

🧠 the pattern match cache (and its lil lifespan)

After each event processing iteration, the pattern match cache is cleared. in other words, if a match is cached with the rule varmap* field, it is available during the ongoing iteration only. blink and it's gone πŸ’€

but wait, there's lore: results from a successful pattern matching are also cached when the subsequent context expression evaluation yields FALSE. this allows for reusing results from partial rule matches. for example, the following rule creates the cache entry ssh_failed_login for any SSH failed login event, even if the context ALERTING_ON does not exist:

type=Single
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (\S+) from ([\d.]+) port \d+ ssh2
varmap=ssh_failed_login; user=1; ip=2
context=ALERTING_ON
desc=SSH login failure for user $1 from $2
action=pipe '%s' /bin/mail -s 'SSH login alert' root@localhost

However, provided the context expression does not contain match variables, enclosing the expression in square brackets (e.g., [ALERTING_ON]) forces its evaluation before the pattern matching, and will thus prevent the matching and the creation of the cache entry if the evaluation yields FALSE. order of operations queen πŸ™ˆ


🌈 which rules run in which order

Rules from the same configuration file are matched against the buffer content in the order they are given in that file. when multiple configuration files have been specified, rule sequences from all files are matched against the buffer content (unless specified otherwise with Options rules). the matching order is determined by the order of configuration files in the SEC command line πŸ“‹

For example, if the Perl glob() function returns filenames in ascending ASCII order, and configuration files /home/risto/A.conf, /home/risto/B.conf2, and /home/risto/C.conf are specified with --conf=/home/risto/*.conf --conf=/home/risto/*.conf2 in SEC command line, then SEC first matches the input against the rule sequence from A.conf, then from C.conf, and finally from B.conf2.

also, note that even if A.conf contains a Suppress rule for a particular event, the event is still processed by rulesets in C.conf and B.conf2. not me getting suppressed and still showing up anyway 😭

However, note that glob() might return file names in different order if locale settings change. if you want to enforce a fixed order for configuration file application in a portable way, it is recommended to create a unique set for each file with the Options rule, and employ the Jump rule for defining the processing order for sets, e.g.:

# This rule appears in A.conf
type=Options
joincfset=FileA
procallin=no

# This rule appears in B.conf2
type=Options
joincfset=FileB
procallin=no

# This rule appears in C.conf
type=Options
joincfset=FileC
procallin=no

# This rule appears in main.conf
type=Jump
ptype=TValue
pattern=TRUE
cfset=FileA FileC FileB

see? deterministic order. we love a structured bestie πŸŽ€


⏱️ after the matching: signals, children, and cleanup

After the relevant input buffer has been updated and its content has been matched by the rules, SEC handles caught signals and checks the status of child processes. when the timeout specified with the --cleantime option has expired, SEC also checks the status of contexts and event correlation operations 🌸

therefore, relatively small values should be specified with the --cleantime option, in order to retain the accuracy of the event correlation process. if the --cleantime option is set to 0, SEC checks event correlation operations and contexts after processing every input line, but this consumes more CPU time. pick your poison, bestie πŸ’…

if the --poll-timeout option value exceeds the value given with --cleantime, the --poll-timeout option value takes precedence (i.e., sleeps after unsuccessful polls will not be shortened). noted, no cap πŸ“


Finally, note that apart from the sleeps after unsuccessful polls, SEC measures all time intervals and occurrence times in seconds, and always uses the time(2) system call for obtaining the current time. Also, for input event occurrence time SEC always uses the time it observed the event, not the timestamp extracted from the event.

so if your log line says it happened at 3am but SEC saw it at 3:07am, it's going with 3:07am. trust the observer, not the narrator ✨

✨ INTERNAL EVENTS AND CONTEXTS ✨

okay so bestie, inside a context's action list you can ALSO refer to the context by its internal name _THIS. πŸ’– SEC creates and deletes the name _THIS on the fly, and it only points at the context during that context's action list execution. This is so useful when your context has been out here living multiple lives under multiple names (courtesy of the alias action) and you genuinely cannot tell which names still exist when the context expires. 😭

like, say the context gets created with create A 60 (report A /bin/mail root), and right after that comes alias A B and then unalias A. The report action will fail, because the name A doesn't refer to the context anymore (it's giving ghosted ✨). But swap that first action for create A 60 (report _THIS /bin/mail root) and boom β€” correct result. 🫢


If you pass the --intevents command line option, SEC will generate internal events when it starts up, when it catches certain signals, and when it terminates normally. Inside SEC, an internal event is treated exactly like a line that was read from a SEC input file. So you can write specific rules to match internal events and do stuff with them (like kicking off an external event correlation module with spawn right when SEC starts up β€” main character energy tbh πŸ‘‘). These are the internal events that are supported:

  • SEC_STARTUP β€” generated when SEC is started. This event will always be the first event that SEC sees. πŸ’…
  • SEC_PRE_RESTART β€” generated before processing the SIGHUP signal. This event will be the last event that SEC sees before clearing all internal data structures and reloading its configuration.
  • SEC_RESTART β€” generated after processing the SIGHUP signal. This event will be the first event that SEC sees after clearing all internal data structures and reloading its configuration.
  • SEC_PRE_SOFTRESTART β€” generated before processing the SIGABRT signal. This event will be the last event that SEC sees before reloading its configuration.
  • SEC_SOFTRESTART β€” generated after processing the SIGABRT signal. This event will be the first event that SEC sees after reloading its configuration.
  • SEC_PRE_LOGROTATE β€” generated before processing the SIGUSR2 signal. This event will be the last event that SEC sees before reopening its log file and closing its outputs.
  • SEC_LOGROTATE β€” generated after processing the SIGUSR2 signal. This event will be the first event that SEC sees after reopening its log file and closing its outputs.
  • SEC_SHUTDOWN β€” generated when SEC receives the SIGTERM signal, or when SEC reaches all EOFs of input files after being started with the --notail option. With the --childterm option, SEC sleeps for 3 seconds after generating the SEC_SHUTDOWN event, and then sends SIGTERM to its child processes (if a child process was triggered by SEC_SHUTDOWN, this delay leaves the process enough time for setting a signal handler for SIGTERM).

Before generating an internal event, SEC sets up a context named SEC_INTERNAL_EVENT, so it can disambiguate internal events from regular input. The SEC_INTERNAL_EVENT context is deleted immediately after the internal event has been matched against all rules. no lingering, we don't do that here 🧚


If the --intcontexts command line option is given, or there's an --input option with a context specified, SEC creates an internal context every single time it reads a line from an input file or a synthetic event. That internal context gets deleted immediately after the line has been matched against all rules. For all input files that have the context name explicitly set with --input=<file_pattern>=<context>, the name of the internal context is <context>. If the line was read from the input file <filename> for which there is no context name set, the name of the internal context is _FILE_EVENT_<filename>. 🌸

For synthetic events, the name of the internal context defaults to _INTERNAL_EVENT, but cspawn and cevent actions can be used for generating synthetic events with custom internal context names. This is what lets you write rules that match data from one particular input source only. iykyk 😌

For example, this rule:

type=Suppress
ptype=TValue
pattern=TRUE
context=[!_FILE_EVENT_/dev/logpipe]

passes only the lines that were read from /dev/logpipe, and also synthetic events that were generated with the _FILE_EVENT_/dev/logpipe internal context (e.g., with the action cevent _FILE_EVENT_/dev/logpipe 0 This is a test event).

As another example, if SEC has been started with the command line

/usr/bin/sec --intevents --intcontexts --conf=/etc/sec/my.conf \
             --input=/var/log/messages=MESSAGES \
             --input=/var/log/secure=SECURE \
             --input=/var/log/cron=CRON

and the rule file /etc/sec/my.conf contains the following rules

type=Single
ptype=RegExp
pattern=^(?:SEC_STARTUP|SEC_RESTART)$
context=[SEC_INTERNAL_EVENT]
desc=listen on 10514/tcp for incoming events
action=cspawn MESSAGES /usr/bin/nc -l -k 10514

type=Single
ptype=RegExp
pattern=.
context=[MESSAGES]
desc=echo everything from 10514/tcp and /var/log/messages
action=write - $0

then SEC will receive input lines from the log files /var/log/messages, /var/log/secure, and /var/log/cron, and will also run /usr/bin/nc for receiving input lines from the port 10514/tcp. All input lines from /var/log/messages and 10514/tcp are matched by the second rule and written to standard output. ate that up fr fr πŸ”₯

πŸ‘Ά CHILD PROCESSES πŸ’…

okay bestie so this is the part where SEC gets ✨friends✨ β€” like, actual processes. Let's talk about who forks what and why it matters, because this is lowkey the part that can bite you fr fr.

The SingleWithScript rule and the shellcmd, spawn, cspawn, cmdexec, spawnexec, cspawnexec, pipe, pipeexec, report, and reportexec actions all fork a child process to run an external program. Here's the tea on shell parsing:

  • For the SingleWithScript rule with shell=yes and for the shellcmd, spawn, cspawn, pipe, and report actions: if the program command line contains shell metacharacters, the command line gets parsed by the shell first, and then the shell starts the program. 🐚
  • For the SingleWithScript rule with shell=no and for the cmdexec, spawnexec, cspawnexec, pipeexec, and reportexec actions: the program command line is not parsed by shell, even if shell metacharacters are sitting right there in the command line. πŸ™…β€β™€οΈ

🚨 why you'd wanna disable shell parsing (a cautionary tale)

Disabling shell parsing for command lines can be useful for avoiding unwanted side effects. Like, picture this badly written rule for sending an e-mail to a local user when 10 SSH login failures have been observed for that user from the same IP address during 300 seconds:

type=SingleWithThreshold
ptype=RegExp
pattern=sshd\[\d+\]: Failed .+ for (.+) from ([\d.]+) port \d+ ssh2
desc=Failed SSH logins for user $1 from $2
action=pipe 'Failed SSH logins from $2' /bin/mail -s alert $1
window=300
thresh=10

ngl bestie, that rule is a whole security incident waiting to happen. It allows for the execution of arbitrary command lines with the privileges of the SEC process. 😭 For example, consider this malicious command line that provides fake input events for the rule:

logger -p authpriv.info -t sshd -i 'Failed password for `/usr/bin/touch
/tmp/test` from 127.0.0.1 port 12345 ssh2'

When this command line is repeatedly executed, the attacker is able to trigger the execution of the command line /bin/mail -s alert /usr/bin/touch /tmp/test``. BUT β€” and here's the plot twist β€” that command line is parsed by shell, which triggers the execution of the command line the attacker specified: /usr/bin/touch /tmp/test. πŸ’€

So how do you fix it? Swap the pipe action for pipeexec, which disables the shell parsing:

action=pipeexec 'Failed SSH logins from $2' /bin/mail -s alert $1
However β€” and I cannot stress this enough β€” when using that approach, you must make sure that the data passed to an external program is handled without any unexpected side effects. No cap. 🫢

As another workaround, the regular expression pattern of the rule can be modified to match user names that do not contain shell metacharacters, like:

pattern=sshd\[\d+\]: Failed .+ for ([\w.-]+) from ([\d.]+) port \d+ ssh2

🍼 how SEC actually talks to its children

SEC communicates with its child processes through pipes (created with the pipe(2) system call). When the child process is at the read end of the pipe, data have to be written to the pipe in blocking mode, which ensures reliable data transmission. In order to avoid being blocked, SEC forks another SEC process for writing data to the pipe reliably. That newly created SEC process then forks the child process, managing the child process on behalf of the main SEC process (i.e., the main SEC process is the grandparent process for the child). πŸ‘‘

So like, for example: if the SEC process that manages the child receives the SIGTERM signal, the signal will be forwarded to the child process, and when the child process terminates, its exit code will be reported to the main SEC process. it's giving family tree energy 🌳

After forking an external program, SEC continues immediately, and checks the program status periodically until the program exits. The running time of a child process is not limited in any way β€” no timeout, no curfew, it does what it wants. ⏰

With the --childterm option, SEC sends the SIGTERM signal to all child processes when it terminates. If some special exit procedures need to be accomplished in the child process (or the child wishes to ignore SIGTERM), then the child must install a handler for the SIGTERM signal.

⚠️ Note that if the program command line is parsed by shell, the parsing shell will run as a child process of SEC and the parent process of the program. Therefore, the SIGTERM signal will be sent to the shell, not the program. πŸ’€

In order to avoid this, the shell's builtin exec command can be used (see sh(1) for more information) which replaces the shell with the program without forking a new process, e.g.

action=spawn exec /usr/local/bin/myscript.pl 2>/var/log/myscript.log

⏳ async order, or: why your actions don't run in the order you wrote them

Note that if an action list includes two actions which fork external programs, the execution order of these programs is not determined by the order of actions in the list, since both programs are running asynchronously. πŸ™ˆ In order to address this issue, the execution order must be specified explicitly β€” e.g., instead of writing action=shellcmd cmd1; shellcmd cmd2, use the shell && operator and write action=shellcmd cmd1 && cmd2. periodt. πŸ’…


πŸŽ€ feeding one program from many rules (named pipes era)

Sometimes it is desirable to start an external program and provide it with data from several rules. In order to create such a setup, named pipes can be harnessed. For example, if /var/log/pipe is a named pipe, then

action=shellcmd /usr/bin/logger -f /var/log/pipe -p user.notice

starts the /usr/bin/logger utility, which sends all lines read from /var/log/pipe to the local syslog daemon with the "user" facility and "notice" level. In order to feed events to /usr/bin/logger, the write action can be used (e.g., write /var/log/pipe This is my event).

Although SEC keeps the named pipe open across different write actions, the pipe will be closed on the reception of SIGHUP, SIGABRT and SIGUSR2 signals. Since many UNIX tools terminate on receiving EOF from standard input, they need restarting after such signals have arrived. For this purpose, the --intevents option and SEC internal events can be used. For example, the following rule starts the /usr/bin/logger utility at SEC startup, and also restarts it after the reception of relevant signals:

type=Single
ptype=RegExp
pattern=^(?:SEC_STARTUP|SEC_RESTART|SEC_SOFTRESTART|SEC_LOGROTATE)$
context=SEC_INTERNAL_EVENT
desc=start the logger tool
action=free %emptystring; owritecl /var/log/pipe %emptystring; \
       shellcmd /usr/bin/logger -f /var/log/pipe -p user.notice

Note that if /var/log/pipe is never opened for writing by a write action, /usr/bin/logger will never see EOF and will thus not terminate. The owritecl action opens and closes /var/log/pipe without writing any bytes, in order to ensure the presence of EOF in such cases. This allows any previous /usr/bin/logger process to terminate before the new process is started. 🌈✨

✨ PERL INTEGRATION ✨

okay bestie, this is the part where SEC stops being just a rules engine and starts being a whole lifestyle, because SEC supports patterns, context expressions, and actions which involve calls to the Perl eval() function or the execution of precompiled Perl code. πŸ’… The use of Perl code in SEC patterns and context expressions allows for creating proper match conditions for scenarios which can't be handled by a simple regular expression match. no cap, sometimes a regex just isn't the moment.

for example, consider the following iptables syslog events:

May 27 10:00:15 box1 kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:be:9e:2f:00:10:db:ff:20:03:08:00 SRC=10.6.4.14 DST=10.1.8.2
LEN=84 TOS=0x00 PREC=0x00 TTL=251 ID=61426 PROTO=ICMP TYPE=8 CODE=0
ID=11670 SEQ=2
May 27 10:02:22 box1 kernel: iptables: IN=eth0 OUT=
MAC=08:00:27:be:9e:2f:00:10:db:ff:20:03:08:00 SRC=10.6.4.14 DST=10.1.8.2
LEN=52 TOS=0x00 PREC=0x00 TTL=60 ID=61441 DF PROTO=TCP SPT=53125 DPT=23
WINDOW=49640 RES=0x00 SYN URGP=0

depending on the protocol and the nature of the traffic, events can have a wide variety of fields, and parsing out all event data with one regular expression is infeasible. like, literally not the vibe. for addressing this issue, a PerlFunc pattern can be used which creates match variables from all fields of the matching event, stores them in one Perl hash, and returns a reference to this hash. outside the PerlFunc pattern, match variables are initialized from the key-value pairs in the returned hash. 🫢

suppose the following Jump rule with a PerlFunc pattern is defined in the main.rules rule file:

type=Jump
ptype=PerlFunc
pattern=sub { my(%var); my($line) = $_[0]; \
        if ($line !~ /kernel: iptables:/g) { return 0; } \
        while ($line =~ /\G\s*([A-Z]+)(?:=(\S*))?/g) { \
          $var{$1} = defined($2)?$2:1; \
        } return \%var; }
varmap=IPTABLES
desc=parse iptables event
cfset=iptables

for example, if the iptables event contains the fields SRC=10.6.4.14, DST=10.1.8.2 and SYN, the above PerlFunc pattern sets up match variable $+{SRC} which holds 10.6.4.14, match variable $+{DST} which holds 10.1.8.2, and match variable $+{SYN} which holds 1. ✨ the Jump rule caches all created match variables under the name IPTABLES, and submits the matching event to iptables ruleset for further processing. suppose the iptables ruleset is defined in the iptables.rules rule file:

type=Options
procallin=no
joincfset=iptables

type=SingleWithThreshold
ptype=Cached
pattern=IPTABLES
context=IPTABLES :> ( sub { return $_[0]->{"PROTO"} eq "ICMP"; } )
desc=ICMP flood type $+{TYPE} code $+{CODE} from host $+{SRC}
action=logonly
window=10
thresh=100

type=SingleWithThreshold
ptype=Cached
pattern=IPTABLES
context=IPTABLES :> ( sub { return exists($_[0]->{"SYN"}) && \
                                   exists($_[0]->{"FIN"}) ; } )
desc=SYN+FIN flood from host $+{SRC}
action=logonly
window=10
thresh=100

the two SingleWithThreshold rules employ Cached patterns for matching iptables events by looking up the IPTABLES entry in the pattern match cache (created by the above Jump rule for each iptables event, iykyk). in order to narrow down the match to specific iptables events, the rules employ precompiled Perl functions in context expressions. the :> operator is used for speeding up the matching, providing the function with a single parameter which refers to the hash of variable name-value pairs for the IPTABLES cache entry. πŸ”₯

the first SingleWithThreshold rule logs a warning message if within 10 seconds 100 iptables events have been observed for ICMP packets with the same type, code, and source IP address. the second SingleWithThreshold rule logs a warning message if within 10 seconds 100 iptables events have been observed for TCP packets coming from the same host, and having both SYN and FIN flag set in each packet. πŸ’€ iconic behaviour honestly.


apart from using action list variables for data sharing between rules, Perl variables created in Perl code can be employed for the same purpose. for example, when SEC has executed the following action

action=eval %a ($b = 1)

the variable $b and its value become visible in the following context expression

context= =(++$b > 10)

(with that expression one can implement event counting implicitly). 🧚 in order to avoid possible clashes with variables inside the SEC code itself, user-defined Perl code is executed in the main::SEC namespace (i.e., inside the special package main::SEC). by using the main:: prefix, SEC data structures can be accessed and modified. for example, the following rules restore and save contexts with names MY_* on SEC startup and shutdown, using Perl Storable module for saving and restoring relevant elements of %main::context_list hash (since the following example does not handle code references with Storable module, it is assumed that context action lists do not contain lcall actions):

type=Single
ptype=SubStr
pattern=SEC_STARTUP
context=SEC_INTERNAL_EVENT
continue=TakeNext
desc=Load the Storable module and terminate if it is not found
action=eval %ret (require Storable); \
       if %ret ( logonly Storable loaded ) else ( eval %o exit(1) )

type=Single
ptype=SubStr
pattern=SEC_STARTUP
context=SEC_INTERNAL_EVENT
desc=Restore contexts MY_* from /var/lib/sec/SEC_CONTEXTS on startup
action=lcall %ret -> ( sub { my($ref, $context); \
       $ref = Storable::retrieve("/var/lib/sec/SEC_CONTEXTS"); \
       foreach $context (keys %{$ref}) { \
         if ($context =~ /^MY_/) \
           { $main::context_list{$context} = $ref->{$context}; } } } )

type=Single
ptype=SubStr
pattern=SEC_SHUTDOWN
context=SEC_INTERNAL_EVENT
desc=Save contexts MY_* into /var/lib/sec/SEC_CONTEXTS on shutdown
action=lcall %ret -> ( sub { my($context, %hash); \
       foreach $context (keys %main::context_list) { \
         if ($context =~ /^MY_/) \
           { $hash{$context} = $main::context_list{$context}; } } \
       Storable::store(\%hash, "/var/lib/sec/SEC_CONTEXTS"); } )

however, note that modifying data structures within SEC code is recommended only for advanced users who have carefully studied relevant parts of the code. πŸ‘‘ like, do not just start poking the internals because you feel brave.

finally, sometimes larger chunks of Perl code have to be used for event processing and correlation. however, writing many lines of code directly into a rule is cumbersome and may decrease its readability. in such cases it is recommended to separate the code into a custom Perl module which is loaded at SEC startup, and use the code through the module interface (see perlmod(1) for further details):

type=Single
ptype=SubStr
pattern=SEC_STARTUP
context=SEC_INTERNAL_EVENT
desc=Load the SecStuff module
action=eval %ret (require '/usr/local/sec/SecStuff.pm'); \
       if %ret ( none ) else ( eval %o exit(1) )

type=Single
ptype=PerlFunc
pattern=sub { return SecStuff::my_match($_[0]); }
desc=event '$0' was matched by my_match()
action=write - %s

and that's the whole modular bestie pipeline: load the module at startup, call it from a PerlFunc pattern, let my_match() do its little dance. πŸ¦‹

πŸ’… Example 1 β€” a ruleset for Cisco events πŸ¦‹

okay so first up, a whole example rulebase for wrangling Cisco devices ✨. the assumption here is that your managed devices have syslog logging turned on, and that every syslog message gets shipped to one central host and written into log file(s) that SEC is watching. no cap, that's the setup 🫢

# Set up contexts NIGHT and WEEKEND for nights
# and weekends. The context NIGHT has a lifetime
# of 8 hours and the context WEEKEND 2 days

type=Calendar
time=0 23 * * *
desc=NIGHT
action=create %s 28800

type=Calendar
time=0 0 * * 6
desc=WEEKEND
action=create %s 172800

# If a router does not come up within 5 minutes
# after it was rebooted, generate event
# "<router> REBOOT FAILURE". The next rule matches
# this event, checks the router with ping and sends
# a notification if there is no response.

type=PairWithWindow
ptype=RegExp
pattern=\s([\w.-]+) \d+: %SYS-5-RELOAD
desc=$1 REBOOT FAILURE
action=event %s
ptype2=RegExp
pattern2=\s$1 \d+: %SYS-5-RESTART
desc2=%1 successful reboot
action2=logonly
window=300

type=SingleWithScript
ptype=RegExp
pattern=^([\w.-]+) REBOOT FAILURE
script=/bin/ping -c 3 -q $1
desc=$1 did not come up after reboot
action=logonly $1 is pingable after reboot
action2=pipe '%t: %s' /bin/mail root@localhost

# Send a notification if CPU load of a router is too
# high (two CPUHOG messages are received within 5
# minutes); send another notification if the load is
# normal again (no CPUHOG messages within last 15
# minutes). Rule is not active at night or weekend.

type=SingleWith2Thresholds
ptype=RegExp
pattern=\s([\w.-]+) \d+: %SYS-3-CPUHOG
context=!(NIGHT || WEEKEND)
desc=$1 CPU overload
action=pipe '%t: %s' /bin/mail root@localhost
window=300
thresh=2
desc2=$1 CPU load normal
action2=pipe '%t: %s' /bin/mail root@localhost
window2=900
thresh2=0

# If a router interface is in down state for less
# than 15 seconds, generate event
# "<router> INTERFACE <interface> SHORT OUTAGE";
# otherwise generate event
# "<router> INTERFACE <interface> DOWN".

type=PairWithWindow
ptype=RegExp
pattern=\s([\w.-]+) \d+: %LINK-3-UPDOWN: Interface ([\w.-]+), changed
state to down
desc=$1 INTERFACE $2 DOWN
action=event %s
ptype2=RegExp
pattern2=\s$1 \d+: %LINK-3-UPDOWN: Interface $2, changed state to up
desc2=%1 INTERFACE %2 SHORT OUTAGE
action2=event %s
window=15

# If "<router> INTERFACE <interface> DOWN" event is
# received, send a notification and wait for
# "interface up" event from the same router interface
# for the next 24 hours

type=Pair
ptype=RegExp
pattern=^([\w.-]+) INTERFACE ([\w.-]+) DOWN
desc=$1 interface $2 is down
action=pipe '%t: %s' /bin/mail root@localhost
ptype2=RegExp
pattern2=\s$1 \d+: %LINK-3-UPDOWN: Interface $2, changed state to up
desc2=%1 interface %2 is up
action2=pipe '%t: %s' /bin/mail root@localhost
window=86400

# If ten "short outage" events have been observed
# in the window of 6 hours, send a notification

type=SingleWithThreshold
ptype=RegExp
pattern=^([\w.-]+) INTERFACE ([\w.-]+) SHORT OUTAGE
desc=Interface $2 at node $1 is unstable
action=pipe '%t: %s' /bin/mail root@localhost
window=21600
thresh=10

like… that's the whole glow-up from raw syslog lines to actual alerts in your inbox πŸ’– read the comments in there, they literally explain every single rule block. i'm obsessed.


🌈 Example 2 β€” hierarchically organized rulesets for iptables and sshd events πŸ‘‘

now THIS one is giving main character energy fr fr. it's an example of rules organized in a hierarchy, built to process Linux iptables events coming out of /var/log/messages, plus SSH login events coming out of /var/log/secure 🌸

the assumptions, bestie: all rule files live in the /etc/sec directory, and the rule hierarchy has two levels ✨

  • the file /etc/sec/main.rules holds the first-level Jump rules β€” those match and parse events from the input files and hand them off to the right rulesets for further processing πŸŽ€
  • all the other rule files in /etc/sec hold second-level rules, which get their input from those first-level Jump rules πŸ’«

and also β€” the example assumes SEC got started with this exact command line:

/usr/bin/sec --conf=/etc/sec/*.rules --intcontexts \
             --input=/var/log/messages --input=/var/log/secure

πŸ“ the content of /etc/sec/main.rules

#
# the content of /etc/sec/main.rules
#

type=Jump
context=[ _FILE_EVENT_/var/log/messages ]
ptype=PerlFunc
pattern=sub { my(%var); my($line) = $_[0]; \
        if ($line !~ /kernel: iptables:/g) { return 0; } \
        while ($line =~ /\G\s*([A-Z]+)(?:=(\S*))?/g) { \
          $var{$1} = defined($2)?$2:1; \
        } return \%var; }
varmap=IPTABLES
desc=parse iptables events and direct to relevant ruleset
cfset=iptables

type=Jump
context=[ _FILE_EVENT_/var/log/secure ]
ptype=RegExp
pattern=sshd\[(?<pid>\d+)\]: (?<status>Accepted|Failed) \
(?<authmethod>[\w-]+) for (?<invuser>invalid user )?\
(?<user>[\w-]+) from (?<srcip>[\d.]+) port (?<srcport>\d+) ssh2$
varmap=SSH_LOGIN
desc=parse SSH login events and direct to relevant ruleset
cfset=ssh-login

type=Jump
context=[ SSH_EVENT ]
ptype=TValue
pattern=True
desc=direct SSH synthetic events to relevant ruleset
cfset=ssh-events

🧚 the content of /etc/sec/iptables.rules

#
# the content of /etc/sec/iptables.rules
#

type=Options
procallin=no
joincfset=iptables

type=SingleWithThreshold
ptype=Cached
pattern=IPTABLES
context=IPTABLES :> ( sub { return exists($_[0]->{"SYN"}) && \
                                   exists($_[0]->{"FIN"}) ; } ) \
        && !SUPPRESS_IP_$+{SRC}
desc=SYN+FIN flood from host $+{SRC}
action=pipe '%t: %s' /bin/mail -s 'iptables alert' root@localhost; \
       create SUPPRESS_IP_$+{SRC} 3600
window=10
thresh=100

type=SingleWithThreshold
ptype=Cached
pattern=IPTABLES
context=IPTABLES :> ( sub { return exists($_[0]->{"SYN"}) && \
                                  !exists($_[0]->{"ACK"}) ; } ) \
        && !SUPPRESS_IP_$+{SRC}
desc=SYN flood from host $+{SRC}
action=pipe '%t: %s' /bin/mail -s 'iptables alert' root@localhost; \
       create SUPPRESS_IP_$+{SRC} 3600
window=10
thresh=100

πŸ’œ the content of /etc/sec/ssh-login.rules

#
# the content of /etc/sec/ssh-login.rules
#

type=Options
procallin=no
joincfset=ssh-login

type=Single
ptype=Cached
pattern=SSH_LOGIN
context=SSH_LOGIN :> ( sub { return $_[0]->{"status"} eq "Failed" && \
                                    $_[0]->{"srcport"} < 1024 && \
                                    defined($_[0]->{"invuser"}); } )
continue=TakeNext
desc=Probe of invalid user $+{user} from privileged port of $+{srcip}
action=pipe '%t: %s' /bin/mail -s 'SSH alert' root@localhost

type=SingleWithThreshold
ptype=Cached
pattern=SSH_LOGIN
context=SSH_LOGIN :> ( sub { return $_[0]->{"status"} eq "Failed" && \
                                    defined($_[0]->{"invuser"}); } )
desc=Ten login probes for invalid users from $+{srcip} within 60s
action=pipe '%t: %s' /bin/mail -s 'SSH alert' root@localhost
thresh=10
window=60

type=PairWithWindow
ptype=Cached
pattern=SSH_LOGIN
context=SSH_LOGIN :> ( sub { return $_[0]->{"status"} eq "Failed"; } )
desc=User $+{user} failed to log in from $+{srcip} within 60s
action=cevent SSH_EVENT 0 %s
ptype2=Cached
pattern2=SSH_LOGIN
context2=SSH_LOGIN :> \
           ( sub { return $_[0]->{"status"} eq "Accepted"; } ) && \
         $+{user} %+{user} $+{srcip} %+{srcip} -> \
           ( sub { return $_[0] eq $_[1]  &&  $_[2] eq $_[3]; }  )
desc2=User $+{user} logged in successfully from $+{srcip} within 60s
action2=logonly
window=60

πŸŽ€ the content of /etc/sec/ssh-events.rules

#
# the content of /etc/sec/ssh-events.rules
#

type=Options
procallin=no
joincfset=ssh-events

type=SingleWithThreshold
ptype=RegExp
pattern=User ([\w-]+) failed to log in from [\d.]+ within 60s
desc=Ten login failures for user $1 within 1h
action=pipe '%t: %s' /bin/mail -s 'SSH alert' root@localhost
thresh=10
window=3600

see how the Jump rules in main.rules just yeet events into iptables, ssh-login and ssh-events rulesets, and then each of those files does its own thing with joincfset? that's the hierarchy, babes. two levels, zero chaos ✨


🌸 ENVIRONMENT 🌸

if the SECRC environment variable is set, SEC takes it as the name of its resource file πŸ“„ and honestly it's so simple:

  • lines in the resource file that are empty, or that start with the number sign (#), get ignored β€” and yes, whitespace is allowed before that # πŸ’…
  • every other line gets appended to the argv array of SEC as a single element β€” one line, one element, no splitting πŸ™…
  • and they get appended in the order they appear in the resource file, in order, periodt ⭐

so here's the thing: if your command line option takes a value, the option name and the value have to be separated either by the equal sign (=) or by a newline. that's the rule, no exceptions 😭

here's a lil resource file example for you:

# read events from standard input
--input=-

# rules are stored in /etc/sec/test.conf
--conf
/etc/sec/test.conf
heads up bestie: even though SEC rereads its resource file when it receives the SIGHUP or SIGABRT signal, adding an option that specifies a certain startup procedure (like --pid or --detach) will not give you the desired effect at runtime πŸ’€ like, it just won't. read the room.

and one more thing β€” the resource file content is not parsed by a shell 🐚, so any shell metacharacters just get passed straight to SEC as-is. no expansion, no funny business, exactly as written πŸ‘‘

more coming your way in the next part, don't you dare go anywhere πŸ’–

🚨 SIGNALS 🚨

okay bestie, this is the part where you learn how to talk to SEC while it's running πŸ’¬ every one of these signals is basically a text you can send it, and SEC WILL respond. no ghosting. let's go 🫢

  • SIGHUP β€” full restart, and when we say full we mean FULL 😀 SEC will reinterpret its command line and resource file options, reopen its log and input files, close its output files and sockets (don't worry, those get reopened on demand πŸ’…), reload its configuration, and drop all event correlation state β€” and yes, that means all event correlation operations get terminated, all contexts get deleted, all action list variables get erased, etc. etc. etc. it's giving factory reset fr fr. oh and if you're running with the --childterm option, SEC will also send the SIGTERM signal to its child processes πŸ”₯
  • SIGABRT β€” soft restart, the chill version 😌 SEC will reinterpret its command line and resource file options, reopen its log file, and close its output files and sockets (again, reopened on demand ✨). If the --keepopen option is specified, previously opened input files will remain open across the soft restart β€” otherwise all input files will be reopened. SEC will (re)load configuration from rule files which have been modified (that's the file modification time returned by stat(2) changing, iykyk) or created after the previous configuration load. SEC will also terminate event correlation operations started from rule files that have been modified or removed after the previous configuration load. other operations and previously loaded configuration from unmodified rule files will remain intact πŸ’– note that on some systems SIGIOT is used in place of SIGABRT β€” not a typo, just a lil system quirk, periodt.
  • SIGUSR1 β€” spill the tea β˜• detailed information about the current state of SEC (performance and rule matching statistics, running event correlation operations, created contexts, etc.) will be written to the SEC dump file. it's literally SEC journalling for you πŸ““
  • SIGUSR2 β€” SEC will reopen its log file (super useful for log file rotation, bestie πŸŒ€), and also close its output files and sockets which will be reopened on demand.
  • SIGINT β€” SEC will increase its logging level by one; if the current level is 6, the level will be set back to 1 πŸ” please note this feature is available only if SEC is running non-interactively (e.g., in daemon mode) β€” so don't try this in interactive mode, it's just not the vibe.
  • SIGTERM β€” SEC will terminate gracefully πŸ•ŠοΈ and with the --childterm option, all SEC child processes will receive SIGTERM.

πŸ› BUGS πŸ›

okay so there's one lil oopsie πŸ™ˆ with some locale settings, single quotes (') in this man page might be displayed incorrectly. it's giving cursed energy, no cap.

as a workaround, set the LANG environment variable to C when reading this man page, like:

env LANG=C man sec

and boom, fixed πŸ’…


πŸ‘‘ AUTHOR πŸ‘‘

Risto Vaarandi (firstname d0t lastname at gmail d0t c0m)


🌈 ACKNOWLEDGMENTS 🌈

the author is grateful to TalTech and SEB Estonia for supporting this work πŸ’– the author also thanks the following people for supplying software patches, documentation fixes, and suggesting new features: Al Sorrell, Brian Mielke, David Lang, James Brown, Jon Frazier, Mark D. Nagel, Peter Eckel, Rick Casey, and William Gertz 🫢

and last but not least, the author expresses his profound gratitude to John P. Rouillard for many great ideas and creative discussions that have helped to develop SEC 🌸


πŸ” SEE ALSO πŸ”


and that's a wrap on sec(1) bestie!! πŸŽ€βœ¨

this iconic page is from SEC 2.9.4, dated February 2026 β€” you made it all the way to the end and honestly? main character energy πŸ’œ go forth and correlate those events, you absolute legend πŸ’…πŸ’–

πŸ“¦ where this page comes from