Visualizing fail2ban ban logs

Hi, I would like to visualize the banned IPs from fail2ban log.
for example:


or

I preferd and tried the first one, but exec with sudo doesn´t work. fail2ban client needs root.
I don´t want to run NR as root.

the scond, fail2ban.log has to be filtered, I've tried a few things, but haven't come up with any results.
Did anyone already built something for fail2ban ?

You can allow the node red user to use sudo
to execute only the command you want without having to enter a password.
Use sudo visudo to edit the config file to allow that.

Or maybe add the node red user to whatever group has access to those logs. Look at the permissions on the log file.

Edit. Use ls -l /var/log/fail2ban.log to see.

.log I can see without root.

the fail2ban client needs root, I did add node-red user in visudo, but exec says fail2ban needs root.

You have to use sudo in the command in the exec node, but configure visudo to not need a password for the fail2ban command.

thats my solution:

[{"id":"dc71ac07a95659c3","type":"exec","z":"7667425061cb9763","command":"tail -n 100 /var/log/fail2ban.log | grep \"Ban\"","addpay":"","append":"","useSpawn":"false","timer":"2","winHide":false,"oldrc":false,"name":"fail2ban","x":320,"y":420,"wires":[["11e68e569fe20be4"],[],[]]},{"id":"285f2b0fa73c5141","type":"inject","z":"7667425061cb9763","name":"jede Minute","props":[{"p":"payload"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":110,"y":420,"wires":[["dc71ac07a95659c3","c29ab3f614c5082b"]]},{"id":"11e68e569fe20be4","type":"split","z":"7667425061cb9763","name":"Log aufteilen","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":550,"y":400,"wires":[["804b2ce39b17f98a"]]},{"id":"804b2ce39b17f98a","type":"function","z":"7667425061cb9763","name":"Fail2Ban","func":"let logEntry = msg.payload;\nif (!logEntry.trim()) {\n    return null;\n}\nlet regex = /(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})\\s+fail2ban.actions\\s+\\[\\d+\\]:\\s+NOTICE\\s+\\[([^\\]]+)\\]\\s+Ban\\s+([a-fA-F0-9:.]+)/;\nlet match = regex.exec(logEntry);\nif (match) {\n    let timestamp = match[1];\n    let service = match[2];\n    let ipAddress = match[3];\n    let date = new Date(timestamp.replace(',', '.'));\n    let timeInMillis = date.getTime();\n    let result = {\n        IP: ipAddress,\n        Dienst: service,\n        time: timeInMillis\n    };\n    msg.payload = result;\n    return msg;\n} \nelse {\n    node.warn(\"Kein passender Log-Eintrag gefunden\");\n    return null;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":440,"wires":[["e3eabf06f5759e16"]]},{"id":"e3eabf06f5759e16","type":"influxdb out","z":"7667425061cb9763","influxdb":"65f9e27871c73f7e","name":"Fail2Ban","measurement":"Fail2Ban","precision":"ms","retentionPolicy":"","database":"database","precisionV18FluxV20":"ms","retentionPolicyV18Flux":"","org":"organisation","bucket":"bucket","x":760,"y":440,"wires":[]},{"id":"b732a0ce799cc393","type":"influxdb in","z":"7667425061cb9763","influxdb":"65f9e27871c73f7e","name":"Fail2Ban","query":"select * from \"Fail2Ban\" order by time desc Limit 30","rawOutput":false,"precision":"","retentionPolicy":"","org":"organisation","x":320,"y":480,"wires":[["9318c96dd873b946"]]},{"id":"602fae0cf5cf60fe","type":"ui_template","z":"7667425061cb9763","group":"0174de37aa15dc39","name":"Fail2Ban","order":6,"width":0,"height":0,"format":"<style>\n    tr:hover {\n        background-color: #f9f9f9;\n    }\n    td,\n    th {\n        font-size: 14px;\n        border: 1px solid #ddd;\n    }\n    th {\n        background-color: #f2f2f2;\n        text-align: center;\n    }\n\n    td {\n        text-align: left;\n    }\n</style>\n<div>\n    <table class=\"table table-bordered\" style=\"width: 100%; border-collapse: collapse;\">\n        <thead>\n            <tr>\n                <th>Zeitpunkt</th>\n                <th>IP Adresse</th>\n                <th>Dienst</th>\n            </tr>\n        </thead>\n        <tbody>\n            <tr ng-repeat=\"entry in msg.payload\">\n                <td>{{ entry.time }}</td>\n                <td>{{ entry.IP }}</td>\n                <td>{{ entry.Dienst }}</td>\n            </tr>\n        </tbody>\n    </table>\n</div>","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":760,"y":480,"wires":[[]]},{"id":"c29ab3f614c5082b","type":"delay","z":"7667425061cb9763","name":"1s","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":130,"y":480,"wires":[["b732a0ce799cc393"]]},{"id":"9318c96dd873b946","type":"function","z":"7667425061cb9763","name":"InfluxDB","func":"let data = msg.payload;\nif (Array.isArray(data)) {\n    msg.payload = data.map(entry => {\n        let date = new Date(entry.time);\n        let weekday = date.toLocaleDateString('de-DE', { weekday: 'short' });\n        let day = date.toLocaleDateString('de-DE', { day: '2-digit' });\n        let month = date.toLocaleDateString('de-DE', { month: '2-digit' });\n        let year = date.toLocaleDateString('de-DE', { year: 'numeric' });\n        let time = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n        let formattedDate = `${weekday}, ${day}.${month}.${year} - ${time} Uhr`;\n        return {\n            time: formattedDate,\n            IP: entry.IP,\n            Dienst: entry.Dienst\n        };\n    });\n    return msg;\n}\nelse {\n    node.error(\"Die Daten sind im falschen Format\");\n    return null;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":480,"wires":[["602fae0cf5cf60fe"]]},{"id":"65f9e27871c73f7e","type":"influxdb","hostname":"192.168.168.5","port":8086,"protocol":"http","database":"db","name":"InfluxDB","usetls":false,"tls":"","influxdbVersion":"1.x","url":"http://localhost:8086","timeout":10,"rejectUnauthorized":true},{"id":"0174de37aa15dc39","type":"ui_group","name":"Fail2Ban","tab":"2353ff8665d46bde","order":1,"disp":true,"width":"10","collapse":false,"className":""},{"id":"2353ff8665d46bde","type":"ui_tab","name":"Fail2Ban","icon":"pan_tool","order":2,"disabled":false,"hidden":false}]

Grafana could also use it: