5 Commits

Author SHA1 Message Date
Ilya Chernishev
5fd2a53541 revert: restore original ufw.go
All checks were successful
CI.yml / build (push) Successful in 51s
2026-01-12 17:53:39 +03:00
Ilya Chernishev
5dbacc765f revert: restore original nftables.go 2026-01-12 17:53:33 +03:00
Ilya Chernishev
d437512d24 revert: restore original interface.go 2026-01-12 17:53:17 +03:00
Ilya Chernishev
c1a3110b79 revert: remove factory.go 2026-01-12 17:53:13 +03:00
Ilya Chernishev
4e52d43d04 revert: remove blocker_factory_usage.go 2026-01-12 17:52:52 +03:00
33 changed files with 103 additions and 1412 deletions

View File

@@ -1,67 +0,0 @@
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

1
.gitignore vendored
View File

@@ -1 +0,0 @@
bin/

View File

@@ -25,6 +25,3 @@ clean:
test:
go test ./...
test-cover:
go test -cover ./...

View File

@@ -1,9 +1,7 @@
# BanForge
Log-based IPS system written in Go for Linux-based system.
Log-based IPS system written in Go for Linux based system.
[![Go Reference](https://pkg.go.dev/badge/github.com/d3m0k1d/BanForge/cmd/banforge.svg)](https://pkg.go.dev/github.com/d3m0k1d/BanForge)
[![License](https://img.shields.io/badge/license-%20%20GNU%20GPLv3%20-green?style=plastic)](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)
# Table of contents
1. [Overview](#overview)
2. [Requirements](#requirements)
@@ -14,40 +12,24 @@ Log-based IPS system written in Go for Linux-based system.
# Overview
BanForge is a simple IPS for replacement fail2ban in Linux system.
The project is currently in its early stages of development.
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) because Github has limits for Actions.
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) because Github have limit for Actions.
If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues).
## Roadmap
- [x] Real-time Nginx log monitoring
- [ ] Real-time Nginx log monitoring
- [ ] Add support for other service
- [ ] Add support for user service with regular expressions
- [ ] TUI interface
# Requirements
- Go 1.25+
- Go 1.21+
- ufw/iptables/nftables/firewalld
# Installation
Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it. Then create or copy a systemd unit file.
Or clone the repo and use the Makefile.
```
git clone https://gitea.d3m0k1d.ru/d3m0k1d/BanForge.git
cd BanForge
sudo make build-daemon
cd bin
```
currently no binary file if you wanna build the project yourself, you can use [Makefile](https://github.com/d3m0k1d/BanForge/blob/master/Makefile)
# Usage
For first steps use this commands
```bash
banforge init # Create config files and database
banforge daemon # Start BanForge daemon (use systemd or another init system to create a service)
```
You can edit the config file with examples in
- `/etc/banforge/config.toml` main config file
- `/etc/banforge/rules.toml` ban rules
For more information see the [docs](https://github.com/d3m0k1d/BanForge/docs).
# License
The project is licensed under the [GPL-3.0](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)

View File

@@ -1,17 +0,0 @@
[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

View File

@@ -1,91 +0,0 @@
package command
import (
"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 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
b = blocker.GetBlocker(fw, cfg.Firewall.Config)
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 {}
},
}

View File

@@ -1,84 +0,0 @@
package command
import (
"fmt"
"net"
"os"
"github.com/d3m0k1d/BanForge/internal/blocker"
"github.com/d3m0k1d/BanForge/internal/config"
"github.com/spf13/cobra"
)
var (
ip string
)
var UnbanCmd = &cobra.Command{
Use: "unban",
Short: "Unban IP",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.LoadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
fmt.Println("IP can't be empty")
os.Exit(1)
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = b.Unban(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("IP unblocked successfully!")
},
}
var BanCmd = &cobra.Command{
Use: "ban",
Short: "Ban IP",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.LoadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
fmt.Println("IP can't be empty")
os.Exit(1)
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = b.Ban(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("IP unblocked successfully!")
},
}
func FwRegister() {
BanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to ban")
UnbanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to unban")
}

View File

@@ -1,106 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/d3m0k1d/BanForge/internal/blocker"
"github.com/d3m0k1d/BanForge/internal/config"
"github.com/d3m0k1d/BanForge/internal/storage"
"github.com/spf13/cobra"
)
var InitCmd = &cobra.Command{
Use: "init",
Short: "Initialize BanForge",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Initializing BanForge...")
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)
}
cfg, err := config.LoadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
b := blocker.GetBlocker(cfg.Firewall.Name, cfg.Firewall.Config)
err = b.Setup(cfg.Firewall.Config)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Firewall configured")
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!")
},
}

View File

@@ -1,73 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/d3m0k1d/BanForge/internal/config"
"github.com/spf13/cobra"
)
var (
name string
service string
path string
status string
method string
)
var RuleCmd = &cobra.Command{
Use: "rule",
Short: "Manage rules",
}
var AddCmd = &cobra.Command{
Use: "add",
Short: "CLI interface for add new rule to file /etc/banforge/rules.toml",
Run: func(cmd *cobra.Command, args []string) {
if name == "" {
fmt.Printf("Rule name can't be empty\n")
os.Exit(1)
}
if service == "" {
fmt.Printf("Service name can't be empty\n")
os.Exit(1)
}
if path == "" && status == "" && method == "" {
fmt.Printf("At least 1 rule field must be filled in.")
os.Exit(1)
}
err := config.NewRule(name, service, path, status, method)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Rule added successfully!")
},
}
var ListCmd = &cobra.Command{
Use: "list",
Short: "List rules",
Run: func(cmd *cobra.Command, args []string) {
r, err := config.LoadRuleConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, rule := range r {
fmt.Printf("Name: %s\nService: %s\nPath: %s\nStatus: %s\nMethod: %s\n\n", rule.Name, rule.ServiceName, rule.Path, rule.Status, rule.Method)
}
},
}
func RuleRegister() {
RuleCmd.AddCommand(AddCmd)
RuleCmd.AddCommand(ListCmd)
AddCmd.Flags().StringVarP(&name, "name", "n", "", "rule name (required)")
AddCmd.Flags().StringVarP(&service, "service", "s", "", "service name")
AddCmd.Flags().StringVarP(&path, "path", "p", "", "request path")
AddCmd.Flags().StringVarP(&status, "status", "c", "", "HTTP status code")
AddCmd.Flags().StringVarP(&method, "method", "m", "", "HTTP method")
}

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"os"
"github.com/d3m0k1d/BanForge/cmd/banforge/command"
"github.com/spf13/cobra"
)
@@ -17,18 +15,30 @@ var rootCmd = &cobra.Command{
},
}
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize BanForge",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Initializing BanForge...")
err := os.Mkdir("/var/log/banforge", 0750)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = os.Mkdir("/etc/banforge", 0750)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
func Init() {
}
func Execute() {
rootCmd.AddCommand(command.DaemonCmd)
rootCmd.AddCommand(command.InitCmd)
rootCmd.AddCommand(command.RuleCmd)
rootCmd.AddCommand(command.BanCmd)
rootCmd.AddCommand(command.UnbanCmd)
command.RuleRegister()
command.FwRegister()
rootCmd.AddCommand(initCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)

View File

@@ -1,51 +0,0 @@
# CLI commands BanForge
BanForge provides a command-line interface (CLI) to manage IP blocking,
configure detection rules, and control the daemon process.
## Commands
### init - create a deps file
```shell
banforge init
```
**Description**
This command creates the necessary directories and base configuration files
required for the daemon to operate.
### daemon - Starts the BanForge daemon process
```shell
banforge daemon
```
**Description**
This command starts the BanForge daemon process in the background.
The daemon continuously monitors incoming requests, detects anomalies,
and applies firewall rules in real-time.
### firewall - Manages firewall rules
```shell
banforge ban <ip>
banforge unban <ip>
```
**Description**
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
### rule - Manages detection rules
```shell
banforge rule add -n rule.name -c 403
banforge rule list
```
**Description**
These command help you to create and manage detection rules in CLI interface.
| Flag | Required |
| ----------- | -------- |
| -n -name | + |
| -s -service | + |
| -p -path | - |
| -m -method | - |
| -c -status | - |
You must specify at least 1 of the optional flags to create a rule.

View File

6
go.mod
View File

@@ -2,11 +2,7 @@ module github.com/d3m0k1d/BanForge
go 1.25.5
require (
github.com/BurntSushi/toml v1.6.0
github.com/mattn/go-sqlite3 v1.14.33
github.com/spf13/cobra v1.10.2
)
require github.com/spf13/cobra v1.10.2
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect

4
go.sum
View File

@@ -1,10 +1,6 @@
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=

View File

@@ -57,7 +57,3 @@ func (f *Firewalld) Unban(ip string) error {
f.logger.Info("Reload " + string(output))
return nil
}
func (f *Firewalld) Setup(config string) error {
return nil
}

View File

@@ -1,28 +1,6 @@
package blocker
import (
"fmt"
"github.com/d3m0k1d/BanForge/internal/logger"
)
type BlockerEngine interface {
Ban(ip string) error
Unban(ip string) error
Setup(config string) error
}
func GetBlocker(fw string, config string) BlockerEngine {
switch fw {
case "ufw":
return NewUfw(logger.New(false))
case "iptables":
return NewIptables(logger.New(false), config)
case "nftables":
return NewNftables(logger.New(false), config)
case "firewalld":
return NewFirewalld(logger.New(false))
default:
panic(fmt.Sprintf("Unknown firewall: %s", fw))
}
}

View File

@@ -101,7 +101,3 @@ func (f *Iptables) Unban(ip string) error {
"output", string(output))
return nil
}
func (f *Iptables) Setup(config string) error {
return nil
}

View File

@@ -20,6 +20,58 @@ func NewNftables(logger *logger.Logger, config string) *Nftables {
}
}
func SetupNftables(config string) error {
err := validateConfigPath(config)
if err != nil {
return err
}
cmd := exec.Command("sudo", "nft", "list", "table", "inet", "banforge")
if err := cmd.Run(); err != nil {
cmd = exec.Command("sudo", "nft", "add", "table", "inet", "banforge")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to create table: %s", string(output))
}
}
cmd = exec.Command("sudo", "nft", "list", "chain", "inet", "banforge", "input")
if err := cmd.Run(); err != nil {
script := "sudo nft 'add chain inet banforge input { type filter hook input priority 0; policy accept; }'"
cmd = exec.Command("bash", "-c", script)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to create input chain: %s", string(output))
}
}
cmd = exec.Command("sudo", "nft", "list", "chain", "inet", "banforge", "banned")
if err := cmd.Run(); err != nil {
cmd = exec.Command("sudo", "nft", "add", "chain", "inet", "banforge", "banned")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to create banned chain: %s", string(output))
}
}
cmd = exec.Command("sudo", "nft", "-a", "list", "chain", "inet", "banforge", "input")
output, err := cmd.CombinedOutput()
if err == nil && !strings.Contains(string(output), "jump banned") {
cmd = exec.Command("sudo", "nft", "add", "rule", "inet", "banforge", "input", "jump", "banned")
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to add jump rule: %s", string(output))
}
}
err = saveNftablesConfig(config)
if err != nil {
return fmt.Errorf("failed to save nftables config: %w", err)
}
return nil
}
func (n *Nftables) Ban(ip string) error {
err := validateIP(ip)
if err != nil {
@@ -96,55 +148,6 @@ func (n *Nftables) Unban(ip string) error {
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()

View File

@@ -1,7 +1,6 @@
package blocker
import (
"fmt"
"os/exec"
"github.com/d3m0k1d/BanForge/internal/logger"
@@ -17,66 +16,32 @@ func NewUfw(logger *logger.Logger) *Ufw {
}
}
func (u *Ufw) Ban(ip string) error {
func (ufw *Ufw) Ban(ip string) error {
err := validateIP(ip)
if err != nil {
return err
}
cmd := exec.Command("sudo", "ufw", "--force", "deny", "from", ip)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to ban IP",
"ip", ip,
"error", err.Error(),
"output", string(output))
return fmt.Errorf("failed to ban IP %s: %w", ip, err)
ufw.logger.Error(err.Error())
return err
}
u.logger.Info("IP banned", "ip", ip, "output", string(output))
ufw.logger.Info("Banning " + ip + " " + string(output))
return nil
}
func (u *Ufw) Unban(ip string) error {
func (ufw *Ufw) Unban(ip string) error {
err := validateIP(ip)
if err != nil {
return err
}
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to unban IP",
"ip", ip,
"error", err.Error(),
"output", string(output))
return fmt.Errorf("failed to unban IP %s: %w", ip, err)
}
u.logger.Info("IP unbanned", "ip", ip, "output", string(output))
return nil
}
func (u *Ufw) Setup(config string) error {
if config != "" {
fmt.Printf("Ufw dont support config file\n")
cmd := exec.Command("sudo", "ufw", "enable")
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to enable ufw",
"error", err.Error(),
"output", string(output))
return fmt.Errorf("failed to enable ufw: %w", err)
}
}
if config == "" {
cmd := exec.Command("sudo", "ufw", "enable")
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to enable ufw",
"error", err.Error(),
"output", string(output))
return fmt.Errorf("failed to enable ufw: %w", err)
}
ufw.logger.Error(err.Error())
return err
}
ufw.logger.Info("Unbanning " + ip + " " + string(output))
return nil
}

