Compare commits
36 Commits
424f5db9af
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6dc88233 | ||
|
|
511b708737 | ||
|
|
803e9db7b4 | ||
|
|
12c40a5748 | ||
|
|
24fe951e49 | ||
|
|
2d699af630 | ||
|
|
17faaa5c27 | ||
|
|
f0180b4bbe | ||
|
|
b2d03a4008 | ||
|
|
95a58dc780 | ||
|
|
0421d9ef40 | ||
|
|
5362761b82 | ||
|
|
9767bb70f1 | ||
|
|
b63da17043 | ||
|
|
fb66a23e33 | ||
|
|
db9c94f2c5 | ||
|
|
72018eb69e | ||
|
|
9e9505e8d5 | ||
|
|
11eac77f5b | ||
|
|
3732ef21d9 | ||
|
|
06ded14fb4 | ||
|
|
1c9a1f2d3e | ||
|
|
74dd666ff6 | ||
|
|
e4b9993748 | ||
|
|
9afe4ac1b9 | ||
|
|
dc915b1e17 | ||
|
|
1689340223 | ||
|
|
adff028281 | ||
|
|
36dcdca210 | ||
|
|
871965f437 | ||
|
|
7c0bdc2dfa | ||
|
|
41ff13fa66 | ||
|
|
99b97836ff | ||
|
|
b3431d248b | ||
|
|
577f7ef0b9 | ||
|
|
95ce6441d1 |
67
.gitea/workflows/CD.yml
Normal file
67
.gitea/workflows/CD.yml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: CD - BanForge Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.TOKEN }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
REPO="${{ gitea.repository }}"
|
||||||
|
SERVER="${{ gitea.server_url }}"
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tag_name": "'$TAG'",
|
||||||
|
"name": "Release '$TAG'",
|
||||||
|
"body": "# BanForge '$TAG'\n\nIntrusion Prevention System",
|
||||||
|
"draft": false,
|
||||||
|
"prerelease": false
|
||||||
|
}' \
|
||||||
|
"$SERVER/api/v1/repos/$REPO/releases"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: release
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
arch: amd64
|
||||||
|
- goos: linux
|
||||||
|
arch: arm64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
cache: false
|
||||||
|
- run: go mod tidy
|
||||||
|
- run: go test ./...
|
||||||
|
- name: Build ${{ matrix.goos }}-${{ matrix.arch }}
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
run: go build -o banforge-${{ matrix.goos }}-${{ matrix.arch }} ./cmd/banforge
|
||||||
|
- name: Upload ${{ matrix.goos }}-${{ matrix.arch }}
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.TOKEN }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
FILE="banforge-${{ matrix.goos }}-${{ matrix.arch }}"
|
||||||
|
curl --user d3m0k1d:$TOKEN \
|
||||||
|
--upload-file $FILE \
|
||||||
|
https://gitea.d3m0k1d.ru/api/packages/d3m0k1d/generic/banforge/$TAG/$FILE
|
||||||
@@ -20,6 +20,11 @@ jobs:
|
|||||||
cache: false
|
cache: false
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
- name: Golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9.2.0
|
||||||
|
with:
|
||||||
|
args: --timeout=5m
|
||||||
|
skip-cache: true
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bin/
|
||||||
21
.golangci.yml
Normal file
21
.golangci.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
tests: false
|
||||||
|
build-tags:
|
||||||
|
- integration
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- errname
|
||||||
|
- govet
|
||||||
|
- staticcheck
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
|
||||||
|
|
||||||
13
README.md
13
README.md
@@ -20,3 +20,16 @@ If you have any questions or suggestions, create issue on [Github](https://githu
|
|||||||
- [ ] Add support for other service
|
- [ ] Add support for other service
|
||||||
- [ ] Add support for user service with regular expressions
|
- [ ] Add support for user service with regular expressions
|
||||||
- [ ] TUI interface
|
- [ ] TUI interface
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- ufw/iptables/nftables/firewalld
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
currently no binary file if you wanna build the project yourself, you can use [Makefile](https://github.com/d3m0k1d/BanForge/blob/master/Makefile)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
# License
|
||||||
|
The project is licensed under the [GPL-3.0](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)
|
||||||
|
|||||||
17
build/banforge.service
Normal file
17
build/banforge.service
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=BanForge - IPS log based system
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
Documentation=https://github.com/d3m0k1d/BanForge
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/banforge daemon
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
Restart=always
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=banforge
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -2,8 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/blocker"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/judge"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/parser"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -19,8 +27,169 @@ var initCmd = &cobra.Command{
|
|||||||
Short: "Initialize BanForge",
|
Short: "Initialize BanForge",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("Initializing BanForge...")
|
fmt.Println("Initializing BanForge...")
|
||||||
os.Mkdir("/var/log/banforge", 0755)
|
|
||||||
os.Mkdir("/etc/banforge", 0755)
|
if _, err := os.Stat("/var/log/banforge"); err == nil {
|
||||||
|
fmt.Println("/var/log/banforge already exists, skipping...")
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
err := os.Mkdir("/var/log/banforge", 0750)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Created /var/log/banforge")
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/var/lib/banforge"); err == nil {
|
||||||
|
fmt.Println("/var/lib/banforge already exists, skipping...")
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
err := os.Mkdir("/var/lib/banforge", 0750)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Created /var/lib/banforge")
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat("/etc/banforge"); err == nil {
|
||||||
|
fmt.Println("/etc/banforge already exists, skipping...")
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
err := os.Mkdir("/etc/banforge", 0750)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Created /etc/banforge")
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.CreateConf()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Config created")
|
||||||
|
|
||||||
|
err = config.FindFirewall()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
db, err := storage.NewDB()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = db.CreateTable()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fmt.Println("Firewall detected and configured")
|
||||||
|
|
||||||
|
fmt.Println("BanForge initialized successfully!")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var daemonCmd = &cobra.Command{
|
||||||
|
Use: "daemon",
|
||||||
|
Short: "Run BanForge daemon process",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
log := logger.New(false)
|
||||||
|
log.Info("Starting BanForge daemon")
|
||||||
|
db, err := storage.NewDB()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to close database connection", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to load config", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var b blocker.BlockerEngine
|
||||||
|
fw := cfg.Firewall.Name
|
||||||
|
switch fw {
|
||||||
|
case "ufw":
|
||||||
|
b = blocker.NewUfw(log)
|
||||||
|
case "iptables":
|
||||||
|
b = blocker.NewIptables(log, cfg.Firewall.Config)
|
||||||
|
case "nftables":
|
||||||
|
b = blocker.NewNftables(log, cfg.Firewall.Config)
|
||||||
|
case "firewalld":
|
||||||
|
b = blocker.NewFirewalld(log)
|
||||||
|
default:
|
||||||
|
log.Error("Unknown firewall", "firewall", fw)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
r, err := config.LoadRuleConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to load rules", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
j := judge.New(db, b)
|
||||||
|
j.LoadRules(r)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
if err := j.ProcessUnviewed(); err != nil {
|
||||||
|
log.Error("Failed to process unviewed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, svc := range cfg.Service {
|
||||||
|
log.Info("Processing service", "name", svc.Name, "enabled", svc.Enabled, "path", svc.LogPath)
|
||||||
|
|
||||||
|
if !svc.Enabled {
|
||||||
|
log.Info("Service disabled, skipping", "name", svc.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Name != "nginx" {
|
||||||
|
log.Info("Only nginx supported, skipping", "name", svc.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Starting parser for service", "name", svc.Name, "path", svc.LogPath)
|
||||||
|
|
||||||
|
pars, err := parser.NewScanner(svc.LogPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create scanner", "service", svc.Name, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go pars.Start()
|
||||||
|
go func(p *parser.Scanner, serviceName string) {
|
||||||
|
log.Info("Starting nginx parser", "service", serviceName)
|
||||||
|
ng := parser.NewNginxParser()
|
||||||
|
resultCh := make(chan *storage.LogEntry, 100)
|
||||||
|
ng.Parse(p.Events(), resultCh)
|
||||||
|
go storage.Write(db, resultCh)
|
||||||
|
}(pars, svc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +198,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
rootCmd.AddCommand(daemonCmd)
|
||||||
rootCmd.AddCommand(initCmd)
|
rootCmd.AddCommand(initCmd)
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -2,7 +2,11 @@ module github.com/d3m0k1d/BanForge
|
|||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require github.com/spf13/cobra v1.10.2
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.33
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,6 +1,10 @@
|
|||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
|||||||
59
internal/blocker/firewalld.go
Normal file
59
internal/blocker/firewalld.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package blocker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Firewalld struct {
|
||||||
|
logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFirewalld(logger *logger.Logger) *Firewalld {
|
||||||
|
return &Firewalld{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Firewalld) Ban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("Add source " + ip + " " + string(output))
|
||||||
|
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("Reload " + string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Firewalld) Unban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("Remove source " + ip + " " + string(output))
|
||||||
|
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("Reload " + string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,4 @@ package blocker
|
|||||||
type BlockerEngine interface {
|
type BlockerEngine interface {
|
||||||
Ban(ip string) error
|
Ban(ip string) error
|
||||||
Unban(ip string) error
|
Unban(ip string) error
|
||||||
IsBanned(ip string) (bool, error)
|
|
||||||
Flush() error
|
|
||||||
}
|
}
|
||||||
|
|||||||
103
internal/blocker/iptables.go
Normal file
103
internal/blocker/iptables.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package blocker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Iptables struct {
|
||||||
|
logger *logger.Logger
|
||||||
|
config string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptables(logger *logger.Logger, config string) *Iptables {
|
||||||
|
return &Iptables{
|
||||||
|
logger: logger,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Iptables) Ban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = validateConfigPath(f.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("sudo", "iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("failed to ban IP",
|
||||||
|
"ip", ip,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("IP banned",
|
||||||
|
"ip", ip,
|
||||||
|
"output", string(output))
|
||||||
|
|
||||||
|
err = validateConfigPath(f.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||||
|
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("failed to save config",
|
||||||
|
"config_path", f.config,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("config saved",
|
||||||
|
"config_path", f.config,
|
||||||
|
"output", string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Iptables) Unban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = validateConfigPath(f.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("sudo", "iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("failed to unban IP",
|
||||||
|
"ip", ip,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("IP unbanned",
|
||||||
|
"ip", ip,
|
||||||
|
"output", string(output))
|
||||||
|
|
||||||
|
err = validateConfigPath(f.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// #nosec G204 - f.config is validated above via validateConfigPath()
|
||||||
|
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("failed to save config",
|
||||||
|
"config_path", f.config,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.logger.Info("config saved",
|
||||||
|
"config_path", f.config,
|
||||||
|
"output", string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
206
internal/blocker/nftables.go
Normal file
206
internal/blocker/nftables.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package blocker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Nftables struct {
|
||||||
|
logger *logger.Logger
|
||||||
|
config string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNftables(logger *logger.Logger, config string) *Nftables {
|
||||||
|
return &Nftables{
|
||||||
|
logger: logger,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nftables) Ban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sudo", "nft", "add", "rule", "inet", "banforge", "banned",
|
||||||
|
"ip", "saddr", ip, "drop")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error("failed to ban IP",
|
||||||
|
"ip", ip,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info("IP banned", "ip", ip)
|
||||||
|
|
||||||
|
err = saveNftablesConfig(n.config)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error("failed to save config",
|
||||||
|
"config_path", n.config,
|
||||||
|
"error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info("config saved", "config_path", n.config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nftables) Unban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := n.findRuleHandle(ip)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error("failed to find rule handle",
|
||||||
|
"ip", ip,
|
||||||
|
"error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if handle == "" {
|
||||||
|
n.logger.Warn("no rule found for IP", "ip", ip)
|
||||||
|
return fmt.Errorf("no rule found for IP %s", ip)
|
||||||
|
}
|
||||||
|
// #nosec G204 - handle is extracted from nftables output and validated
|
||||||
|
cmd := exec.Command("sudo", "nft", "delete", "rule", "inet", "banforge", "banned",
|
||||||
|
"handle", handle)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error("failed to unban IP",
|
||||||
|
"ip", ip,
|
||||||
|
"handle", handle,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info("IP unbanned", "ip", ip, "handle", handle)
|
||||||
|
|
||||||
|
err = saveNftablesConfig(n.config)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error("failed to save config",
|
||||||
|
"config_path", n.config,
|
||||||
|
"error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.logger.Info("config saved", "config_path", n.config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nftables) Setup(config string) error {
|
||||||
|
err := validateConfigPath(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("path error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nftConfig := `table inet banforge {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0
|
||||||
|
policy accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain banned {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
cmd := exec.Command("sudo", "tee", config)
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start tee command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stdin.Write([]byte(nftConfig))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config: %w", err)
|
||||||
|
}
|
||||||
|
err = stdin.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cmd.Wait(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("sudo", "nft", "-f", config)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load nftables config: %s", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nftables) findRuleHandle(ip string) (string, error) {
|
||||||
|
cmd := exec.Command("sudo", "nft", "-a", "list", "chain", "inet", "banforge", "banned")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to list chain rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, ip) && strings.Contains(line, "drop") {
|
||||||
|
if idx := strings.Index(line, "# handle"); idx != -1 {
|
||||||
|
parts := strings.Fields(line[idx:])
|
||||||
|
if len(parts) >= 3 && parts[1] == "handle" {
|
||||||
|
return parts[2], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveNftablesConfig(configPath string) error {
|
||||||
|
err := validateConfigPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sudo", "nft", "list", "ruleset")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get nftables ruleset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("sudo", "tee", configPath)
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start tee command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stdin.Write(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to config file: %w", err)
|
||||||
|
}
|
||||||
|
err = stdin.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package blocker
|
package blocker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ufw struct {
|
type Ufw struct {
|
||||||
@@ -15,26 +17,41 @@ func NewUfw(logger *logger.Logger) *Ufw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ufw *Ufw) Ban(ip string) error {
|
func (u *Ufw) Ban(ip string) error {
|
||||||
validateIP(ip)
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command("sudo", "ufw", "--force", "deny", "from", ip)
|
cmd := exec.Command("sudo", "ufw", "--force", "deny", "from", ip)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ufw.logger.Error(err.Error())
|
u.logger.Error("failed to ban IP",
|
||||||
return err
|
"ip", ip,
|
||||||
}
|
"error", err.Error(),
|
||||||
ufw.logger.Info("Banning " + ip + " " + string(output))
|
"output", string(output))
|
||||||
return nil
|
return fmt.Errorf("failed to ban IP %s: %w", ip, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.logger.Info("IP banned", "ip", ip, "output", string(output))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (u *Ufw) Unban(ip string) error {
|
||||||
|
err := validateIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ufw *Ufw) Unban(ip string) error {
|
|
||||||
validateIP(ip)
|
|
||||||
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
|
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ufw.logger.Error(err.Error())
|
u.logger.Error("failed to unban IP",
|
||||||
return err
|
"ip", ip,
|
||||||
|
"error", err.Error(),
|
||||||
|
"output", string(output))
|
||||||
|
return fmt.Errorf("failed to unban IP %s: %w", ip, err)
|
||||||
}
|
}
|
||||||
ufw.logger.Info("Unbanning " + ip + " " + string(output))
|
|
||||||
|
u.logger.Info("IP unbanned", "ip", ip, "output", string(output))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package blocker
|
package blocker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateIP(ip string) error {
|
func validateIP(ip string) error {
|
||||||
@@ -16,3 +19,21 @@ func validateIP(ip string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateConfigPath(pathIn string) error {
|
||||||
|
if pathIn == "" {
|
||||||
|
return errors.New("config path cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := filepath.Clean(pathIn)
|
||||||
|
|
||||||
|
if !filepath.IsAbs(cleanPath) {
|
||||||
|
return fmt.Errorf("config path must be absolute, got: %s", cleanPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(cleanPath, "..") {
|
||||||
|
return fmt.Errorf("config path contains path traversal: %s", cleanPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
22
internal/config/appconf.go
Normal file
22
internal/config/appconf.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadRuleConfig() ([]Rule, error) {
|
||||||
|
log := logger.New(false)
|
||||||
|
var cfg Rules
|
||||||
|
|
||||||
|
_, err := toml.DecodeFile("/etc/banforge/rules.toml", &cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(fmt.Sprintf("failed to decode config: %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("loaded %d rules", len(cfg.Rules)))
|
||||||
|
return cfg.Rules, nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DetectedFirewall string
|
var DetectedFirewall string
|
||||||
@@ -19,10 +21,6 @@ func CreateConf() error {
|
|||||||
return fmt.Errorf("you must be root to run this command, use sudo/doas")
|
return fmt.Errorf("you must be root to run this command, use sudo/doas")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(ConfigDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create config directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath := filepath.Join(ConfigDir, ConfigFile)
|
configPath := filepath.Join(ConfigDir, ConfigFile)
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
@@ -30,40 +28,109 @@ func CreateConf() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(configPath)
|
file, err := os.Create("/etc/banforge/config.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create config file: %w", err)
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
err = file.Close()
|
||||||
if err := os.Chmod(configPath, 0644); err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := os.Chmod(configPath, 0600); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions: %w", err)
|
return fmt.Errorf("failed to set permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
err = os.WriteFile(configPath, []byte(Base_config), 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
|
}
|
||||||
fmt.Printf(" Config file created: %s\n", configPath)
|
fmt.Printf(" Config file created: %s\n", configPath)
|
||||||
|
file, err = os.Create("/etc/banforge/rules.toml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create rules file: %w", err)
|
||||||
|
}
|
||||||
|
file, err = os.Create("/var/lib/banforge/storage.db")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create database file: %w", err)
|
||||||
|
}
|
||||||
|
err = os.Chmod("/var/lib/banforge/storage.db", 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := os.Chmod(configPath, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Rules file created: %s\n", configPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFirewall() error {
|
func FindFirewall() error {
|
||||||
|
|
||||||
if os.Getegid() != 0 {
|
if os.Getegid() != 0 {
|
||||||
fmt.Printf("Firewall settings needs sudo privileges\n")
|
fmt.Printf("Firewall settings needs sudo privileges\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
firewalls := []string{"iptables", "nft", "firewall-cmd", "ufw"}
|
|
||||||
|
firewalls := []string{"nft", "firewall-cmd", "iptables", "ufw"}
|
||||||
for _, firewall := range firewalls {
|
for _, firewall := range firewalls {
|
||||||
_, err := exec.LookPath(firewall)
|
_, err := exec.LookPath(firewall)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if firewall == "firewall-cmd" {
|
switch firewall {
|
||||||
|
case "firewall-cmd":
|
||||||
DetectedFirewall = "firewalld"
|
DetectedFirewall = "firewalld"
|
||||||
}
|
case "nft":
|
||||||
if firewall == "nft" {
|
|
||||||
DetectedFirewall = "nftables"
|
DetectedFirewall = "nftables"
|
||||||
}
|
default:
|
||||||
DetectedFirewall = firewall
|
DetectedFirewall = firewall
|
||||||
fmt.Printf("Detected firewall: %s\n", firewall)
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Detected firewall: %s\n", DetectedFirewall)
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
_, err := toml.DecodeFile("/etc/banforge/config.toml", cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Firewall.Name = DetectedFirewall
|
||||||
|
|
||||||
|
file, err := os.Create("/etc/banforge/config.toml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := toml.NewEncoder(file)
|
||||||
|
if err := encoder.Encode(cfg); err != nil {
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close file: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to encode config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Config updated with firewall: %s\n", DetectedFirewall)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("no firewall found (checked ufw, firewall-cmd, iptables, nft) please install one of them")
|
|
||||||
|
return fmt.Errorf("firewall not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
cfg := &Config{}
|
||||||
|
_, err := toml.DecodeFile("/etc/banforge/config.toml", cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode config: %w", err)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
const Base_config = `# This is a TOML config file for BanForge it's a simple config file
|
const Base_config = `
|
||||||
# https://github.com/d3m0k1d/BanForge
|
# This is a TOML config file for BanForge
|
||||||
|
# [https://github.com/d3m0k1d/BanForge](https://github.com/d3m0k1d/BanForge)
|
||||||
|
|
||||||
# Firewall settings block
|
|
||||||
[firewall]
|
[firewall]
|
||||||
name = "iptables" # Name one of the support firewall(iptables, nftables, firewalld, ufw)
|
name = ""
|
||||||
|
config = "/etc/nftables.conf"
|
||||||
ban_time = 1200
|
ban_time = 1200
|
||||||
|
|
||||||
[Service]
|
[[service]]
|
||||||
name = "nginx"
|
name = "nginx"
|
||||||
log_path = "/var/log/nginx/access.log"
|
log_path = "/var/log/nginx/access.log"
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
[[service]]
|
||||||
|
name = "nginx"
|
||||||
|
log_path = "/var/log/nginx/access.log"
|
||||||
|
enabled = false
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// TODO: fix types for use 1 or any services"
|
||||||
|
|||||||
@@ -2,11 +2,30 @@ package config
|
|||||||
|
|
||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
Ban_time int `toml:ban_time`
|
Config string `toml:"config"`
|
||||||
|
BanTime int `toml:"ban_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
Log_path string `toml:"log_path"`
|
LogPath string `toml:"log_path"`
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Firewall Firewall `toml:"firewall"`
|
||||||
|
Service []Service `toml:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
type Rules struct {
|
||||||
|
Rules []Rule `toml:"rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
ServiceName string `toml:"service"`
|
||||||
|
Path string `toml:"path"`
|
||||||
|
Status string `toml:"status"`
|
||||||
|
Method string `toml:"method"`
|
||||||
|
}
|
||||||
|
|||||||
92
internal/judge/judge.go
Normal file
92
internal/judge/judge.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package judge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/blocker"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Judge struct {
|
||||||
|
db *storage.DB
|
||||||
|
logger *logger.Logger
|
||||||
|
Blocker blocker.BlockerEngine
|
||||||
|
rulesByService map[string][]config.Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *storage.DB, b blocker.BlockerEngine) *Judge {
|
||||||
|
return &Judge{
|
||||||
|
db: db,
|
||||||
|
logger: logger.New(false),
|
||||||
|
rulesByService: make(map[string][]config.Rule),
|
||||||
|
Blocker: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Judge) LoadRules(rules []config.Rule) {
|
||||||
|
j.rulesByService = make(map[string][]config.Rule)
|
||||||
|
for _, rule := range rules {
|
||||||
|
j.rulesByService[rule.ServiceName] = append(
|
||||||
|
j.rulesByService[rule.ServiceName],
|
||||||
|
rule,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
j.logger.Info("Rules loaded and indexed by service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Judge) ProcessUnviewed() error {
|
||||||
|
rows, err := j.db.SearchUnViewed()
|
||||||
|
if err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Failed to query database: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Failed to close database connection: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var entry storage.LogEntry
|
||||||
|
err = rows.Scan(&entry.ID, &entry.Service, &entry.IP, &entry.Path, &entry.Status, &entry.Method, &entry.IsViewed, &entry.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Failed to scan database row: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, serviceExists := j.rulesByService[entry.Service]
|
||||||
|
if serviceExists {
|
||||||
|
for _, rule := range rules {
|
||||||
|
if (rule.Method == "" || entry.Method == rule.Method) &&
|
||||||
|
(rule.Status == "" || entry.Status == rule.Status) &&
|
||||||
|
(rule.Path == "" || entry.Path == rule.Path) {
|
||||||
|
|
||||||
|
j.logger.Info(fmt.Sprintf("Rule matched for IP: %s, Service: %s", entry.IP, entry.Service))
|
||||||
|
err = j.Blocker.Ban(entry.IP)
|
||||||
|
if err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Failed to ban IP: %v", err))
|
||||||
|
}
|
||||||
|
j.logger.Info(fmt.Sprintf("IP banned: %s", entry.IP))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = j.db.MarkAsViewed(entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Failed to mark entry as viewed: %v", err))
|
||||||
|
} else {
|
||||||
|
j.logger.Info(fmt.Sprintf("Entry marked as viewed: ID=%d", entry.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
j.logger.Error(fmt.Sprintf("Error iterating rows: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -24,4 +24,3 @@ func New(debug bool) *Logger {
|
|||||||
Logger: slog.New(handler),
|
Logger: slog.New(handler),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
internal/parser/NginxParser.go
Normal file
48
internal/parser/NginxParser.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NginxParser struct {
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNginxParser() *NginxParser {
|
||||||
|
pattern := regexp.MustCompile(
|
||||||
|
`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*\[(.*?)\]\s+"(\w+)\s+(.*?)\s+HTTP.*"\s+(\d+)`,
|
||||||
|
)
|
||||||
|
return &NginxParser{
|
||||||
|
pattern: pattern,
|
||||||
|
logger: logger.New(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NginxParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEntry) {
|
||||||
|
// Group 1: IP, Group 2: Timestamp, Group 3: Method, Group 4: Path, Group 5: Status
|
||||||
|
go func() {
|
||||||
|
for event := range eventCh {
|
||||||
|
matches := p.pattern.FindStringSubmatch(event.Data)
|
||||||
|
if matches == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := matches[4]
|
||||||
|
status := matches[5]
|
||||||
|
method := matches[3]
|
||||||
|
|
||||||
|
resultCh <- &storage.LogEntry{
|
||||||
|
Service: "nginx",
|
||||||
|
IP: matches[1],
|
||||||
|
Path: path,
|
||||||
|
Status: status,
|
||||||
|
Method: method,
|
||||||
|
IsViewed: false,
|
||||||
|
}
|
||||||
|
p.logger.Info("Parsed nginx log entry", "ip", matches[1], "path", path, "status", status, "method", method)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ type Scanner struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewScanner(path string) (*Scanner, error) {
|
func NewScanner(path string) (*Scanner, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path) // #nosec G304 -- admin tool, runs as root, path controlled by operator
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,7 @@ func (s *Scanner) Start() {
|
|||||||
s.ch <- Event{
|
s.ch <- Event{
|
||||||
Data: s.scanner.Text(),
|
Data: s.scanner.Text(),
|
||||||
}
|
}
|
||||||
|
s.logger.Info("Scanner event", "data", s.scanner.Text())
|
||||||
} else {
|
} else {
|
||||||
if err := s.scanner.Err(); err != nil {
|
if err := s.scanner.Err(); err != nil {
|
||||||
s.logger.Error("Scanner error")
|
s.logger.Error("Scanner error")
|
||||||
@@ -67,7 +68,10 @@ func (s *Scanner) Start() {
|
|||||||
func (s *Scanner) Stop() {
|
func (s *Scanner) Stop() {
|
||||||
close(s.stopCh)
|
close(s.stopCh)
|
||||||
time.Sleep(150 * time.Millisecond)
|
time.Sleep(150 * time.Millisecond)
|
||||||
s.file.Close()
|
err := s.file.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to close file")
|
||||||
|
}
|
||||||
close(s.ch)
|
close(s.ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
internal/storage/db.go
Normal file
60
internal/storage/db.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
logger *logger.Logger
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB() (*DB, error) {
|
||||||
|
db, err := sql.Open("sqlite3", "/var/lib/banforge/storage.db")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &DB{
|
||||||
|
logger: logger.New(false),
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
d.logger.Info("Closing database connection")
|
||||||
|
err := d.db.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) CreateTable() error {
|
||||||
|
_, err := d.db.Exec(CreateTables)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.logger.Info("Created tables")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) SearchUnViewed() (*sql.Rows, error) {
|
||||||
|
rows, err := d.db.Query("SELECT id, service, ip, path, status, method, viewed, created_at FROM requests WHERE viewed = 0")
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Error("Failed to query database")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) MarkAsViewed(id int) error {
|
||||||
|
_, err := d.db.Exec("UPDATE requests SET viewed = 1 WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Error("Failed to mark as viewed", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
internal/storage/migrations.go
Normal file
28
internal/storage/migrations.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
const CreateTables = `
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS requests (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
service TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
path TEXT,
|
||||||
|
method TEXT,
|
||||||
|
status TEXT,
|
||||||
|
viewed BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bans (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
ip TEXT UNIQUE NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
banned_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_service ON requests(service);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ip ON requests(ip);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_status ON requests(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_created_at ON requests(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ban_ip ON bans(ip);
|
||||||
|
`
|
||||||
19
internal/storage/models.go
Normal file
19
internal/storage/models.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
type LogEntry struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Service string `db:"service"`
|
||||||
|
IP string `db:"ip"`
|
||||||
|
Path string `db:"path"`
|
||||||
|
Status string `db:"status"`
|
||||||
|
Method string `db:"method"`
|
||||||
|
IsViewed bool `db:"viewed"`
|
||||||
|
CreatedAt string `db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ban struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
IP string `db:"ip"`
|
||||||
|
Reason string `db:"reason"`
|
||||||
|
BannedAt string `db:"banned_at"`
|
||||||
|
}
|
||||||
22
internal/storage/writer.go
Normal file
22
internal/storage/writer.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Write(db *DB, resultCh <-chan *LogEntry) {
|
||||||
|
for result := range resultCh {
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
result.Service,
|
||||||
|
result.IP,
|
||||||
|
result.Path,
|
||||||
|
result.Method,
|
||||||
|
result.Status,
|
||||||
|
time.Now().Format(time.RFC3339),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Error("Failed to write to database", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user