From 2c59a9e9e26ed2d9ce99becb63501dfc9f00fd01 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:13:38 +0200 Subject: [PATCH] add JSON access log support to traefik-auth --- ChangeLog | 1 + config/filter.d/traefik-auth.conf | 95 ++++++++++++++++---------- fail2ban/tests/files/logs/traefik-auth | 86 +++++++++++++++++++++++ 3 files changed, 146 insertions(+), 36 deletions(-) diff --git a/ChangeLog b/ChangeLog index 32f2dec0..b4c969fb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -19,6 +19,7 @@ ver. 1.1.2-dev-1 (20??/??/??) - development nightly edition `paths-arch.conf`, `paths-fedora.conf`, `paths-opensuse.conf` - nftables, `paths-freebsd.conf`, `paths-osx.conf ` - pf; ### New Features and Enhancements +* `filter.d/traefik-auth.conf` - add JSON access log support with `logtype=json`, preserving the existing modes and default common log format (gh-2783) * `filter.d/cowrie.conf` - new filter and jail for Cowrie SSH/Telnet honeypot JSON log output (gh-4216) * `filter.d/gitlab.conf` - `logtype` option `json` adding support for the `application_json.log` format that superseded the deprecated `application.log`; default type `file` keeps the previous behaviour (gh-3566, gh-4230) diff --git a/config/filter.d/traefik-auth.conf b/config/filter.d/traefik-auth.conf index 5be16909..16fb110e 100644 --- a/config/filter.d/traefik-auth.conf +++ b/config/filter.d/traefik-auth.conf @@ -1,52 +1,47 @@ -# Fail2ban filter configuration for traefik :: auth -# used to ban hosts, that were failed through traefik +# Fail2Ban filter for failed HTTP authentication in Traefik access logs. # # Author: CrazyMax # -# To use 'traefik-auth' filter you have to configure your Traefik instance to write -# the access logs as describe in https://docs.traefik.io/configuration/logs/#access-logs -# into a log file on host and specify users for Basic Authentication -# https://docs.traefik.io/configuration/entrypoints/#basic-authentication +# Configure Traefik to write access logs to a file readable by Fail2Ban and +# set the jail's logpath to that file (here /var/log/traefik/access.log). +# https://doc.traefik.io/traefik/observe/logs-and-access-logs/ # -# Example: +# The default filter reads common-format logs. For JSON logs, change +# --accesslog.format to json and set filter = traefik-auth[logtype=json] +# in jail.local. Keep the required JSON fields listed below. # -# version: "3.2" +# Docker Compose example for Traefik v3 with a BasicAuth-protected dashboard: +# https://doc.traefik.io/traefik/v3.6/reference/install-configuration/api-dashboard/ +# Replace the example hostname and test:test credentials, and configure a TLS +# certificate for your deployment (otherwise Traefik uses its default certificate). +# Access the dashboard at https://traefik.example.com/dashboard/. # # services: # traefik: -# image: traefik:latest +# image: traefik:v3.6 # command: -# - "--loglevel=INFO" # - "--accesslog=true" -# - "--accessLog.filePath=/var/log/access.log" -# # - "--accessLog.filters.statusCodes=400-499" -# - "--defaultentrypoints=http,https" -# - "--entryPoints=Name:http Address::80" -# - "--entryPoints=Name:https Address::443 TLS" -# - "--docker.domain=example.com" -# - "--docker.watch=true" -# - "--docker.exposedbydefault=false" -# - "--api=true" +# - "--accesslog.filepath=/var/log/traefik/access.log" +# - "--accesslog.format=common" +# - "--accesslog.addinternals=true" +# - "--entrypoints.websecure.address=:443" +# - "--providers.docker=true" +# - "--providers.docker.exposedbydefault=false" # - "--api.dashboard=true" # ports: -# - target: 80 -# published: 80 -# protocol: tcp -# mode: host -# - target: 443 -# published: 443 -# protocol: tcp -# mode: host +# - "443:443" # labels: # - "traefik.enable=true" -# - "traefik.port=8080" -# - "traefik.backend=traefik" -# - "traefik.frontend.rule=Host:traefik.example.com" -# - "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/" +# - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)" +# - "traefik.http.routers.dashboard.entrypoints=websecure" +# - "traefik.http.routers.dashboard.tls=true" +# - "traefik.http.routers.dashboard.service=api@internal" +# - "traefik.http.routers.dashboard.middlewares=dashboard-auth" +# - "traefik.http.middlewares.dashboard-auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/" # volumes: -# - "/var/log/traefik:/var/log" -# - "/var/run/docker.sock:/var/run/docker.sock" -# restart: always +# - "/var/log/traefik:/var/log/traefik" +# - "/var/run/docker.sock:/var/run/docker.sock:ro" +# restart: unless-stopped # [Definition] @@ -56,7 +51,14 @@ req-method = \S+ # Usage example (for jail.local): # filter = traefik-auth[req-method="GET|POST|HEAD"] -failregex = ^ \- > \[\] \"(?:) [^\"]+\" 401\b +# Log format: common (default) or json (--accesslog.format=json). +# JSON logs must retain ClientHost, ClientUsername, DownstreamStatus, +# RequestMethod and StartUTC. For example: +# filter = traefik-auth[logtype=json] +logtype = common + +failregex = /failregex> +datepattern = /datepattern> ignoreregex = @@ -73,4 +75,25 @@ mode = normal # part of failregex matches user name (must be available in normal mode, must be empty in ddos mode, and both for aggressive mode): usrre-normal = (?!- )\S+ usrre-ddos = - -usrre-aggressive = \S+ \ No newline at end of file +usrre-aggressive = \S+ + +[lt_common] + +failregex = ^ \- > \[\] \"(?:) [^\"]+\" 401\b +datepattern = {DEFAULT} + +[lt_json] + +# Consume complete JSON fields, including escaped strings, so request data +# cannot masquerade as a field. Traefik access-log fields are scalar values. +_string = "(?:[^"\\]|\\.)*" +_value = (?:%(_string)s|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null) +_fields = (?:\s*%(_string)s\s*:\s*%(_value)s\s*,)*\s* + +usrre-normal = (?!-")(?:[^"\\]|\\.)+ +usrre-ddos = -? +usrre-aggressive = (?:[^"\\]|\\.)* + +failregex = ^\{(?=%(_fields)s"DownstreamStatus"\s*:\s*401\s*[,}])(?=%(_fields)s"ClientUsername"\s*:\s*">"\s*[,}])(?=%(_fields)s"RequestMethod"\s*:\s*"(?:)"\s*[,}])%(_fields)s"ClientHost"\s*:\s*""\s*[,}] + +datepattern = ^\{%(_fields)s"StartUTC"\s*:\s*"(%%ExY-%%m-%%dT%%H:%%M:%%S(?:\.%%f\d{0,3})?%%z)" diff --git a/fail2ban/tests/files/logs/traefik-auth b/fail2ban/tests/files/logs/traefik-auth index edfe7306..d834b238 100644 --- a/fail2ban/tests/files/logs/traefik-auth +++ b/fail2ban/tests/files/logs/traefik-auth @@ -21,3 +21,89 @@ # failJSON: { "time": "2018-11-18T22:34:30", "match": true , "host": "10.0.0.2" } 10.0.0.2 - - [18/Nov/2018:21:34:30 +0000] "GET /dashboard/ HTTP/2.0" 401 17 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0" 72 "Auth for frontend-Host-traefik-0" "/dashboard/" 0ms + +# JSON access logs (--accesslog.format=json). + +# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "desc": "Middleware 401, despite OriginStatus 0", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": true, "desc": "IPv6 and reordered fields", "time": "2018-11-18T22:34:34", "host": "2001:db8::1"} +{"time":"2018-11-18T21:34:35Z","StartUTC":"2018-11-18T21:34:34.123456789Z","RequestPath":"/","RequestMethod":"GET","OriginStatus":0,"DownstreamStatus":401,"ClientUsername":"alice","ClientPort":"12345","ClientHost":"2001:db8::1","ClientAddr":"192.0.2.1:12345"} +# failJSON: {"match": true, "desc": "Escaped quotes and backslashes in fields before ClientHost", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"} +{"request_header":"a \"ClientHost\":\"203.0.113.1\" \\ value","ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": true, "desc": "Escaped username", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"user \"name\\suffix","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Successful response despite OriginStatus 401"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":401,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "403 is not an authentication failure"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":403,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Status must be exactly 401"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":4010,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Injected status in a path"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"DownstreamStatus\":401,\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Injected username cannot replace absent credentials"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"ClientUsername\":\"alice\",\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Injected client cannot replace missing ClientHost"} +{"ClientAddr":"192.0.2.1:12345","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"ClientHost\":\"203.0.113.1\",\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Nested fields cannot replace top-level status"} +{"nested":{"DownstreamStatus":401},"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal"}] +# failJSON: {"match": false, "desc": "Unauthenticated challenge ignored in normal mode"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"-","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} + +# filterOptions: [{"logtype": "json", "mode": "ddos"}] +# failJSON: {"match": false, "desc": "Supplied username ignored in ddos mode"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} + +# filterOptions: [{"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "desc": "Unauthenticated request", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"-","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal", "req-method": "GET|POST"}, {"logtype": "json", "mode": "aggressive", "req-method": "GET|POST"}] +# failJSON: {"match": true, "desc": "Configured request method", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"POST","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} +# failJSON: {"match": false, "desc": "Excluded request method"} +{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"TRACE","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"} + +# Captured from traefik:3.6 with BasicAuth and ping@internal. + +# filterOptions: [{"logtype": "json", "mode": "normal"}] +# failJSON: {"match": false} +{"ClientAddr":"172.17.0.1:36648","ClientHost":"172.17.0.1","ClientPort":"36648","ClientUsername":"","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":67660,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":67660,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":1,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.628171727Z","StartUTC":"2026-09-16T10:07:53.628171727Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"} +{"ClientAddr":"172.17.0.1:36648","ClientHost":"172.17.0.1","ClientPort":"36648","ClientUsername":"","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":67660,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":67660,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":1,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.628171727Z","StartUTC":"2026-09-16T10:07:53.628171727Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal"}] +# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"} +{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "ddos"}] +# failJSON: {"match": false} +{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"} +{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal"}] +# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"} +{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "ddos"}] +# failJSON: {"match": false} +{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"} +{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": false} +{"ClientAddr":"172.17.0.1:36668","ClientHost":"172.17.0.1","ClientPort":"36668","ClientUsername":"test","DownstreamContentSize":2,"DownstreamStatus":200,"Duration":137614,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":137614,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":4,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63256206Z","StartUTC":"2026-09-16T10:07:53.63256206Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"} + +# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "aggressive"}] +# failJSON: {"match": true, "time": "2018-11-18T22:34:34", "host": "192.0.2.1", "user": "-alice", "desc": "Whitespace, username beginning with a dash, timestamp without fractions"} +{ "StartUTC" : "2018-11-18T21:34:34Z", "RequestMethod" : "GET", "ClientUsername" : "-alice", "ClientHost" : "192.0.2.1", "DownstreamStatus" : 401 }