diff --git a/README.md b/README.md index 988cf3c..0188e88 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,55 @@ # reaction -a program that scans program outputs, such as logs, -for repeated patterns, such as failed login attempts, -and takes action, such as banning ips. +A daemon that scans program outputs for repeated patterns, and takes action. -(adapted from [ fail2ban ](http://fail2ban.org)'s presentation 😄) +A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. -🚧 this program hasn't received external audit. however, it already works well on my servers 🚧 +🚧 This program hasn't received external audit. however, it already works well on my servers 🚧 -## rationale +## Rationale -i was using fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption +I was using the honorable fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption and all its heavy default configuration. -in my view, a security-oriented program should be simple to configure (`sudo` is a very bad example!) -and an always-running daemon should be implemented in a fast language. +In my view, a security-oriented program should be simple to configure +and an always-running daemon should be implemented in a fast*er* language. -📽️ french example +reaction does not have all the features of the honorable fail2ban, but it's ~10x faster and has more manageable configuration. -## configuration +📽️ french quick explanation 😉 -this configuration file is all that should be needed to prevent brute force attacks on an ssh server. +## Configuration -see [ reaction.service ](./config/reaction.service) and [reaction.yml](./app/reaction.yml) for the fully explained examples. +YAML and [JSONnet](https://jsonnet.org/) (more powerful) are supported. +both are extensions of JSON, so JSON is transitively supported. + +- See [reaction.yml](./app/example.yml) or [reaction.jsonnet](./config/example.jsonnet) for a fully explained reference +- See [server.jsonnet](./config/server.jsonnet) for a real-world configuration +- See [reaction.service](./config/reaction.service) for a systemd service file +- This quick example shows what's needed to prevent brute force attacks on an ssh server: + +
+ +/etc/reaction.yml -`/etc/reaction.yml` ```yaml -definitions: - - &iptablesban [ "ip46tables" "-w" "-I" "reaction" "1" "-s" "" "-j" "block" ] - - &iptablesunban [ "ip46tables" "-w" "-D" "reaction" "1" "-s" "" "-j" "block" ] - patterns: ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' start: - - [ "ip46tables", "-w", "-N", "reaction" ] - - [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ] - - [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ] - - [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ] + - [ 'ip46tables', '-w', '-N', 'reaction' ] + - [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] stop: - - [ "ip46tables", "-w," "-D," "INPUT", "-p", "all", "-j", "reaction" ] - - [ "ip46tables", "-w," "-F," "reaction" ] - - [ "ip46tables", "-w," "-X," "reaction" ] + - [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-F', 'reaction' ] + - [ 'ip46tables', '-w', '-X', 'reaction' ] streams: ssh: - cmd: [ "journalctl" "-fu" "sshd.service" ] + cmd: [ 'journalctl', '-fu', 'sshd.service' ] filters: failedlogin: regex: @@ -55,36 +58,46 @@ streams: retryperiod: '6h' actions: ban: - cmd: *iptablesban + cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'block' ] unban: - cmd: *iptablesunban + cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'block' ] after: '48h' ``` -jsonnet is also supported: +
+ +
+ +/etc/reaction.jsonnet -`/etc/reaction.jsonnet` ```jsonnet local iptables(args) = [ 'ip46tables', '-w' ] + args; -local iptablesban(ip) = iptables([ '-A', 'reaction', '1', '-s', ip, '-j', 'DROP' ]); -local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DROP' ]); +local banFor(time) = { + ban: { + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), + }, + unban: { + after: time, + cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), + }, +}; { patterns: { ip: { regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', }, }, - start: { - iptables([ "-N", "reaction" ]), - iptables([ "-A", "reaction", "-j", "ACCEPT" ]), - iptables([ "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]), - iptables([ "-I", "INPUT", "-p", "all", "-j", "reaction" ]), - }, - stop: { - iptables([ "-D,", "INPUT", "-p", "all", "-j", "reaction" ]), - iptables([ "-F,", "reaction" ]), - iptables([ "-X,", "reaction" ]), - }, + start: [ + iptables([ '-N', 'reaction' ]), + iptables([ '-A', 'reaction', '-j', 'ACCEPT' ]), + iptables([ '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]), + iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]), + ], + stop: [ + iptables([ '-D,', 'INPUT', '-p', 'all', '-j', 'reaction' ]), + iptables([ '-F,', 'reaction' ]), + iptables([ '-X,', 'reaction' ]), + ], streams: { ssh: { cmd: [ 'journalctl', '-fu', 'sshd.service' ], @@ -93,16 +106,7 @@ local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DRO regex: [ @'authentication failure;.*rhost=' ], retry: 3, retryperiod: '6h', - actions: { - ban: { - cmd: iptablesban(''), - }, - unban: { - cmd: iptablesunban(''), - after: '48h', - onexit: true, - }, - }, + actions: banFor('48h'), }, }, }, @@ -110,57 +114,45 @@ local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DRO } ``` -note that both yaml and jsonnet are extensions of json, so json is also inherently supported. +
-`/etc/systemd/system/reaction.service` -```systemd -[ Unit ] -WantedBy=multi-user.target -[ Service ] -ExecStart=/path/to/reaction -c /etc/reaction.yml +### Database -StateDirectory=reaction -RuntimeDirectory=reaction -WorkingDirectory=/var/lib/reaction -``` +The embedded database is stored in the working directory. +If you don't know where to start reaction, `/var/lib/reaction` should be a sane choice. -### database +### CLI -the working directory of `reaction` will be used to create and read from the embedded database. -if you don't know where to start it, `/var/lib/reaction` should be a sane choice. - -### socket - -the socket allowing communication between the cli and server will be created at `/run/reaction/reaction.socket`. +- `reaction start` runs the server +- `reaction show` show pending actions (ie. bans) +- `reaction flush` permits to run pending actions (ie. clear bans) +- `reaction test-regex` permits to test regexes +- `reaction help` for full usage. ### `ip46tables` `ip46tables` is a minimal c program present in its own subdirectory with only standard posix dependencies. -it permits to configure `iptables` and `ip6tables` at the same time. -it will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6 and both if no ip address is present on the command line. +It permits to configure `iptables` and `ip6tables` at the same time. +It will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6 and both if no ip address is present on the command line. -### compilation +### Compilation -you'll need the go toolchain for reaction and a c compiler for ip46tables. +You'll need the go toolchain for reaction and a c compiler for ip46tables. ```shell $ make ``` -alternatively, +Alternatively, ```shell +# creates ./reaction $ go build . +# creates ./ip46tables $ gcc ip46tables.d/ip46tables.c -o ip46tables ``` -### nixos +### NixOS -in addition to the [ package ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/pkgs/reaction/default.nix) -and [ module ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction.nix) -that i didn't try to upstream to nixpkgs yet (although they are ready), i use extensively reaction on my servers. if you're using nixos, -consider reading and building upon [ my own building blocks ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction-variables.nix), -[ my own non-root reaction conf, including conf for SSH, port scanning & Nginx common attack URLS ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction-custom.nix), -and the configuration for [ nextcloud ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/file.ppom.me.nix#L53), -[ vaultwarden ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/vaultwarden.nix#L45), -and [ maddy ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/mail.nix#L74). see also an [example](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/mail.nix#L85) where it does something else than banning IPs. +- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix) +- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix) diff --git a/app/example.yml b/app/example.yml index 95a8220..2c00f23 100644 --- a/app/example.yml +++ b/app/example.yml @@ -3,8 +3,8 @@ # using YAML anchors `&name` and pointers `*name` # definitions are not readed by reaction definitions: - - &iptablesban [ "ip46tables", "-w", "-A", "reaction", "1", "-s", "", "-j", "DROP" ] - - &iptablesunban [ "ip46tables", "-w", "-D", "reaction", "1", "-s", "", "-j", "DROP" ] + - &iptablesban [ 'ip46tables', '-w', '-A', 'reaction', '1', '-s', '', '-j', 'DROP' ] + - &iptablesunban [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'DROP' ] # ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. # it permits to handle both ipv4/iptables and ipv6/ip6tables commands @@ -20,20 +20,20 @@ patterns: # Those commands will be executed in order at start, before everything else start: - - [ "ip46tables", "-w", "-N", "reaction" ] - - [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ] - - [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ] - - [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ] + - [ 'ip46tables', '-w', '-N', 'reaction' ] + - [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] # Those commands will be executed in order at stop, after everything else stop: - - [ "ip46tables", "-w,", "-D", "INPUT", "-p", "all", "-j", "reaction" ] - - [ "ip46tables", "-w", "-F", "reaction" ] - - [ "ip46tables", "-w", "-X", "reaction" ] + - [ 'ip46tables', '-w,', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-F', 'reaction' ] + - [ 'ip46tables', '-w', '-X', 'reaction' ] # streams are commands -# they're run and their ouptut is captured +# they are run and their ouptut is captured # *example:* `tail -f /var/log/nginx/access.log` # their output will be used by one or more filters streams: @@ -41,7 +41,7 @@ streams: ssh: # note that if the command is not in environment's `PATH` # its full path must be given. - cmd: [ "journalctl", "-n0", "-fu", "sshd.service" ] + cmd: [ 'journalctl', '-n0', '-fu', 'sshd.service' ] # filters run actions when they match regexes on a stream filters: # filters have a user-defined name @@ -73,7 +73,7 @@ streams: onexit: true # (defaults to false) # here it is not useful because we will flush the chain containing the bans anyway - # (see /conf/reaction.service) + # (with the stop commands) # persistence # tldr; when an `after` action is set in a filter, such filter acts as a 'jail', diff --git a/app/main.go b/app/main.go index ba4c437..e5cca74 100644 --- a/app/main.go +++ b/app/main.go @@ -118,13 +118,10 @@ func Main() { logger.Fatalln("No argument provided. Try `reaction help`") basicUsage() os.Exit(1) - } else if os.Args[1] == "-h" || os.Args[1] == "--help" { - basicUsage() - os.Exit(0) } f := flag.NewFlagSet(os.Args[1], flag.ExitOnError) switch os.Args[1] { - case "help", "-h", "--help": + case "help", "-h", "-help", "--help": basicUsage() case "example-conf": @@ -224,7 +221,7 @@ func Main() { } default: - logger.Fatalln("subcommand not recognized") + logger.Fatalf("subcommand %v not recognized. Try `reaction help`", os.Args[1]) basicUsage() os.Exit(1) } diff --git a/config/example.jsonnet b/config/example.jsonnet index 598fab1..6a7881b 100644 --- a/config/example.jsonnet +++ b/config/example.jsonnet @@ -3,11 +3,8 @@ // JSONNET is a superset of JSON, so one can write plain JSON files if wanted. // Note that YAML is also supported, see ./example.yml -// A JSONNET function +// JSONNET functions local iptables(args) = ['ip46tables', '-w'] + args; -// variables defined for later use. -local iptablesban = iptables(['-A', 'reaction', '1', '-s', '', '-j', 'drop']); -local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '', '-j', 'drop']); // ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. // it permits to handle both ipv4/iptables and ipv6/ip6tables commands @@ -48,7 +45,7 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '', '-j', 'drop ], // streams are commands - // they're run and their ouptut is captured + // they are run and their ouptut is captured // *example:* `tail -f /var/log/nginx/access.log` // their output will be used by one or more filters streams: { @@ -77,11 +74,10 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '', '-j', 'drop actions: { // actions have a user-defined name ban: { - // JSONNET substitutes the variable (defined at the beginning of the file) - cmd: iptablesban, + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), }, unban: { - cmd: iptablesunban, + cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), // if after is defined, the action will not take place immediately, but after a specified duration // same format as retryperiod after: '48h', @@ -90,7 +86,7 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '', '-j', 'drop onexit: true, // (defaults to false) // here it is not useful because we will flush the chain containing the bans anyway - // (see /conf/reaction.service) + // (with the stop commands) }, }, }, diff --git a/config/heavy-load.yml b/config/heavy-load.yml index a342a16..a63999a 100644 --- a/config/heavy-load.yml +++ b/config/heavy-load.yml @@ -9,7 +9,7 @@ patterns: streams: tailDown1: - cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo found $(($i % 100)); done" ] + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo found $(($i % 100)); done' ] filters: findIP: regex: @@ -18,13 +18,13 @@ streams: retryperiod: 1m actions: damn: - cmd: [ "echo", "" ] + cmd: [ 'echo', '' ] undamn: - cmd: [ "echo", "undamn", "" ] + cmd: [ 'echo', 'undamn', '' ] after: 1m onexit: false tailDown2: - cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo prout $(($i % 100)); done" ] + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo prout $(($i % 100)); done' ] filters: findIP: regex: @@ -33,13 +33,13 @@ streams: retryperiod: 1m actions: damn: - cmd: [ "echo", "" ] + cmd: [ 'echo', '' ] undamn: - cmd: [ "echo", "undamn", "" ] + cmd: [ 'echo', 'undamn', '' ] after: 1m onexit: false tailDown3: - cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done" ] + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ] filters: findIP: regex: @@ -48,13 +48,13 @@ streams: retryperiod: 2m actions: damn: - cmd: [ "true" ] + cmd: [ 'true' ] undamn: - cmd: [ "true" ] + cmd: [ 'true' ] after: 1m onexit: false tailDown4: - cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done" ] + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ] filters: findIP: regex: @@ -63,8 +63,8 @@ streams: retryperiod: 2m actions: damn: - cmd: [ "echo", "" ] + cmd: [ 'echo', '' ] undamn: - cmd: [ "echo", "undamn", "" ] + cmd: [ 'echo', 'undamn', '' ] after: 1m onexit: false diff --git a/config/reaction.service b/config/reaction.service index 5232252..1cc83b5 100644 --- a/config/reaction.service +++ b/config/reaction.service @@ -4,7 +4,7 @@ WantedBy=multi-user.target # See `man systemd.exec` and `man systemd.service` for most options below [Service] -ExecStart=/path/to/reaction -c /etc/reaction.yml +ExecStart=/path/to/reaction start -c /etc/reaction.yml # Ask systemd to create /var/lib/reaction (/var/lib/ is implicit) StateDirectory=reaction diff --git a/config/server.jsonnet b/config/server.jsonnet new file mode 100644 index 0000000..71ecd3d --- /dev/null +++ b/config/server.jsonnet @@ -0,0 +1,147 @@ +// This is the extensive configuration used on a **real** server! + +local banFor(time) = { + ban: { + cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse'], + }, + unban: { + after: time, + cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse'], + }, +}; + +{ + patterns: { + // IPs can be IPv4 or IPv6 + // ip46tables (C program also in this repo) handles running the good commands + ip: { + regex: @'(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})', + }, + }, + + streams: { + // Ban hosts failing to connect via ssh + ssh: { + cmd: [' journalctl', '-fn0', '-u', 'sshd.service'], + filters: { + failedlogin: { + regex: [ + @'authentication failure;.*rhost=', + @'Connection reset by authenticating user .* ', + ], + retry: 3, + retryperiod: '6h', + actions: banFor('48h'), + }, + }, + }, + + // Ban hosts which knock on closed ports. + // It needs this iptables chain to be used to drop packets: + // ip46tables -N log-refuse + // ip46tables -A log-refuse -p tcp --syn -j LOG --log-level info --log-prefix 'refused connection: ' + // ip46tables -A log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse + // ip46tables -A log-refuse -j DROP + kernel: { + cmd: ['journalctl', '-fn0', '-k'], + filters: { + portscan: { + regex: ['refused connection: .*SRC='], + retry: 4, + retryperiod: '1h', + actions: banFor('720h'), + }, + }, + }, + // Note: nextcloud and vaultwarden could also be filters on the nginx stream + // I did use their own logs instead because it's less logs to parse than the front webserver + + // Ban hosts failing to connect to Nextcloud + nextcloud: { + cmd: ['journalctl', '-fn0', '-u', 'phpfpm-nextcloud.service'], + filters: { + failedLogin: { + regex: [ + @'"remoteAddr":"".*"message":"Login failed:', + @'"remoteAddr":"".*"message":"Trusted domain error.', + ], + retry: 3, + retryperiod: '1h', + actions: banFor('1h'), + }, + }, + }, + + // Ban hosts failing to connect to vaultwarden + vaultwarden: { + cmd: ['journalctl', '-fn0', '-u', 'vaultwarden.service'], + filters: { + failedlogin: { + actions: banFor('2h'), + regex: [@'Username or password is incorrect\. Try again\. IP: \. Username:'], + retry: 3, + retryperiod: '1h', + }, + }, + }, + + // Used with this nginx log configuration: + // log_format withhost '$remote_addr - $remote_user [$time_local] $host "$request" $status $bytes_sent "$http_referer" "$http_user_agent"'; + // access_log /var/log/nginx/access.log withhost; + nginx: { + cmd: ['tail', '-n0', '-f', '/var/log/nginx/access.log'], + filters: { + // Ban hosts failing to connect to Directus + directus: { + regex: [ + @'^ .* "POST /auth/login HTTP/..." 401 [0-9]+ .https://directusdomain', + ], + retry: 6, + retryperiod: '4h', + actions: banFor('4h'), + }, + + // Ban hosts presenting themselves as bots of ChatGPT + gptbot: { + regex: [@'^.*GPTBot/1.0'], + action: banFor('720h'), + }, + + // Ban hosts failing to connect to slskd + slskd: { + regex: [ + @'^ .* "POST /api/v0/session HTTP/..." 401 [0-9]+ .https://slskddomain', + ], + retry: 3, + retryperiod: '1h', + actions: banFor('6h'), + }, + + // Ban suspect HTTP requests + // Those are frequent malicious requests I got from bots + // Make sure you don't have honnest use cases for those requests, or your clients may be banned for 2 weeks! + suspectRequests: { + regex: [ + // (?:[^/" ]*/)* is a "non-capturing group" regex that allow for subpath(s) + // example: /code/.env should be matched as well as /.env + // ^^^^^ + @'^.*"GET /(?:[^/" ]*/)*\.env ', + @'^.*"GET /(?:[^/" ]*/)*info\.php ', + @'^.*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ', + @'^.*"GET /(?:[^/" ]*/)*auth.html ', + @'^.*"GET /(?:[^/" ]*/)*auth1.html ', + @'^.*"GET /(?:[^/" ]*/)*password.txt ', + @'^.*"GET /(?:[^/" ]*/)*passwords.txt ', + @'^.*"GET /(?:[^/" ]*/)*dns-query ', + // Do not include this if you have a Wordpress website ;) + @'^.*"GET /(?:[^/" ]*/)*wp-login\.php', + @'^.*"GET /(?:[^/" ]*/)*wp-includes', + // Do not include this if a client must retrieve a config.json file ;) + @'^.*"GET /(?:[^/" ]*/)*config\.json ', + ], + action: banFor('720h'), + }, + }, + }, + }, +} diff --git a/config/test.jsonnet b/config/test.jsonnet index c3ccf97..468c831 100644 --- a/config/test.jsonnet +++ b/config/test.jsonnet @@ -39,7 +39,6 @@ }, tailDown2: { cmd: ['sh', '-c', 'echo coucou; sleep 2m'], - // cmd: ['sh', '-c', "echo 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 | tr ' ' '\n' | while read i; do sleep 3; echo found $(($i % 60)); done"], filters: { findIP: { regex: ['^found $'],