View File

@@ -1,47 +0,0 @@
package blocker
import (
"testing"
)
func TestValidateConfigPath(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{name: "empty", input: "", wantErr: true},
{name: "valid path", input: "/path/to/config", wantErr: false},
{name: "invalid path", input: "path/to/config", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateConfigPath(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validateConfigPath(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestValidateIP(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{name: "empty", input: "", wantErr: true},
{name: "invalid IP", input: "1.1.1", wantErr: true},
{name: "valid IP", input: "1.1.1.1", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateIP(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validateIP(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}

View File

@@ -1,106 +0,0 @@
package config
import (
"fmt"
"os"
"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
}
func NewRule(Name string, ServiceName string, Path string, Status string, Method string) error {
r, err := LoadRuleConfig()
if err != nil {
r = []Rule{}
}
if Name == "" {
fmt.Printf("Rule name can't be empty\n")
return nil
}
r = append(r, Rule{Name: Name, ServiceName: ServiceName, Path: Path, Status: Status, Method: Method})
file, err := os.Create("/etc/banforge/rules.toml")
if err != nil {
return err
}
defer func() {
err = file.Close()
if err != nil {
fmt.Println(err)
}
}()
cfg := Rules{Rules: r}
err = toml.NewEncoder(file).Encode(cfg)
if err != nil {
return err
}
return nil
}
func EditRule(Name string, ServiceName string, Path string, Status string, Method string) error {
if Name == "" {
return fmt.Errorf("Rule name can't be empty")
}
r, err := LoadRuleConfig()
if err != nil {
return fmt.Errorf("rules is empty, please use 'banforge add rule' or create rules.toml")
}
found := false
for i, rule := range r {
if rule.Name == Name {
found = true
if ServiceName != "" {
r[i].ServiceName = ServiceName
}
if Path != "" {
r[i].Path = Path
}
if Status != "" {
r[i].Status = Status
}
if Method != "" {
r[i].Method = Method
}
break
}
}
if !found {
return fmt.Errorf("rule '%s' not found", Name)
}
file, err := os.Create("/etc/banforge/rules.toml")
if err != nil {
return err
}
defer func() {
err = file.Close()
if err != nil {
fmt.Println(err)
}
}()
cfg := Rules{Rules: r}
if err := toml.NewEncoder(file).Encode(cfg); err != nil {
return fmt.Errorf("failed to encode config: %w", err)
}
return nil
}

View File

@@ -5,8 +5,6 @@ import (
"os"
"os/exec"
"path/filepath"
"github.com/BurntSushi/toml"
)
var DetectedFirewall string
@@ -41,96 +39,31 @@ func CreateConf() error {
if err := os.Chmod(configPath, 0600); err != nil {
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)
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
}
func FindFirewall() error {
if os.Getegid() != 0 {
fmt.Printf("Firewall settings needs sudo privileges\n")
os.Exit(1)
}
firewalls := []string{"nft", "firewall-cmd", "iptables", "ufw"}
firewalls := []string{"iptables", "nft", "firewall-cmd", "ufw"}
for _, firewall := range firewalls {
_, err := exec.LookPath(firewall)
if err == nil {
switch firewall {
case "firewall-cmd":
if firewall == "firewall-cmd" {
DetectedFirewall = "firewalld"
case "nft":
}
if firewall == "nft" {
DetectedFirewall = "nftables"
default:
DetectedFirewall = 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)
DetectedFirewall = firewall
fmt.Printf("Detected firewall: %s\n", firewall)
return nil
}
}
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
return fmt.Errorf("no firewall found (checked ufw, firewall-cmd, iptables, nft) please install one of them")
}

View File

@@ -1,24 +1,15 @@
package config
const Base_config = `
# This is a TOML config file for BanForge
# [https://github.com/d3m0k1d/BanForge](https://github.com/d3m0k1d/BanForge)
const Base_config = `# This is a TOML config file for BanForge it's a simple config file
# https://github.com/d3m0k1d/BanForge
# Firewall settings block
[firewall]
name = ""
config = "/etc/nftables.conf"
name = "iptables" # Name one of the support firewall(iptables, nftables, firewalld, ufw)
ban_time = 1200
[[service]]
[Service]
name = "nginx"
log_path = "/var/log/nginx/access.log"
enabled = true
[[service]]
name = "nginx"
log_path = "/var/log/nginx/access.log"
enabled = false
`
// TODO: fix types for use 1 or any services"

View File

@@ -7,25 +7,7 @@ type Firewall struct {
}
type Service struct {
Name string `toml:"name"`
LogPath string `toml:"log_path"`
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"`
Name string `toml:"name"`
Log_path string `toml:"log_path"`
Enabled bool `toml:"enabled"`
}

View File

@@ -1,92 +0,0 @@
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
}

View File

@@ -1,48 +0,0 @@
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)
}
}()
}

View File

@@ -52,7 +52,6 @@ func (s *Scanner) Start() {
s.ch <- Event{
Data: s.scanner.Text(),
}
s.logger.Info("Scanner event", "data", s.scanner.Text())
} else {
if err := s.scanner.Err(); err != nil {
s.logger.Error("Scanner error")

View File

@@ -1,64 +0,0 @@
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?mode=rwc&_journal_mode=WAL&_busy_timeout=10000&cache=shared")
if err != nil {
return nil, err
}
if err := db.Ping(); 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
}

View File

@@ -1,177 +0,0 @@
package storage
import (
"database/sql"
"github.com/d3m0k1d/BanForge/internal/logger"
_ "github.com/mattn/go-sqlite3"
"os"
"path/filepath"
"testing"
"time"
)
func createTestDB(t *testing.T) *sql.DB {
tmpDir, err := os.MkdirTemp("", "banforge-test-*")
if err != nil {
t.Fatal(err)
}
filePath := filepath.Join(tmpDir, "test.db")
db, err := sql.Open("sqlite3", filePath)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
db.Close()
os.RemoveAll(tmpDir)
})
return db
}
func createTestDBStruct(t *testing.T) *DB {
tmpDir, err := os.MkdirTemp("", "banforge-test-*")
if err != nil {
t.Fatal(err)
}
filePath := filepath.Join(tmpDir, "test.db")
sqlDB, err := sql.Open("sqlite3", filePath)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
sqlDB.Close()
os.RemoveAll(tmpDir)
})
return &DB{
logger: logger.New(false),
db: sqlDB,
}
}
func TestCreateTable(t *testing.T) {
d := createTestDBStruct(t)
err := d.CreateTable()
if err != nil {
t.Fatal(err)
}
rows, err := d.db.Query("SELECT 1 FROM requests LIMIT 1")
if err != nil {
t.Fatal("requests table should exist:", err)
}
rows.Close()
rows, err = d.db.Query("SELECT 1 FROM bans LIMIT 1")
if err != nil {
t.Fatal("bans table should exist:", err)
}
rows.Close()
}
func TestMarkAsViewed(t *testing.T) {
d := createTestDBStruct(t)
err := d.CreateTable()
if err != nil {
t.Fatal(err)
}
_, err = d.db.Exec(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
"test",
"127.0.0.1",
"/test",
"GET",
"200",
time.Now().Format(time.RFC3339),
)
if err != nil {
t.Fatal(err)
}
err = d.MarkAsViewed(1)
if err != nil {
t.Fatal(err)
}
var isViewed bool
err = d.db.QueryRow("SELECT viewed FROM requests WHERE id = 1").Scan(&isViewed)
if err != nil {
t.Fatal(err)
}
if !isViewed {
t.Fatal("viewed should be true")
}
}
func TestSearchUnViewed(t *testing.T) {
d := createTestDBStruct(t)
err := d.CreateTable()
if err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
_, err := d.db.Exec(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
"test",
"127.0.0.1",
"/test",
"GET",
"200",
time.Now().Format(time.RFC3339),
)
if err != nil {
t.Fatal(err)
}
}
rows, err := d.SearchUnViewed()
if err != nil {
t.Fatal(err)
}
defer rows.Close()
count := 0
for rows.Next() {
var id int
var service, ip, path, status, method string
var viewed bool
var createdAt string
err := rows.Scan(&id, &service, &ip, &path, &status, &method, &viewed, &createdAt)
if err != nil {
t.Fatal(err)
}
if viewed {
t.Fatal("should be unviewed")
}
count++
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if count != 2 {
t.Fatalf("expected 2 unviewed requests, got %d", count)
}
}
func TestClose(t *testing.T) {
d := createTestDBStruct(t)
err := d.Close()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -1,28 +0,0 @@
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);
`

View File

@@ -1,19 +0,0 @@
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"`
}

View File

@@ -1,22 +0,0 @@
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)
}
}
}

View File

@@ -1,40 +0,0 @@
package storage
import (
"testing"
"time"
)
func TestWrite(t *testing.T) {
var ip string
d := createTestDBStruct(t)
err := d.CreateTable()
if err != nil {
t.Fatal(err)
}
resultCh := make(chan *LogEntry)
go Write(d, resultCh)
resultCh <- &LogEntry{
Service: "test",
IP: "127.0.0.1",
Path: "/test",
Method: "GET",
Status: "200",
}
close(resultCh)
time.Sleep(100 * time.Millisecond)
err = d.db.QueryRow("SELECT ip FROM requests LIMIT 1").Scan(&ip)
if err != nil {
t.Fatal(err)
}
if ip != "127.0.0.1" {
t.Fatal("ip should be 127.0.0.1")
}
}