commit 3998f9e2c2197a5630cd6980d2950f60faf81638 Author: Markus Pesch Date: Mon Oct 2 12:50:34 2023 +0200 Initial Commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..12eac9e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,832 @@ +--- +kind: pipeline +type: kubernetes +name: linter + +clone: + disable: true + +platform: + os: linux + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: markdown lint + commands: + - markdownlint *.md + image: git.cryptic.systems/volker.raschek/markdownlint:0.36.0 + resources: + limits: + cpu: 150 + memory: 150M + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + event: + exclude: + - tag + +--- +kind: pipeline +type: docker +name: unit-test-amd64 + +clone: + disable: true + +platform: + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: unit-test + commands: + - go test -v ./... + image: docker.io/library/golang:1.21.0 + +trigger: + event: + exclude: + - tag + +--- +kind: pipeline +type: docker +name: unit-test-arm-v7 + +clone: + disable: true + +platform: + arch: arm + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: unit-test + commands: + - go test -v ./... + image: docker.io/library/golang:1.21.0 + +trigger: + event: + include: + - pull_request + - push + exclude: + - tag + +--- +kind: pipeline +type: docker +name: unit-test-arm64 + +clone: + disable: true + +platform: + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: unit-test + commands: + - go test -v ./... + image: docker.io/library/golang:1.21.0 + +trigger: + event: + include: + - pull_request + - push + exclude: + - tag + +--- +kind: pipeline +type: docker +name: dry-run-amd64 + +clone: + disable: true + +depends_on: +- linter +- unit-test-amd64 + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + dry_run: true + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-amd64 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + exclude: + - master + event: + - pull_request + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: dry-run-arm-v7 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm-v7 + +platform: + os: linux + arch: arm + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + dry_run: true + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-arm-v7 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + exclude: + - master + event: + - pull_request + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: dry-run-arm64-v8 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm64 + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + dry_run: true + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-arm64-v8 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + exclude: + - master + event: + - pull_request + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: latest-amd64 + +clone: + disable: true + +depends_on: +- linter +- unit-test-amd64 + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-amd64 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: latest-arm-v7 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm-v7 + +platform: + os: linux + arch: arm + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-arm-v7 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: latest-arm64-v8 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm64 + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: false + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + tags: latest-arm64-v8 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: kubernetes +name: latest-manifest + +clone: + disable: true + +depends_on: +- latest-amd64 +- latest-arm-v7 +- latest-arm64-v8 + +# docker.io/plugins/manifest only for amd64 architectures available +node_selector: + kubernetes.io/os: linux + kubernetes.io/arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build-manifest + image: docker.io/plugins/manifest:1.4.0 + settings: + auto_tag: false + ignore_missing: true + spec: manifest.tmpl + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: tagged-amd64 + +clone: + disable: true + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: true + auto_tag_suffix: amd64 + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + build_args: + - VERSION=${DRONE_TAG} + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: tagged-arm-v7 + +clone: + disable: true + +platform: + os: linux + arch: arm + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: true + auto_tag_suffix: arm-v7 + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + build_args: + - VERSION=${DRONE_TAG} + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: docker +name: tagged-arm64-v8 + +clone: + disable: true + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.10.9 + settings: + auto_tag: true + auto_tag_suffix: arm64-v8 + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + build_args: + - VERSION=${DRONE_TAG} + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/prometheus-fail2ban-exporter + +--- +kind: pipeline +type: kubernetes +name: tagged-manifest + +clone: + disable: true + +depends_on: +- tagged-amd64 +- tagged-arm-v7 +- tagged-arm64-v8 + +# docker.io/plugins/manifest only for amd64 architectures available +node_selector: + kubernetes.io/os: linux + kubernetes.io/arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build-manifest + image: docker.io/plugins/manifest:1.4.0 + settings: + auto_tag: true + ignore_missing: true + spec: manifest.tmpl + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/prometheus-fail2ban-exporter diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dd69de0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + +[Makefile] +indent_style = tab \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98f76ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea/ +vendor/ +*.iml + +build/ +dist/ + +prometheus-fail2ban-exporter diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..0e98dd1 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,143 @@ +# markdownlint YAML configuration +# https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "atx" + +# MD004/ul-style - Unordered list style +MD004: + style: "dash" + +# MD007/ul-indent - Unordered list indentation +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs - Hard tabs +MD010: + # Include code blocks + code_blocks: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 80 + # Number of characters for headings + heading_line_length: 80 + # Number of characters for code blocks + code_block_line_length: 80 + # Include code blocks + code_blocks: false + # Include tables + tables: false + # Include headings + headings: true + # Include headings + headers: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + # Only check sibling headings + allow_different_nesting: true + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD029/ol-prefix - Ordered list item prefix +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "---" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: false + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "fenced" + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence syle + style: "backtick" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b2b1971 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM docker.io/library/golang:1.20.5-buster AS build + +WORKDIR /workspace +ADD . /workspace + +RUN apt update --yes && \ + apt install --yes build-essential && \ + make install \ + PREFIX=/usr \ + DESTDIR=/app \ + EXECUTABLE=prometheus-fail2ban-exporter + +FROM docker.io/library/debian:10-slim + +COPY --from=build /app / + +EXPOSE 9191 + +ENTRYPOINT [ "/usr/bin/prometheus-fail2ban-exporter" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c9b997 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Hector +Copyright (c) 2023 Markus Pesch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30abeaa --- /dev/null +++ b/Makefile @@ -0,0 +1,85 @@ +DESTDIR?= +PREFIX?=/usr/local +EXECUTABLE?=prometheus-fail2ban-exporter + +CONTAINER_RUNTIME?=$(shell which podman) + +# List make commands +.PHONY: ls +ls: + cat Makefile | grep "^[a-zA-Z#].*" | cut -d ":" -f 1 | sed s';#;\n#;'g + +# Download dependencies +.PHONY: download +download: + go mod download + +# Update project dependencies +.PHONY: update +update: + go get -u + go mod download + go mod tidy + +# Run project tests +.PHONY: test +test: download + go test ./... -v -race + +# Look for "suspicious constructs" in source code +.PHONY: vet +vet: download + go vet ./... + +# Format code +.PHONY: fmt +fmt: download + go mod tidy + go fmt ./... + +# Check for unformatted go code +.PHONY: check/fmt +check/fmt: download + test -z $(shell gofmt -l .) + +# Build project +.PHONY: build +build: + CGO_ENABLED=0 go build \ + -ldflags "\ + -X main.version=${shell git describe --tags} \ + -X main.commit=${shell git rev-parse HEAD} \ + -X main.date=${shell date --iso-8601=seconds} \ + -X main.builtBy=manual \ + " \ + -trimpath \ + -o ${EXECUTABLE} \ + exporter.go + +# build container-image +.PHONY: build/container-image +build/container-image: + ${CONTAINER_RUNTIME} build \ + --tag ${EXECUTABLE} \ + . + +.PHONY: install +install: build + mkdir --parents ${DESTDIR}/usr/lib/systemd/system + sed -e "s/EXECUTABLE/${EXECUTABLE}/gm" systemd/systemd.service > ${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service + chmod 0644 ${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service + + install -D --mode 0755 --target-directory ${DESTDIR}${PREFIX}/bin ${EXECUTABLE} + +# NOTE: Set restrict file permissions by default to protect optional basic auth credentials + install -D --mode 0600 env ${DESTDIR}/etc/conf.d/${EXECUTABLE} + + install -D --mode 0755 --target-directory ${DESTDIR}${PREFIX}/share/licenses/${EXECUTABLE} LICENSE + +.PHONY: uninstall +uninstall: + -rm --recursive --force \ + ${DESTDIR}${PREFIX}/bin/${EXECUTABLE} \ + ${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service \ + ${DESTDIR}/etc/conf.d/${EXECUTABLE} \ + ${DESTDIR}${PREFIX}/share/licenses/${EXECUTABLE}/LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..6267fbd --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ +# prometheus-fail2ban-exporter + +[![Build Status](https://drone.cryptic.systems/api/badges/volker.raschek/prometheus-fail2ban-exporter/status.svg)](https://drone.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter) + +This is a fork of Hector's fail2ban +[exporter](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter). This +fork contains some changes to get the application running in a kubernetes +cluster. + +## Table of Contents + +1. Quick Start +2. Metrics +3. Configuration +4. Building from source +5. Textfile metrics + +## 1. Quick Start + +The exporter can be run as a standalone binary or a docker container. + +### 1.1. Standalone + +The following command will start collecting metrics from the +`/var/run/fail2ban/fail2ban.sock` file and expose them on port `9191`. + +```bash +$ fail2ban_exporter --collector.f2b.socket=/var/run/fail2ban/fail2ban.sock --web.listen-address=":9191" + +2022/02/20 09:54:06 fail2ban exporter version 0.8.1 +2022/02/20 09:54:06 starting server at :9191 +2022/02/20 09:54:06 reading metrics from fail2ban socket: /var/run/fail2ban/fail2ban.sock +2022/02/20 09:54:06 metrics available at '/metrics' +2022/02/20 09:54:06 ready +``` + +Binary files for each release can be found on the +[releases](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/releases) +page. + +There is also an [example systemd service +file](/_examples/systemd/fail2ban_exporter.service) included in the repository. +This is a starting point to run the exporter as a service. + +### 1.2. Docker + +#### Docker run + +```bash +$ docker run -d \ + --name "fail2ban-exporter" \ + -v /var/run/fail2ban:/var/run/fail2ban:ro \ + -p "9191:9191" \ + registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest +``` + +#### Docker compose + +```yaml +version: "2" +services: + exporter: + image: registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest + volumes: + - /var/run/fail2ban/:/var/run/fail2ban:ro + ports: + - "9191:9191" +``` + +Use the `:latest` tag to get the latest stable release. See the [registry +page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/container_registry) +for all available tags. + +**NOTE:** While it is possible to mount the `fail2ban.sock` file directly, it is +recommended to mount the parent folder instead. The `.sock` file is deleted by +fail2ban on shutdown and re-created on startup and this causes problems for the +docker mount. See [this +reply](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/issues/11#note_665003499) +for more details. + +## 2. Metrics + +The exporter exposes the following metrics: + +*All metric names are prefixed with `f2b_`* + +| Metric | Description | Example | +|------------------------------|------------------------------------------------------------------------------------|-----------------------------------------------------| +| `up` | Returns 1 if the exporter is up and running | `f2b_up 1` | +| `errors` | Count the number of errors since startup by type | | +| `errors{type="socket_conn"}` | Errors connecting to the fail2ban socket (e.g. connection refused) | `f2b_errors{type="socket_conn"} 0` | +| `errors{type="socket_req"}` | Errors sending requests to the fail2ban server (e.g. invalid responses) | `f2b_errors{type="socket_req"} 0` | +| `jail_count` | Number of jails configured in fail2ban | `f2b_jail_count 2` | +| `jail_banned_current` | Number of IPs currently banned per jail | `f2b_jail_banned_current{jail="sshd"} 15` | +| `jail_banned_total` | Total number of banned IPs since fail2ban startup per jail (includes expired bans) | `f2b_jail_banned_total{jail="sshd"} 31` | +| `jail_failed_current` | Number of current failures per jail | `f2b_jail_failed_current{jail="sshd"} 6` | +| `jail_failed_total` | Total number of failures since fail2ban startup per jail | `f2b_jail_failed_total{jail="sshd"} 125` | +| `jail_config_ban_time` | How long an IP is banned for in this jail (in seconds) | `f2b_config_jail_ban_time{jail="sshd"} 600` | +| `jail_config_find_time` | How far back the filter will look for failures in this jail (in seconds) | `f2b_config_jail_find_time{jail="sshd"} 600` | +| `jail_config_max_retry` | The max number of failures allowed before banning an IP in this jail | `f2b_config_jail_max_retries{jail="sshd"} 5` | +| `version` | Version string of the exporter and fail2ban | `f2b_version{exporter="0.5.0",fail2ban="0.11.1"} 1` | + +The metrics above correspond to the matching fields in the `fail2ban-client +status ` command: + +```text +Status for the jail: sshd +|- Filter +| |- Currently failed: 6 +| |- Total failed: 125 +| `- File list: /var/log/auth.log +`- Actions + |- Currently banned: 15 + |- Total banned: 31 + `- Banned IP list: ... +``` + +### 2.1. Grafana + +The metrics exported by this tool are compatible with Prometheus and Grafana. A +sample grafana dashboard can be found in the +[grafana.json](/_examples/grafana/dashboard.json) file. Just import the contents +of this file into a new Grafana dashboard to get started. + +The dashboard supports displaying data from multiple exporters. Use the +`instance` dashboard variable to select which ones to display. + +*(Sample dashboard is compatible with Grafana `9.1.8` and above)* + +## 3. Configuration + +The exporter is configured with CLI flags and environment variables. +There are no configuration files. + +### CLI flags + +```text +🚀 Collect prometheus metrics from a running Fail2Ban instance + +Flags: + -h, --help Show context-sensitive help. + -v, --version Show version info and exit + --dry-run Attempt to connect to the fail2ban socket then exit + before starting the server + --web.listen-address=":9191" Address to use for the metrics server + ($F2B_WEB_LISTEN_ADDRESS) + --collector.f2b.socket="/var/run/fail2ban/fail2ban.sock" + Path to the fail2ban server socket + ($F2B_COLLECTOR_SOCKET) + --collector.f2b.exit-on-socket-connection-error + When set to true the exporter will immediately + exit on a fail2ban socket connection error + ($F2B_EXIT_ON_SOCKET_CONN_ERROR) + --collector.textfile.directory=STRING + Directory to read text files with metrics from + ($F2B_COLLECTOR_TEXT_PATH) + --web.basic-auth.username=STRING + Username to use to protect endpoints with basic auth + ($F2B_WEB_BASICAUTH_USER) + --web.basic-auth.password=STRING + Password to use to protect endpoints with basic auth + ($F2B_WEB_BASICAUTH_PASS) +``` + +### Environment variables + +Each environment variable corresponds to a CLI flag. +If both are specified, the CLI flag takes precedence. + +| Environment variable | Corresponding CLI flag | +|---------------------------------|---------------------------------------------------| +| `F2B_COLLECTOR_SOCKET` | `--collector.f2b.socket` | +| `F2B_COLLECTOR_TEXT_PATH` | `--collector.textfile.directory` | +| `F2B_WEB_LISTEN_ADDRESS` | `--web.listen-address` | +| `F2B_WEB_BASICAUTH_USER` | `--web.basic-auth.username` | +| `F2B_WEB_BASICAUTH_PASS` | `--web.basic-auth.password` | +| `F2B_EXIT_ON_SOCKET_CONN_ERROR` | `--collector.f2b.exit-on-socket-connection-error` | + +## 4. Building from source + +Building from source has the following dependencies: + +- Go v1.20 +- Make + +From there, simply run `make build` + +This will download the necessary dependencies and build a `fail2ban_exporter` +binary in the root of the project. + +## 5. Textfile metrics + +For more flexibility the exporter also allows exporting metrics collected from a +text file. + +To enable textfile metrics provide the directory to read files from with the +`--collector.textfile.directory` flag. + +Metrics collected from these files will be exposed directly alongside the other +metrics without any additional processing. This means that it is the +responsibility of the file creator to ensure the format is correct. + +By exporting textfile metrics an extra metric is also exported with an error +count for each file: + +```text +# HELP textfile_error Checks for errors while reading text files +# TYPE textfile_error gauge +textfile_error{path="file.prom"} 0 +``` + +**NOTE:** Any file not ending with `.prom` will be ignored. + +### Running in Docker + +To collect textfile metrics inside a docker container, a couple of things need +to be done: + +1. Mount the folder with the metrics files +2. Set the `F2B_COLLECTOR_TEXT_PATH` environment variable + +*For example:* + +```bash +$ docker run -d \ + --name "fail2ban-exporter" \ + -v /var/run/fail2ban:/var/run/fail2ban:ro \ + -v /path/to/metrics:/app/metrics/:ro \ + -e F2B_COLLECTOR_TEXT_PATH=/app/metrics \ + -p "9191:9191" \ + registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest +``` diff --git a/_examples/grafana/dashboard.json b/_examples/grafana/dashboard.json new file mode 100644 index 0000000..caa7893 --- /dev/null +++ b/_examples/grafana/dashboard.json @@ -0,0 +1,877 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.1.8" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 2, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".*Time" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 206, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "f2b_config_jail_max_retries{instance=~\"$instance\"}", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{jail}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "f2b_config_jail_ban_time{instance=~\"$instance\"}", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{jail}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "f2b_config_jail_find_time{instance=~\"$instance\"}", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{jail}}", + "refId": "C" + } + ], + "title": "F2B Config", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "groupBy", + "options": { + "fields": { + "Value #A": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "Value #B": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "Value #C": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "instance": { + "aggregations": [], + "operation": "groupby" + }, + "jail": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Value #A (lastNotNull)": "Max Retries", + "Value #B (lastNotNull)": "Ban Time", + "Value #C (lastNotNull)": "Find Time", + "jail": "Jail" + } + } + } + ], + "transparent": true, + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 190, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_jail_failed_total{instance=~\"$instance\"}", + "hide": false, + "interval": "", + "legendFormat": "{{jail}} ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Failures (Total)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 191, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_jail_banned_total{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{jail}} ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Bans (Total)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 208, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_jail_failed_current{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{jail}} ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Failures (Current)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 209, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_jail_banned_current{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{jail}} ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Bans (Current)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 203, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_up{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Up ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Up", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 204, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "f2b_errors{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{type}} ({{instance}})", + "range": true, + "refId": "A" + } + ], + "title": "Fail2Ban Exporter Errors", + "transparent": true, + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "DataSource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "f2b_up", + "description": "Select which instance(s) to show", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "f2b_up", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "/.*instance=\"([^\"]+)\"/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "F2B", + "uid": "cTkH9AT7z", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/_examples/systemd/README.md b/_examples/systemd/README.md new file mode 100644 index 0000000..52d25c3 --- /dev/null +++ b/_examples/systemd/README.md @@ -0,0 +1,7 @@ +# Systemd + +The `.service` file in this directory should be copied to the `/etc/systemd/system/` folder. +- It expects the binary file to be installed at `/usr/sbin/fail2ban_exporter`. +- It expects a user named `fail2ban_exporter` to exist. This user should not have a shell or any special privileges aside from read-access to the fail2ban socket file. + +The `ExecStart` line can be modified to add any custom CLI flags. diff --git a/_examples/systemd/fail2ban_exporter.service b/_examples/systemd/fail2ban_exporter.service new file mode 100644 index 0000000..db1cf68 --- /dev/null +++ b/_examples/systemd/fail2ban_exporter.service @@ -0,0 +1,9 @@ +[Unit] +Description=Fail2Ban Exporter + +[Service] +User=fail2ban_exporter +ExecStart=/usr/sbin/fail2ban_exporter + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/auth/basic.go b/auth/basic.go new file mode 100644 index 0000000..243b2a5 --- /dev/null +++ b/auth/basic.go @@ -0,0 +1,29 @@ +package auth + +import ( + "fmt" + "net/http" +) + +func NewBasicAuthProvider(username, password string) AuthProvider { + return &basicAuthProvider{ + hashedAuth: encodeBasicAuth(username, password), + } +} + +type basicAuthProvider struct { + hashedAuth string +} + +func (p *basicAuthProvider) IsAllowed(request *http.Request) bool { + username, password, ok := request.BasicAuth() + if !ok { + return false + } + requestAuth := encodeBasicAuth(username, password) + return p.hashedAuth == requestAuth +} + +func encodeBasicAuth(username, password string) string { + return HashString(fmt.Sprintf("%s:%s", username, password)) +} diff --git a/auth/basic_test.go b/auth/basic_test.go new file mode 100644 index 0000000..e4ca1fd --- /dev/null +++ b/auth/basic_test.go @@ -0,0 +1,53 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithCorrectCreds_THEN_TrueReturned(t *testing.T) { + // assemble + username := "u1" + password := HashString("abc") + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth(username, password) + provider := NewBasicAuthProvider(username, password) + + // act + result := provider.IsAllowed(request) + + // assert + if !result { + t.Errorf("expected request to be allowed, but failed") + } +} + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithoutCreds_THEN_FalseReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + provider := NewBasicAuthProvider("u1", "p1") + + // act + result := provider.IsAllowed(request) + + // assert + if result { + t.Errorf("expected request to be denied, but was allowed") + } +} + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithWrongCreds_THEN_FalseReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth("wrong", "pw") + provider := NewBasicAuthProvider("u1", "p1") + + // act + result := provider.IsAllowed(request) + + // assert + if result { + t.Errorf("expected request to be denied, but was allowed") + } +} diff --git a/auth/empty.go b/auth/empty.go new file mode 100644 index 0000000..01e6775 --- /dev/null +++ b/auth/empty.go @@ -0,0 +1,14 @@ +package auth + +import "net/http" + +func NewEmptyAuthProvider() AuthProvider { + return &emptyAuthProvider{} +} + +type emptyAuthProvider struct { +} + +func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool { + return true +} diff --git a/auth/empty_test.go b/auth/empty_test.go new file mode 100644 index 0000000..048de9d --- /dev/null +++ b/auth/empty_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithoutAuth_THEN_TrueReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + provider := NewEmptyAuthProvider() + + // act + response := provider.IsAllowed(request) + + // assert + if !response { + t.Errorf("expected request to be allowed, but failed") + } +} + +func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithAuth_THEN_TrueReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth("user", "pass") + provider := NewEmptyAuthProvider() + + // act + response := provider.IsAllowed(request) + + // assert + if !response { + t.Errorf("expected request to be allowed, but failed") + } +} diff --git a/auth/hash.go b/auth/hash.go new file mode 100644 index 0000000..cc41e36 --- /dev/null +++ b/auth/hash.go @@ -0,0 +1,18 @@ +package auth + +import ( + "crypto/sha256" + "encoding/hex" +) + +func hash(data []byte) []byte { + if len(data) == 0 { + return []byte{} + } + b := sha256.Sum256(data) + return b[:] +} + +func HashString(data string) string { + return hex.EncodeToString(hash([]byte(data))) +} diff --git a/auth/hash_test.go b/auth/hash_test.go new file mode 100644 index 0000000..ffe4bea --- /dev/null +++ b/auth/hash_test.go @@ -0,0 +1,26 @@ +package auth + +import ( + "reflect" + "testing" +) + +func TestHashString(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + {"Happy path #1", "123", "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"}, + {"Happy path #2", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"}, + {"Happy path #3", "H3Ll0_W0RLD", "d58a27fe9a6e73a1d8a67189fb8acace047e7a1a795276a0056d3717ad61bd0e"}, + {"Blank string", "", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HashString(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("HashString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/auth/provider.go b/auth/provider.go new file mode 100644 index 0000000..a259ea0 --- /dev/null +++ b/auth/provider.go @@ -0,0 +1,9 @@ +package auth + +import ( + "net/http" +) + +type AuthProvider interface { + IsAllowed(*http.Request) bool +} diff --git a/cfg/cfg.go b/cfg/cfg.go new file mode 100644 index 0000000..142173e --- /dev/null +++ b/cfg/cfg.go @@ -0,0 +1,84 @@ +package cfg + +import ( + "fmt" + "log" + "os" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth" + "github.com/alecthomas/kong" +) + +var cliStruct struct { + VersionMode bool `name:"version" short:"v" help:"Show version info and exit"` + DryRunMode bool `name:"dry-run" help:"Attempt to connect to the fail2ban socket then exit before starting the server"` + ServerAddress string `name:"web.listen-address" env:"F2B_WEB_LISTEN_ADDRESS" help:"Address to use for the metrics server" default:"${default_address}"` + F2bSocketPath string `name:"collector.f2b.socket" env:"F2B_COLLECTOR_SOCKET" help:"Path to the fail2ban server socket" default:"${default_socket}"` + ExitOnSocketError bool `name:"collector.f2b.exit-on-socket-connection-error" env:"F2B_EXIT_ON_SOCKET_CONN_ERROR" help:"When set to true the exporter will immediately exit on a fail2ban socket connection error"` + TextFileExporterPath string `name:"collector.textfile.directory" env:"F2B_COLLECTOR_TEXT_PATH" help:"Directory to read text files with metrics from"` + BasicAuthUser string `name:"web.basic-auth.username" env:"F2B_WEB_BASICAUTH_USER" help:"Username to use to protect endpoints with basic auth"` + BasicAuthPass string `name:"web.basic-auth.password" env:"F2B_WEB_BASICAUTH_PASS" help:"Password to use to protect endpoints with basic auth"` +} + +func Parse() *AppSettings { + ctx := kong.Parse( + &cliStruct, + kong.Vars{ + "default_socket": "/var/run/fail2ban/fail2ban.sock", + "default_address": ":9191", + }, + kong.Name("fail2ban_exporter"), + kong.Description("🚀 Export prometheus metrics from a running Fail2Ban instance"), + kong.UsageOnError(), + ) + + validateFlags(ctx) + settings := &AppSettings{ + VersionMode: cliStruct.VersionMode, + DryRunMode: cliStruct.DryRunMode, + MetricsAddress: cliStruct.ServerAddress, + Fail2BanSocketPath: cliStruct.F2bSocketPath, + FileCollectorPath: cliStruct.TextFileExporterPath, + ExitOnSocketConnError: cliStruct.ExitOnSocketError, + AuthProvider: createAuthProvider(), + } + return settings +} + +func createAuthProvider() auth.AuthProvider { + username := cliStruct.BasicAuthUser + password := cliStruct.BasicAuthPass + + if len(username) == 0 && len(password) == 0 { + return auth.NewEmptyAuthProvider() + } + log.Print("basic auth enabled") + return auth.NewBasicAuthProvider(username, password) +} + +func validateFlags(cliCtx *kong.Context) { + var flagsValid = true + var messages = []string{} + if !cliStruct.VersionMode { + if cliStruct.F2bSocketPath == "" { + messages = append(messages, "error: fail2ban socket path must not be blank") + flagsValid = false + } + if cliStruct.ServerAddress == "" { + messages = append(messages, "error: invalid server address, must not be blank") + flagsValid = false + } + if (len(cliStruct.BasicAuthUser) > 0) != (len(cliStruct.BasicAuthPass) > 0) { + messages = append(messages, "error: to enable basic auth both the username and the password must be provided") + flagsValid = false + } + } + if !flagsValid { + cliCtx.PrintUsage(false) + fmt.Println() + for i := 0; i < len(messages); i++ { + fmt.Println(messages[i]) + } + os.Exit(1) + } +} diff --git a/cfg/settings.go b/cfg/settings.go new file mode 100644 index 0000000..826fa03 --- /dev/null +++ b/cfg/settings.go @@ -0,0 +1,13 @@ +package cfg + +import "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth" + +type AppSettings struct { + VersionMode bool + DryRunMode bool + MetricsAddress string + Fail2BanSocketPath string + FileCollectorPath string + AuthProvider auth.AuthProvider + ExitOnSocketConnError bool +} diff --git a/collector/f2b/collector.go b/collector/f2b/collector.go new file mode 100644 index 0000000..2447c68 --- /dev/null +++ b/collector/f2b/collector.go @@ -0,0 +1,76 @@ +package f2b + +import ( + "log" + "os" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg" + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/socket" + "github.com/prometheus/client_golang/prometheus" +) + +type Collector struct { + socketPath string + exporterVersion string + lastError error + socketConnectionErrorCount int + socketRequestErrorCount int + exitOnSocketConnError bool +} + +func NewExporter(appSettings *cfg.AppSettings, exporterVersion string) *Collector { + log.Printf("reading fail2ban metrics from socket file: %s", appSettings.Fail2BanSocketPath) + printFail2BanServerVersion(appSettings.Fail2BanSocketPath) + return &Collector{ + socketPath: appSettings.Fail2BanSocketPath, + exporterVersion: exporterVersion, + lastError: nil, + socketConnectionErrorCount: 0, + socketRequestErrorCount: 0, + exitOnSocketConnError: appSettings.ExitOnSocketConnError, + } +} + +func (c *Collector) Describe(ch chan<- *prometheus.Desc) { + ch <- metricServerUp + ch <- metricJailCount + ch <- metricJailFailedCurrent + ch <- metricJailFailedTotal + ch <- metricJailBannedCurrent + ch <- metricJailBannedTotal + ch <- metricErrorCount +} + +func (c *Collector) Collect(ch chan<- prometheus.Metric) { + s, err := socket.ConnectToSocket(c.socketPath) + if err != nil { + log.Printf("error opening socket: %v", err) + c.socketConnectionErrorCount++ + if c.exitOnSocketConnError { + os.Exit(1) + } + } else { + defer s.Close() + } + + c.collectServerUpMetric(ch, s) + if err == nil && s != nil { + c.collectJailMetrics(ch, s) + c.collectVersionMetric(ch, s) + } + c.collectErrorCountMetric(ch) +} + +func printFail2BanServerVersion(socketPath string) { + s, err := socket.ConnectToSocket(socketPath) + if err != nil { + log.Printf("error connecting to socket: %v", err) + } else { + version, err := s.GetServerVersion() + if err != nil { + log.Printf("error interacting with socket: %v", err) + } else { + log.Printf("successfully connected to fail2ban socket! fail2ban version: %s", version) + } + } +} diff --git a/collector/f2b/socket.go b/collector/f2b/socket.go new file mode 100644 index 0000000..a7378a5 --- /dev/null +++ b/collector/f2b/socket.go @@ -0,0 +1,180 @@ +package f2b + +import ( + "log" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/socket" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + namespace = "f2b" +) + +var ( + metricErrorCount = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "errors"), + "Number of errors found since startup", + []string{"type"}, nil, + ) + metricServerUp = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Check if the fail2ban server is up", + nil, nil, + ) + metricJailCount = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "jail_count"), + "Number of defined jails", + nil, nil, + ) + metricJailFailedCurrent = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "jail_failed_current"), + "Number of current failures on this jail's filter", + []string{"jail"}, nil, + ) + metricJailFailedTotal = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "jail_failed_total"), + "Number of total failures on this jail's filter", + []string{"jail"}, nil, + ) + metricJailBannedCurrent = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "jail_banned_current"), + "Number of IPs currently banned in this jail", + []string{"jail"}, nil, + ) + metricJailBannedTotal = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "jail_banned_total"), + "Total number of IPs banned by this jail (includes expired bans)", + []string{"jail"}, nil, + ) + metricJailBanTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_ban_time"), + "How long an IP is banned for in this jail (in seconds)", + []string{"jail"}, nil, + ) + metricJailFindTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_find_time"), + "How far back will the filter look for failures in this jail (in seconds)", + []string{"jail"}, nil, + ) + metricJailMaxRetry = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_max_retries"), + "The number of failures allowed until the IP is banned by this jail", + []string{"jail"}, nil, + ) + metricVersionInfo = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "version"), + "Version of the exporter and fail2ban server", + []string{"exporter", "fail2ban"}, nil, + ) +) + +func (c *Collector) collectErrorCountMetric(ch chan<- prometheus.Metric) { + ch <- prometheus.MustNewConstMetric( + metricErrorCount, prometheus.CounterValue, float64(c.socketConnectionErrorCount), "socket_conn", + ) + ch <- prometheus.MustNewConstMetric( + metricErrorCount, prometheus.CounterValue, float64(c.socketRequestErrorCount), "socket_req", + ) +} + +func (c *Collector) collectServerUpMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) { + var serverUp float64 = 0 + if s != nil { + pingSuccess, err := s.Ping() + if err != nil { + c.socketRequestErrorCount++ + log.Print(err) + } + if err == nil && pingSuccess { + serverUp = 1 + } + } + ch <- prometheus.MustNewConstMetric( + metricServerUp, prometheus.GaugeValue, serverUp, + ) +} + +func (c *Collector) collectJailMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) { + jails, err := s.GetJails() + var count float64 = 0 + if err != nil { + c.socketRequestErrorCount++ + log.Print(err) + } + if err == nil { + count = float64(len(jails)) + } + ch <- prometheus.MustNewConstMetric( + metricJailCount, prometheus.GaugeValue, count, + ) + + for i := range jails { + c.collectJailStatsMetric(ch, s, jails[i]) + c.collectJailConfigMetrics(ch, s, jails[i]) + } +} + +func (c *Collector) collectJailStatsMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) { + stats, err := s.GetJailStats(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get stats for jail %s: %v", jail, err) + return + } + + ch <- prometheus.MustNewConstMetric( + metricJailFailedCurrent, prometheus.GaugeValue, float64(stats.FailedCurrent), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailFailedTotal, prometheus.GaugeValue, float64(stats.FailedTotal), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailBannedCurrent, prometheus.GaugeValue, float64(stats.BannedCurrent), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailBannedTotal, prometheus.GaugeValue, float64(stats.BannedTotal), jail, + ) +} + +func (c *Collector) collectJailConfigMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) { + banTime, err := s.GetJailBanTime(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get ban time for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailBanTime, prometheus.GaugeValue, float64(banTime), jail, + ) + } + findTime, err := s.GetJailFindTime(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get find time for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailFindTime, prometheus.GaugeValue, float64(findTime), jail, + ) + } + maxRetry, err := s.GetJailMaxRetries(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get max retries for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailMaxRetry, prometheus.GaugeValue, float64(maxRetry), jail, + ) + } +} + +func (c *Collector) collectVersionMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) { + fail2banVersion, err := s.GetServerVersion() + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get fail2ban server version: %v", err) + } + + ch <- prometheus.MustNewConstMetric( + metricVersionInfo, prometheus.GaugeValue, float64(1), c.exporterVersion, fail2banVersion, + ) +} diff --git a/collector/textfile/collector.go b/collector/textfile/collector.go new file mode 100644 index 0000000..f490e6d --- /dev/null +++ b/collector/textfile/collector.go @@ -0,0 +1,49 @@ +package textfile + +import ( + "log" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg" + "github.com/prometheus/client_golang/prometheus" +) + +type Collector struct { + enabled bool + folderPath string + fileMap map[string]*fileData +} + +type fileData struct { + readErrors int + fileName string + fileContents []byte +} + +func NewCollector(appSettings *cfg.AppSettings) *Collector { + collector := &Collector{ + enabled: appSettings.FileCollectorPath != "", + folderPath: appSettings.FileCollectorPath, + fileMap: make(map[string]*fileData), + } + if collector.enabled { + log.Printf("reading textfile metrics from: %s", collector.folderPath) + } + return collector +} + +func (c *Collector) Describe(ch chan<- *prometheus.Desc) { + if c.enabled { + ch <- metricReadError + } +} + +func (c *Collector) Collect(ch chan<- prometheus.Metric) { + if c.enabled { + c.collectFileContents() + c.collectFileErrors(ch) + } +} + +func (c *Collector) appendErrorForPath(path string) { + c.fileMap[path].readErrors++ +} diff --git a/collector/textfile/file.go b/collector/textfile/file.go new file mode 100644 index 0000000..7591511 --- /dev/null +++ b/collector/textfile/file.go @@ -0,0 +1,56 @@ +package textfile + +import ( + "log" + "os" + "path/filepath" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +const namespace = "textfile" + +var ( + metricReadError = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "error"), + "Checks for errors while reading text files", + []string{"path"}, nil, + ) +) + +func (c *Collector) collectFileContents() { + files, err := os.ReadDir(c.folderPath) + if err != nil { + log.Printf("error reading directory '%s': %v", c.folderPath, err) + return + } + + for _, file := range files { + fileName := file.Name() + if !strings.HasSuffix(strings.ToLower(fileName), ".prom") { + continue + } + c.fileMap[fileName] = &fileData{ + readErrors: 0, + fileName: fileName, + } + + fullPath := filepath.Join(c.folderPath, fileName) + content, err := os.ReadFile(fullPath) + if err != nil { + c.appendErrorForPath(fileName) + log.Printf("error reading contents of file '%s': %v", fileName, err) + } + + c.fileMap[fileName].fileContents = content + } +} + +func (c *Collector) collectFileErrors(ch chan<- prometheus.Metric) { + for _, f := range c.fileMap { + ch <- prometheus.MustNewConstMetric( + metricReadError, prometheus.GaugeValue, float64(f.readErrors), f.fileName, + ) + } +} diff --git a/collector/textfile/writer.go b/collector/textfile/writer.go new file mode 100644 index 0000000..3bc3e63 --- /dev/null +++ b/collector/textfile/writer.go @@ -0,0 +1,20 @@ +package textfile + +import ( + "log" + "net/http" +) + +func (c *Collector) WriteTextFileMetrics(w http.ResponseWriter, r *http.Request) { + if !c.enabled { + return + } + + for _, f := range c.fileMap { + _, err := w.Write(f.fileContents) + if err != nil { + c.appendErrorForPath(f.fileName) + log.Printf("error writing file contents to response writer '%s': %v", f.fileName, err) + } + } +} diff --git a/env b/env new file mode 100644 index 0000000..d0286e8 --- /dev/null +++ b/env @@ -0,0 +1,6 @@ +# F2B_COLLECTOR_SOCKET="" +# F2B_COLLECTOR_TEXT_PATH="" +# F2B_WEB_LISTEN_ADDRESS="" +# F2B_WEB_BASICAUTH_USER="" +# F2B_WEB_BASICAUTH_PASS="" +# F2B_EXIT_ON_SOCKET_CONN_ERROR="" diff --git a/exporter.go b/exporter.go new file mode 100644 index 0000000..e748914 --- /dev/null +++ b/exporter.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg" + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/f2b" + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile" + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/server" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" + builtBy = "unknown" +) + +func printAppVersion() { + fmt.Println(version) + fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) +} + +func main() { + appSettings := cfg.Parse() + if appSettings.VersionMode { + printAppVersion() + return + } + + handleGracefulShutdown() + log.Printf("fail2ban exporter version %s", version) + log.Printf("starting server at %s", appSettings.MetricsAddress) + + f2bCollector := f2b.NewExporter(appSettings, version) + prometheus.MustRegister(f2bCollector) + + textFileCollector := textfile.NewCollector(appSettings) + prometheus.MustRegister(textFileCollector) + + if !appSettings.DryRunMode { + svrErr := server.StartServer(appSettings, textFileCollector) + err := <-svrErr + log.Fatal(err) + } else { + log.Print("running in dry-run mode - exiting") + } +} + +func handleGracefulShutdown() { + var signals = make(chan os.Signal, 1) + + signal.Notify(signals, syscall.SIGTERM) + signal.Notify(signals, syscall.SIGINT) + + go func() { + sig := <-signals + log.Printf("caught signal: %+v", sig) + os.Exit(0) + }() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2bdf344 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter + +go 1.20 + +require ( + github.com/alecthomas/kong v0.8.0 + github.com/kisielk/og-rek v1.2.0 + github.com/nlpodyssey/gopickle v0.2.0 + github.com/prometheus/client_golang v1.16.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5259440 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s= +github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/kisielk/og-rek v1.2.0 h1:CTvDIin+YnetsSQAYbe+QNAxXU3B50C5hseEz8xEoJw= +github.com/kisielk/og-rek v1.2.0/go.mod h1:6ihsOSzSAxR/65S3Bn9zNihoEqRquhDQZ2c6I2+MG3c= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/nlpodyssey/gopickle v0.2.0 h1:4naD2DVylYJupQLbCQFdwo6yiXEmPyp+0xf5MVlrBDY= +github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= +github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/manifest.tmpl b/manifest.tmpl new file mode 100644 index 0000000..ac9c9eb --- /dev/null +++ b/manifest.tmpl @@ -0,0 +1,26 @@ +image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} + - "latest" +{{/if}} +manifests: + - + image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-amd64 + platform: + architecture: amd64 + os: linux + - + image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-arm-v7 + platform: + architecture: arm + os: linux + variant: v7 + - + image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-arm64-v8 + platform: + architecture: arm64 + os: linux + variant: v8 diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..7bdf16b --- /dev/null +++ b/server/auth.go @@ -0,0 +1,17 @@ +package server + +import ( + "net/http" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth" +) + +func AuthMiddleware(handlerFunc http.HandlerFunc, authProvider auth.AuthProvider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if authProvider.IsAllowed(r) { + handlerFunc.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusUnauthorized) + } + } +} diff --git a/server/auth_test.go b/server/auth_test.go new file mode 100644 index 0000000..5a460c9 --- /dev/null +++ b/server/auth_test.go @@ -0,0 +1,46 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type testAuthProvider struct { + match bool +} + +func (p testAuthProvider) IsAllowed(request *http.Request) bool { + return p.match +} + +func newTestRequest() *http.Request { + return httptest.NewRequest(http.MethodGet, "http://example.com", nil) +} + +func executeAuthMiddlewareTest(t *testing.T, authMatches bool, expectedCode int, expectedCallCount int) { + callCount := 0 + testHandler := func(w http.ResponseWriter, r *http.Request) { + callCount++ + } + + handler := AuthMiddleware(testHandler, testAuthProvider{match: authMatches}) + recorder := httptest.NewRecorder() + request := newTestRequest() + handler.ServeHTTP(recorder, request) + + if recorder.Code != expectedCode { + t.Errorf("statusCode = %v, want %v", recorder.Code, expectedCode) + } + if callCount != expectedCallCount { + t.Errorf("callCount = %v, want %v", callCount, expectedCallCount) + } +} + +func Test_GIVEN_MatchingBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) { + executeAuthMiddlewareTest(t, true, http.StatusOK, 1) +} + +func Test_GIVEN_NonMatchingBasicAuth_WHEN_MethodCalled_THEN_RequestRejected(t *testing.T) { + executeAuthMiddlewareTest(t, false, http.StatusUnauthorized, 0) +} diff --git a/server/handler.go b/server/handler.go new file mode 100644 index 0000000..43a9011 --- /dev/null +++ b/server/handler.go @@ -0,0 +1,33 @@ +package server + +import ( + "log" + "net/http" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + metricsPath = "/metrics" +) + +func rootHtmlHandler(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte( + ` + Fail2Ban Exporter + +

