Filter
Filters run Actions when they Match regexes on a Stream.
Filters are reaction's main component, enclosing most of its runtime logic.
A Filter is attached to a Stream and receive its text stream as an input.
It applies one or more regexes to each line. When there is one or more Match in a given period, the Filter is Triggered and executes one or more Actions.
regex
A list of regexes to try matching on the input Stream.
Whenever one of them matches, a Match is created.
streams:
nginx:
...
filters:
unauthorized:
regex:
- '^<ip> .* HTTP/\d\.\d" 401'streams: {
nginx: {
...
filters: {
unauthorized: {
regex: [
'^<ip> .* HTTP/\d\.\d" 401',
],
},
},
},
}
Patterns can be referenced in the regexes by providing their name enclosed in < and >.
The Match will then contain the actual value matched by the Pattern and can be reused in Actions.
All regexes of a given Filter must include the exact same set of Patterns.
patterns:
ip:
type: 'ip'
streams:
nginx:
...
filters:
unauthorized:
regex:
- '<ip> .* HTTP/\d\.\d" 401'patterns: {
ip: {
type: 'ip',
},
},
streams: {
nginx: {
...
filters: {
unauthorized: {
regex: [
'^<ip> .* HTTP/\d\.\d" 401',
],
},
},
},
}
type
Available since v2.4.0.
Defaults to "regex", in which case a regex must be provided.
{
my_fitler: {
type: "regex",
regex: [...],
}
}my_fitler:
type: regex
regex: [...]
Otherwise, it can be "struct", in which case reaction will parse each line as a structured format, such as JSON.
options
Available since v2.4.0.
Used in conjunction with type: "struct".
options.lang
Must be set. Only "json" is available for now.
options.fields
This represents the fields we want to match in our JSON line.
It replaces the common regex option.
Each field can be matched with one of those options:
options.fields.<name>.regex
This field's value must match the regex. The regex can contain Patterns.
A regex can be matched against strings and numbers. Numbers will be converted to string.
{
my_filter: {
type: "struct",
options: {
lang: "json",
fields: {
# short form
ClientIP: "<ip>",
# long form
ClientIP: {
regex: "<ip>",
}
}
}
}
}my_filter:
type: struct
options:
lang: json
fields:
# short form
ClientIP: "<ip>"
# long form
ClientIP:
regex: "<ip>"
Example of matching line:
{"ClientIP":"203.0.113.57","DownstreamStatus":401}options.fields.<name>.equals
This field's value must match the given value.
The given value can be any JSON value: number, string, arrary...
{
my_filter: {
type: "struct",
options: {
lang: "json",
fields: {
RequestPath: {
equals: "/login",
},
DownstreamStatus: {
equals: 403,
},
ProxyChain: {
equals: ["http://10.0.0.2:443", "http://127.0.0.1:8709"],
},
}
}
}
}my_filter:
type: struct
options:
lang: json
fields:
RequestPath:
equals: "/login"
DownstreamStatus:
equals: 403
ProxyChain:
equals: ["http://10.0.0.2:443", "http://127.0.0.1:8709"]
Example of matching line:
{"ClientIP":"203.0.113.57","DownstreamStatus":403,"ProxyChain":["http://10.0.0.2:443","http://127.0.0.1:8709"],"RequestPath":"/login"}options.fields.<name>.exists
If true, the field must exist and its value must be non-null.
If false, the field must not exist, or its value must be null.
{
my_filter: {
type: "struct",
options: {
lang: "json",
fields: {
RejectReason: {
exist: true,
},
}
}
}
}my_filter:
type: struct
options:
lang: json
fields:
RejectReason:
exist: true
Example of matching lines:
{"ClientIP":"203.0.113.57","RejectReason":"invalid password"}
{"ClientIP":"203.0.113.57","RejectReason":true}
{"ClientIP":"203.0.113.57","RejectReason":false}
Example of non-matching lines:
{"ClientIP":"203.0.113.57","error":"invalid password"}
{"ClientIP":"203.0.113.57","RejectReason":null}
{"ClientIP":"203.0.113.57"}options.fields.<name>.<name>
It is possible to match on nested fields of arbitrary depth:
{
my_filter: {
type: "struct",
options: {
lang: "json",
fields: {
request: {
client_ip: "<ip>",
uri: {
equals: "/.env",
},
headers: {
"User-Agent": {
regex: ".*(ClaudeBot|GPTBot).*",
}
}
}
}
}
}
}my_filter:
type: struct
options:
lang: json
fields:
request:
client_ip: "<ip>"
uri:
equals: "/.env"
headers:
"User-Agent":
regex: ".*(ClaudeBot|GPTBot).*"retry
How many Matches must happen before the Filter Triggers its Actions.
If not specified, defaults to 1. If specified, must be > 1.
Must be specified along with retryperiod.
retryperiod
The retain period for Matches.
Must be specified along with retry.
Format is defined as follows:
<number> <unit>
- whitespace between the integer and unit is optional
- number must be a positive integer (>= 0, no floating point)
- unit can be one of:
ms/millis/millisecond/millisecondss/sec/secs/second/secondsm/min/mins/minute/minutesh/hour/hoursd/day/days
Examples:
{
streams:
stream1: {
filters: {
filter0: {
regex: [ ... ],
// no retry/retryperiod, trigger as soon as there is a match
},
filter1: {
regex: [ ... ],
// 2 matches in 10 seconds to trigger
retry: 2,
retryperiod: '10 secs',
},
filter2: {
regex: [ ... ],
// 4 matches in 30 minutes to trigger
retry: 4,
retryperiod: '30m',
},
filter3: {
regex: [ ... ],
// 3 matches in 1 day to trigger
retry: 3,
retryperiod: '1day',
},
},
},
},
}streams:
stream1:
filters:
filter0:
regex:
- ...
# no retry/retryperiod, trigger as soon as there is a match
filter1:
regex:
- ...
# 2 matches in 10 seconds to trigger
retry: 2,
retryperiod: '10 secs',
filter2:
regex:
- ...
# 4 matches in 30 minutes to trigger
retry: 4,
retryperiod: '30m',
filter3:
regex:
- ...
# 3 matches in 1 day to trigger
retry: 3,
retryperiod: '1day',duplicate
Available since v2.2.0.
Specify what reaction must do when the Filter matches an already Triggered Match.
3 behaviors are possible: extend, rerun and ignore.
Before v2.2.0, reaction's filters used to execute the same actions multiple times.
For example, when an IP address was spamming the server, due to the latency
between the moment the request was received and the moment the ban action was
executed by reaction, and made effective by the firewall, other logs
concerning this IP address could appear, resulting in a new trigger of the
reaction filter.
Therefore, an IP could be banned multiple times, resulting in bugs on some
firewalls that deduplicate IPs, like ipset and nftables.
The new behavior defaults to extending the trigger period.
Let's take this filter as an example:
{
regex: [ @'Failed password for .* from <ip>' ],
actions: {
ban: {
cmd: ['iptables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
},
unban: {
cmd: ['iptables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
after: '2h',
},
},
}regex: [ 'Failed password for .* from <ip>' ]
actions:
ban:
cmd: ['iptables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP']
unban:
cmd: ['iptables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP']
after: '2h'
If a filter is triggered at 0:00, the ban action runs, and the unban action is scheduled at 2:00.
If for some reason, some new matches appear at 0:10:
- reaction defaults to extending the
unban. So it won't schedule a newbancommand. It will instead reschedule the existingunbanaction to 2:10. This default behavior can be explicitly specified by:duplicate: 'extend' // the default - reaction can launch a new
bancommand, and schedule a newunbancommand at 2:10. This (old default) behavior is possible by adding this parameter:duplicate: 'rerun' - reaction can also simply ignore new matches for this IP, until the
unbancommand runs:duplicate: 'ignore'
Last example with duplicate:
{
duplicate: 'rerun',
regex: [ @'Failed password for .* from <ip>' ],
actions: {
ban: {
cmd: ['iptables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
},
unban: {
cmd: ['iptables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
after: '2h',
},
},
}duplicate: 'rerun'
regex:
- 'Failed password for .* from <ip>'
actions:
ban:
cmd: ['iptables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP']
unban:
cmd: ['iptables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP']
after: '2h'actions
We can attach one or more Actions to a Filter.
{
streams: {
...
filters: {
failedlogin: {
regex: [ ... ],
actions: {
...
},
},
connectionreset: {
regex: [ ... ],
actions: {
...
},
},
},
},
}streams:
...
filters:
failedlogin:
regex:
...
actions:
...
connectionreset:
regex:
...
actions:
...