Fail2Ban Exporter

+

Metrics

+ + `)) + if err != nil { + log.Printf("error handling root url: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func metricHandler(w http.ResponseWriter, r *http.Request, collector *textfile.Collector) { + promhttp.Handler().ServeHTTP(w, r) + collector.WriteTextFileMetrics(w, r) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..7bb229a --- /dev/null +++ b/server/server.go @@ -0,0 +1,41 @@ +package server + +import ( + "log" + "net/http" + "time" + + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg" + "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile" +) + +func StartServer( + appSettings *cfg.AppSettings, + textFileCollector *textfile.Collector, +) chan error { + http.HandleFunc("/", AuthMiddleware( + rootHtmlHandler, + appSettings.AuthProvider, + )) + http.HandleFunc(metricsPath, AuthMiddleware( + func(w http.ResponseWriter, r *http.Request) { + metricHandler(w, r, textFileCollector) + }, + appSettings.AuthProvider, + )) + log.Printf("metrics available at '%s'", metricsPath) + + svrErr := make(chan error) + go func() { + httpServer := &http.Server{ + Addr: appSettings.MetricsAddress, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 30 * time.Second, + } + svrErr <- httpServer.ListenAndServe() + }() + log.Print("ready") + return svrErr +} diff --git a/socket/decoder.go b/socket/decoder.go new file mode 100644 index 0000000..41f1ac4 --- /dev/null +++ b/socket/decoder.go @@ -0,0 +1,8 @@ +package socket + +// Py_builtins_str is used by the pickle decoder to parse the server response into a format Go can understand +type Py_builtins_str struct{} + +func (c Py_builtins_str) Call(args ...interface{}) (interface{}, error) { + return args[0], nil +} diff --git a/socket/fail2banSocket.go b/socket/fail2banSocket.go new file mode 100644 index 0000000..0288dd9 --- /dev/null +++ b/socket/fail2banSocket.go @@ -0,0 +1,179 @@ +package socket + +import ( + "fmt" + "github.com/kisielk/og-rek" + "github.com/nlpodyssey/gopickle/types" + "net" + "strings" +) + +type Fail2BanSocket struct { + socket net.Conn + encoder *ogórek.Encoder +} + +type JailStats struct { + FailedCurrent int + FailedTotal int + BannedCurrent int + BannedTotal int +} + +func ConnectToSocket(path string) (*Fail2BanSocket, error) { + c, err := net.Dial("unix", path) + if err != nil { + return nil, err + } + return &Fail2BanSocket{ + socket: c, + encoder: ogórek.NewEncoder(c), + }, nil +} + +func (s *Fail2BanSocket) Close() error { + return s.socket.Close() +} + +func (s *Fail2BanSocket) Ping() (bool, error) { + response, err := s.sendCommand([]string{pingCommand, "100"}) + if err != nil { + return false, newConnectionError(pingCommand, err) + } + + if t, ok := response.(*types.Tuple); ok { + if (*t)[1] == "pong" { + return true, nil + } + return false, fmt.Errorf("unexpected response data (expecting 'pong'): %s", (*t)[1]) + } + return false, newBadFormatError(pingCommand, response) +} + +func (s *Fail2BanSocket) GetJails() ([]string, error) { + response, err := s.sendCommand([]string{statusCommand}) + if err != nil { + return nil, err + } + + if lvl1, ok := response.(*types.Tuple); ok { + if lvl2, ok := lvl1.Get(1).(*types.List); ok { + if lvl3, ok := lvl2.Get(1).(*types.Tuple); ok { + if lvl4, ok := lvl3.Get(1).(string); ok { + splitJails := strings.Split(lvl4, ",") + return trimSpaceForAll(splitJails), nil + } + } + } + } + return nil, newBadFormatError(statusCommand, response) +} + +func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) { + response, err := s.sendCommand([]string{statusCommand, jail}) + if err != nil { + return JailStats{}, err + } + + stats := JailStats{ + FailedCurrent: -1, + FailedTotal: -1, + BannedCurrent: -1, + BannedTotal: -1, + } + + if lvl1, ok := response.(*types.Tuple); ok { + if lvl2, ok := lvl1.Get(1).(*types.List); ok { + if filter, ok := lvl2.Get(0).(*types.Tuple); ok { + if filterLvl1, ok := filter.Get(1).(*types.List); ok { + if filterCurrentTuple, ok := filterLvl1.Get(0).(*types.Tuple); ok { + if filterCurrent, ok := filterCurrentTuple.Get(1).(int); ok { + stats.FailedCurrent = filterCurrent + } + } + if filterTotalTuple, ok := filterLvl1.Get(1).(*types.Tuple); ok { + if filterTotal, ok := filterTotalTuple.Get(1).(int); ok { + stats.FailedTotal = filterTotal + } + } + } + } + if actions, ok := lvl2.Get(1).(*types.Tuple); ok { + if actionsLvl1, ok := actions.Get(1).(*types.List); ok { + if actionsCurrentTuple, ok := actionsLvl1.Get(0).(*types.Tuple); ok { + if actionsCurrent, ok := actionsCurrentTuple.Get(1).(int); ok { + stats.BannedCurrent = actionsCurrent + } + } + if actionsTotalTuple, ok := actionsLvl1.Get(1).(*types.Tuple); ok { + if actionsTotal, ok := actionsTotalTuple.Get(1).(int); ok { + stats.BannedTotal = actionsTotal + } + } + } + } + return stats, nil + } + } + return stats, newBadFormatError(statusCommand, response) +} + +func (s *Fail2BanSocket) GetJailBanTime(jail string) (int, error) { + command := fmt.Sprintf(banTimeCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + +func (s *Fail2BanSocket) GetJailFindTime(jail string) (int, error) { + command := fmt.Sprintf(findTimeCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + +func (s *Fail2BanSocket) GetJailMaxRetries(jail string) (int, error) { + command := fmt.Sprintf(maxRetriesCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + +func (s *Fail2BanSocket) GetServerVersion() (string, error) { + response, err := s.sendCommand([]string{versionCommand}) + if err != nil { + return "", err + } + + if lvl1, ok := response.(*types.Tuple); ok { + if versionStr, ok := lvl1.Get(1).(string); ok { + return versionStr, nil + } + } + return "", newBadFormatError(versionCommand, response) +} + +// sendSimpleIntCommand sends a command to the fail2ban socket and parses the response to extract an int. +// This command assumes that the response data is in the format of `(d, d)` where `d` is a number. +func (s *Fail2BanSocket) sendSimpleIntCommand(command string) (int, error) { + response, err := s.sendCommand(strings.Split(command, " ")) + if err != nil { + return -1, err + } + + if lvl1, ok := response.(*types.Tuple); ok { + if banTime, ok := lvl1.Get(1).(int); ok { + return banTime, nil + } + } + return -1, newBadFormatError(command, response) +} + +func newBadFormatError(command string, data interface{}) error { + return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data) +} + +func newConnectionError(command string, err error) error { + return fmt.Errorf("(%s) failed to send command through socket: %v", command, err) +} + +func trimSpaceForAll(slice []string) []string { + for i := range slice { + slice[i] = strings.TrimSpace(slice[i]) + } + return slice +} diff --git a/socket/protocol.go b/socket/protocol.go new file mode 100644 index 0000000..e350e7b --- /dev/null +++ b/socket/protocol.go @@ -0,0 +1,69 @@ +package socket + +import ( + "bufio" + "bytes" + "fmt" + "github.com/nlpodyssey/gopickle/pickle" +) + +const ( + commandTerminator = "" + pingCommand = "ping" + statusCommand = "status" + versionCommand = "version" + banTimeCommandFmt = "get %s bantime" + findTimeCommandFmt = "get %s findtime" + maxRetriesCommandFmt = "get %s maxretry" + socketReadBufferSize = 1024 +) + +func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) { + err := s.write(command) + if err != nil { + return nil, err + } + return s.read() +} + +func (s *Fail2BanSocket) write(command []string) error { + err := s.encoder.Encode(command) + if err != nil { + return err + } + _, err = s.socket.Write([]byte(commandTerminator)) + if err != nil { + return err + } + return nil +} + +func (s *Fail2BanSocket) read() (interface{}, error) { + reader := bufio.NewReader(s.socket) + + data := []byte{} + for { + buf := make([]byte, socketReadBufferSize) + _, err := reader.Read(buf) + if err != nil { + return nil, err + } + data = append(data, buf...) + containsTerminator := bytes.Contains(data, []byte(commandTerminator)) + if containsTerminator { + break + } + } + + bufReader := bytes.NewReader(data) + unpickler := pickle.NewUnpickler(bufReader) + + unpickler.FindClass = func(module, name string) (interface{}, error) { + if (module == "builtins" || module == "__builtin__") && name == "str" { + return &Py_builtins_str{}, nil + } + return nil, fmt.Errorf("class not found: " + module + " : " + name) + } + + return unpickler.Load() +} diff --git a/systemd/systemd.service b/systemd/systemd.service new file mode 100644 index 0000000..1622c9f --- /dev/null +++ b/systemd/systemd.service @@ -0,0 +1,22 @@ +[Unit] +Description=Prometheus exporter for fail2ban metrics +Requires=network-online.target +After=network-online.target + +[Service] +EnvironmentFile=/etc/conf.d/EXECUTABLE +ExecStart=/usr/bin/EXECUTABLE +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=5s + +NoNewPrivileges=true + +# NOTE: Would be great to create and use a dedicated user/group via +# sysusers.conf to access the fail2ban socket, but currently it is no possible +# without manual configuration of the fail2ban daemon. +User=root +Group=root + +[Install] +WantedBy=multi-user.target