Compare commits
63 Commits
577f7ef0b9
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3c0dbeaa | ||
|
|
46dc54f5a7 | ||
|
|
a1321300cb | ||
|
|
9eb1fa36c4 | ||
|
|
c954e929c8 | ||
|
|
1225c9323a | ||
|
|
847002129d | ||
|
|
6f24088069 | ||
|
|
03305a06f6 | ||
|
|
31184e009b | ||
|
|
914168f80f | ||
|
|
3a61371e58 | ||
|
|
d7d49ec0ed | ||
|
|
59e4393e82 | ||
|
|
bd73ba24e8 | ||
|
|
28d1410d62 | ||
|
|
680973df3d | ||
|
|
1603fbee35 | ||
|
|
bbb152dfb8 | ||
|
|
a7b79d0e27 | ||
|
|
eaf276bd3f | ||
|
|
14c6c64989 | ||
|
|
623bd87b4c | ||
|
|
7d9645b3e3 | ||
|
|
bf6ff50da8 | ||
|
|
85f6919bda | ||
|
|
7a7f57f5ae | ||
|
|
36508201ad | ||
|
|
3cb9bcbcf3 | ||
|
|
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 |
45
.gitea/workflows/CD.yml
Normal file
45
.gitea/workflows/CD.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: CD - BanForge Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install syft
|
||||
run: curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Go setup
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: false
|
||||
- name: Install deps
|
||||
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
|
||||
run: go test ./...
|
||||
- name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
GITEA_TOKEN: ${{ secrets.TOKEN }}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI.yml
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -20,8 +20,11 @@ jobs:
|
||||
cache: false
|
||||
- name: Install deps
|
||||
run: go mod tidy
|
||||
- name: Run linter
|
||||
run: golangci-lint run ./...
|
||||
- name: Golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
with:
|
||||
args: --timeout=5m
|
||||
skip-cache: true
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
- name: Build
|
||||
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
dist/
|
||||
@@ -12,10 +12,12 @@ linters:
|
||||
- govet
|
||||
- staticcheck
|
||||
- gosec
|
||||
- nilerr
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
- golines
|
||||
|
||||
|
||||
|
||||
72
.goreleaser.yml
Normal file
72
.goreleaser.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
project_name: BanForge
|
||||
|
||||
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.d3m0k1d.ru/api/v1
|
||||
download: https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download
|
||||
skip_tls_verify: false
|
||||
|
||||
|
||||
builds:
|
||||
- id: banforge
|
||||
main: ./cmd/banforge/main.go
|
||||
binary: banforge-{{ .Version }}-{{ .Os }}-{{ .Arch }}
|
||||
ignore:
|
||||
- goos: windows
|
||||
- goos: darwin
|
||||
- goos: freebsd
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- "-s -w"
|
||||
archives:
|
||||
- format: tar.gz
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
nfpms:
|
||||
- id: banforge
|
||||
package_name: banforge
|
||||
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
homepage: https://gitea.d3m0k1d.ru/d3m0k1d/BanForge
|
||||
description: BanForge IPS log-based system
|
||||
maintainer: d3m0k1d <contact@d3m0k1d.ru>
|
||||
license: GPLv3.0
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
|
||||
release:
|
||||
gitea:
|
||||
owner: d3m0k1d
|
||||
name: BanForge
|
||||
mode: keep-existing
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
|
||||
algorithm: sha256
|
||||
|
||||
sboms:
|
||||
- artifacts: archive
|
||||
documents:
|
||||
- "{{ .ArtifactName }}.spdx.json"
|
||||
cmd: syft
|
||||
args: ["$artifact", "--output", "spdx-json=$document"]
|
||||
|
||||
6
Makefile
6
Makefile
@@ -25,3 +25,9 @@ clean:
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
test-cover:
|
||||
go test -cover ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run --fix
|
||||
|
||||
39
README.md
39
README.md
@@ -1,7 +1,11 @@
|
||||
# BanForge
|
||||
|
||||
Log-based IPS system written in Go for Linux based system.
|
||||
Log-based IPS system written in Go for Linux-based system.
|
||||
|
||||
[](https://pkg.go.dev/github.com/d3m0k1d/BanForge)
|
||||
[](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)
|
||||
[](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/actions)
|
||||

|
||||
# Table of contents
|
||||
1. [Overview](#overview)
|
||||
2. [Requirements](#requirements)
|
||||
@@ -12,11 +16,40 @@ 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 have limit for Actions.
|
||||
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) because Github has limits for Actions.
|
||||
If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues).
|
||||
|
||||
## Roadmap
|
||||
- [ ] Real-time Nginx log monitoring
|
||||
- [x] Real-time Nginx log monitoring
|
||||
- [ ] Add support for other service
|
||||
- [ ] Add support for user service with regular expressions
|
||||
- [ ] TUI interface
|
||||
|
||||
# Requirements
|
||||
|
||||
- Go 1.25+
|
||||
- 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
|
||||
```
|
||||
|
||||
# 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)
|
||||
|
||||
21
build/banforge
Normal file
21
build/banforge
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="BanForge - IPS log based system"
|
||||
command="/usr/bin/banforge"
|
||||
command_args="daemon"
|
||||
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
command_background="yes"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after network
|
||||
}
|
||||
|
||||
start_post() {
|
||||
einfo "BanForge is now running"
|
||||
}
|
||||
|
||||
stop_post() {
|
||||
einfo "BanForge is now stopped"
|
||||
}
|
||||
21
build/banforge.service
Normal file
21
build/banforge.service
Normal file
@@ -0,0 +1,21 @@
|
||||
[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
|
||||
|
||||
TimeoutStopSec=90
|
||||
KillSignal=SIGTERM
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
106
cmd/banforge/command/daemon.go
Normal file
106
cmd/banforge/command/daemon.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"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) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||
defer stop()
|
||||
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 j.UnbanChecker()
|
||||
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()
|
||||
defer pars.Stop()
|
||||
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)
|
||||
}
|
||||
<-ctx.Done()
|
||||
log.Info("Shutdown signal received")
|
||||
},
|
||||
}
|
||||
84
cmd/banforge/command/fw.go
Normal file
84
cmd/banforge/command/fw.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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")
|
||||
}
|
||||
106
cmd/banforge/command/init.go
Normal file
106
cmd/banforge/command/init.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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!")
|
||||
},
|
||||
}
|
||||
27
cmd/banforge/command/list.go
Normal file
27
cmd/banforge/command/list.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var BanListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List banned IP adresses",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var log = logger.New(false)
|
||||
d, err := storage.NewDB()
|
||||
if err != nil {
|
||||
log.Error("Failed to create database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = d.BanList()
|
||||
if err != nil {
|
||||
log.Error("Failed to get ban list", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
84
cmd/banforge/command/rule.go
Normal file
84
cmd/banforge/command/rule.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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
|
||||
ttl 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)
|
||||
}
|
||||
if ttl == "" {
|
||||
ttl = "1y"
|
||||
}
|
||||
err := config.NewRule(name, service, path, status, method, ttl)
|
||||
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", "", "status code")
|
||||
AddCmd.Flags().StringVarP(&method, "method", "m", "", "method")
|
||||
AddCmd.Flags().StringVarP(&ttl, "ttl", "t", "", "ban time")
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/cmd/banforge/command"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,30 +17,19 @@ 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(initCmd)
|
||||
rootCmd.AddCommand(command.DaemonCmd)
|
||||
rootCmd.AddCommand(command.InitCmd)
|
||||
rootCmd.AddCommand(command.RuleCmd)
|
||||
rootCmd.AddCommand(command.BanCmd)
|
||||
rootCmd.AddCommand(command.UnbanCmd)
|
||||
rootCmd.AddCommand(command.BanListCmd)
|
||||
command.RuleRegister()
|
||||
command.FwRegister()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
||||
61
docs/cli.md
Normal file
61
docs/cli.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
### list - Lists the IP addresses that are currently blocked
|
||||
```shell
|
||||
banforge list
|
||||
```
|
||||
|
||||
**Description**
|
||||
This command output table of IP addresses that are currently blocked
|
||||
|
||||
### 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 | - |
|
||||
| -t -ttl | -(if not used default ban 1 year) |
|
||||
|
||||
You must specify at least 1 of the optional flags to create a rule.
|
||||
45
docs/config.md
Normal file
45
docs/config.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Configs
|
||||
|
||||
## config.toml
|
||||
Main configuration file for BanForge.
|
||||
|
||||
Example:
|
||||
```toml
|
||||
[firewall]
|
||||
name = "nftables"
|
||||
config = "/etc/nftables.conf"
|
||||
|
||||
[[service]]
|
||||
name = "nginx"
|
||||
log_path = "/home/d3m0k1d/test.log"
|
||||
enabled = true
|
||||
|
||||
[[service]]
|
||||
name = "nginx"
|
||||
log_path = "/var/log/nginx/access.log"
|
||||
enabled = false
|
||||
```
|
||||
**Description**
|
||||
The [firewall] section defines firewall parameters. The banforge init command automatically detects your installed firewall (nftables, iptables, ufw, firewalld). For firewalls that require a configuration file, specify the path in the config parameter.
|
||||
|
||||
The [[service]] section is configured manually. Currently, only nginx is supported. To add a service, create a [[service]] block and specify the log_path to the nginx log file you want to monitor.
|
||||
|
||||
|
||||
## rules.toml
|
||||
Rules configuration file for BanForge.
|
||||
|
||||
If you wanna configure rules by cli command see [here](https://github.com/d3m0k1d/BanForge/blob/main/docs/cli.md)
|
||||
|
||||
Example:
|
||||
```toml
|
||||
[[rule]]
|
||||
name = "304 http"
|
||||
service = "nginx"
|
||||
path = ""
|
||||
status = "304"
|
||||
method = ""
|
||||
ban_time = "1m"
|
||||
```
|
||||
**Description**
|
||||
The [[rule]] section require name and one of the following parameters: service, path, status, method. To add a rule, create a [[rule]] block and specify the parameters.
|
||||
ban_time require in format "1m", "1h", "1d", "1M", "1y"
|
||||
11
go.mod
11
go.mod
@@ -2,9 +2,18 @@ module github.com/d3m0k1d/BanForge
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/spf13/cobra v1.10.2
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.7.8
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/spf13/cobra v1.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
23
go.sum
23
go.sum
@@ -1,11 +1,34 @@
|
||||
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
|
||||
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
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=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
63
internal/blocker/firewalld.go
Normal file
63
internal/blocker/firewalld.go
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (f *Firewalld) Setup(config string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
)
|
||||
|
||||
type BlockerEngine interface {
|
||||
Ban(ip string) error
|
||||
Unban(ip string) error
|
||||
IsBanned(ip string) (bool, error)
|
||||
Flush() 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))
|
||||
}
|
||||
}
|
||||
|
||||
107
internal/blocker/iptables.go
Normal file
107
internal/blocker/iptables.go
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (f *Iptables) Setup(config string) error {
|
||||
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,6 +1,7 @@
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
@@ -16,32 +17,66 @@ func NewUfw(logger *logger.Logger) *Ufw {
|
||||
}
|
||||
}
|
||||
|
||||
func (ufw *Ufw) Ban(ip string) error {
|
||||
func (u *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 {
|
||||
ufw.logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
ufw.logger.Info("Banning " + ip + " " + string(output))
|
||||
return 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)
|
||||
}
|
||||
|
||||
func (ufw *Ufw) Unban(ip string) error {
|
||||
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
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
ufw.logger.Error(err.Error())
|
||||
return err
|
||||
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.Info("Unbanning " + ip + " " + string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateIP(ip string) error {
|
||||
@@ -16,3 +19,21 @@ func validateIP(ip string) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
47
internal/blocker/validators_test.go
Normal file
47
internal/blocker/validators_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
154
internal/config/appconf.go
Normal file
154
internal/config/appconf.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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,
|
||||
ttl 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,
|
||||
BanTime: ttl,
|
||||
},
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
func ParseDurationWithYears(s string) (time.Duration, error) {
|
||||
if strings.HasSuffix(s, "y") {
|
||||
years, err := strconv.Atoi(strings.TrimSuffix(s, "y"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(years) * 365 * 24 * time.Hour, nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(s, "M") {
|
||||
months, err := strconv.Atoi(strings.TrimSuffix(s, "M"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(months) * 30 * 24 * time.Hour, nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(s, "d") {
|
||||
days, err := strconv.Atoi(strings.TrimSuffix(s, "d"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(days) * 24 * time.Hour, nil
|
||||
}
|
||||
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var DetectedFirewall string
|
||||
@@ -39,31 +41,96 @@ 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{"iptables", "nft", "firewall-cmd", "ufw"}
|
||||
|
||||
firewalls := []string{"nft", "firewall-cmd", "iptables", "ufw"}
|
||||
for _, firewall := range firewalls {
|
||||
_, err := exec.LookPath(firewall)
|
||||
if err == nil {
|
||||
if firewall == "firewall-cmd" {
|
||||
switch firewall {
|
||||
case "firewall-cmd":
|
||||
DetectedFirewall = "firewalld"
|
||||
}
|
||||
if firewall == "nft" {
|
||||
case "nft":
|
||||
DetectedFirewall = "nftables"
|
||||
}
|
||||
default:
|
||||
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 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,20 @@
|
||||
package config
|
||||
|
||||
const Base_config = `# This is a TOML config file for BanForge it's a simple config file
|
||||
# https://github.com/d3m0k1d/BanForge
|
||||
const Base_config = `
|
||||
# This is a TOML config file for BanForge
|
||||
# [https://github.com/d3m0k1d/BanForge](https://github.com/d3m0k1d/BanForge)
|
||||
|
||||
# Firewall settings block
|
||||
[firewall]
|
||||
name = "iptables" # Name one of the support firewall(iptables, nftables, firewalld, ufw)
|
||||
ban_time = 1200
|
||||
name = ""
|
||||
config = "/etc/nftables.conf"
|
||||
|
||||
[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
|
||||
`
|
||||
|
||||
@@ -2,11 +2,30 @@ package config
|
||||
|
||||
type Firewall struct {
|
||||
Name string `toml:"name"`
|
||||
BanTime int `toml:"ban_time"`
|
||||
Config string `toml:"config"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Name string `toml:"name"`
|
||||
Log_path string `toml:"log_path"`
|
||||
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"`
|
||||
BanTime string `toml:"ban_time"`
|
||||
}
|
||||
|
||||
139
internal/judge/judge.go
Normal file
139
internal/judge/judge.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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,
|
||||
),
|
||||
)
|
||||
ban_status, err := j.db.IsBanned(entry.IP)
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to check ban status: %v", err))
|
||||
return err
|
||||
}
|
||||
if !ban_status {
|
||||
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))
|
||||
err = j.db.AddBan(entry.IP, rule.BanTime)
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to add ban: %v", err))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (j *Judge) UnbanChecker() {
|
||||
tick := time.NewTicker(5 * time.Minute)
|
||||
defer tick.Stop()
|
||||
|
||||
for range tick.C {
|
||||
ips, err := j.db.CheckExpiredBans()
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to check expired bans: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if err := j.Blocker.Unban(ip); err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to unban IP %s: %v", ip, err))
|
||||
continue
|
||||
}
|
||||
j.logger.Info(fmt.Sprintf("IP unbanned: %s", ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
60
internal/judge/judge_test.go
Normal file
60
internal/judge/judge_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package judge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||
)
|
||||
|
||||
func TestJudgeLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputRule config.Rule
|
||||
inputLog storage.LogEntry
|
||||
wantErr bool
|
||||
wantMatch bool
|
||||
}{
|
||||
{
|
||||
name: "Empty rule",
|
||||
inputRule: config.Rule{Name: "", ServiceName: "", Path: "", Status: "", Method: ""},
|
||||
inputLog: storage.LogEntry{ID: 0, Service: "nginx", IP: "127.0.0.1", Path: "/api", Status: "200", Method: "GET", IsViewed: false, CreatedAt: ""},
|
||||
wantErr: true,
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "Matching rule",
|
||||
inputRule: config.Rule{Name: "test", ServiceName: "nginx", Path: "/api", Status: "200", Method: "GET"},
|
||||
inputLog: storage.LogEntry{ID: 1, Service: "nginx", IP: "127.0.0.1", Path: "/api", Status: "200", Method: "GET", IsViewed: false, CreatedAt: ""},
|
||||
wantErr: false,
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Non-matching status",
|
||||
inputRule: config.Rule{Name: "test", ServiceName: "nginx", Path: "/api", Status: "404", Method: "GET"},
|
||||
inputLog: storage.LogEntry{ID: 2, Service: "nginx", IP: "127.0.0.1", Path: "/api", Status: "200", Method: "GET", IsViewed: false, CreatedAt: ""},
|
||||
wantErr: false,
|
||||
wantMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.inputRule.Name == "" {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("Expected error for empty rule name, but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
result := (tt.inputRule.Method == "" || tt.inputLog.Method == tt.inputRule.Method) &&
|
||||
(tt.inputRule.Status == "" || tt.inputLog.Status == tt.inputRule.Status) &&
|
||||
(tt.inputRule.Path == "" || tt.inputLog.Path == tt.inputRule.Path) &&
|
||||
(tt.inputRule.ServiceName == "" || tt.inputLog.Service == tt.inputRule.ServiceName)
|
||||
|
||||
if result != tt.wantMatch {
|
||||
t.Errorf("Expected error: %v, but got: %v", tt.wantErr, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
internal/parser/NginxParser.go
Normal file
58
internal/parser/NginxParser.go
Normal file
@@ -0,0 +1,58 @@
|
||||
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,9 @@ type Scanner struct {
|
||||
}
|
||||
|
||||
func NewScanner(path string) (*Scanner, error) {
|
||||
file, err := os.Open(path) // #nosec G304 -- admin tool, runs as root, path controlled by operator
|
||||
file, err := os.Open(
|
||||
path,
|
||||
) // #nosec G304 -- admin tool, runs as root, path controlled by operator
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -52,6 +54,7 @@ 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")
|
||||
|
||||
168
internal/storage/db.go
Normal file
168
internal/storage/db.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
_ "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
|
||||
}
|
||||
|
||||
func (d *DB) IsBanned(ip string) (bool, error) {
|
||||
var bannedIP string
|
||||
err := d.db.QueryRow("SELECT ip FROM bans WHERE ip = ? ", ip).Scan(&bannedIP)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check ban status: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *DB) AddBan(ip string, ttl string) error {
|
||||
duration, err := config.ParseDurationWithYears(ttl)
|
||||
if err != nil {
|
||||
d.logger.Error("Invalid duration format", "ttl", ttl, "error", err)
|
||||
return fmt.Errorf("invalid duration: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiredAt := now.Add(duration)
|
||||
|
||||
_, err = d.db.Exec(
|
||||
"INSERT INTO bans (ip, reason, banned_at, expired_at) VALUES (?, ?, ?, ?)",
|
||||
ip,
|
||||
"1",
|
||||
now.Format(time.RFC3339),
|
||||
expiredAt.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to add ban", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) BanList() error {
|
||||
|
||||
var count int
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleBold)
|
||||
t.AppendHeader(table.Row{"№", "IP", "Banned At"})
|
||||
rows, err := d.db.Query("SELECT ip, banned_at FROM bans")
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
count++
|
||||
var ip string
|
||||
var bannedAt string
|
||||
err := rows.Scan(&ip, &bannedAt)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return err
|
||||
}
|
||||
t.AppendRow(table.Row{count, ip, bannedAt})
|
||||
|
||||
}
|
||||
t.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) CheckExpiredBans() ([]string, error) {
|
||||
var ips []string
|
||||
rows, err := d.db.Query(
|
||||
"SELECT ip FROM bans WHERE expired_at < ?",
|
||||
time.Now().Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
r, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
d.logger.Info("Ban removed", "ip", ip, "rows", r)
|
||||
err = rows.Scan(&ip)
|
||||
if err != nil {
|
||||
d.logger.Error("Failed to get ban list", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
243
internal/storage/db_test.go
Normal file
243
internal/storage/db_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
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 TestIsBanned(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("INSERT INTO bans (ip, banned_at) VALUES (?, ?)", "127.0.0.1", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
isBanned, err := d.IsBanned("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isBanned {
|
||||
t.Fatal("should be banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddBan(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = d.AddBan("127.0.0.1", "7h")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var ip string
|
||||
err = d.db.QueryRow("SELECT ip FROM bans WHERE ip = ?", "127.0.0.1").Scan(&ip)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ip != "127.0.0.1" {
|
||||
t.Fatal("ip should be 127.0.0.1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanList(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.CreateTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = d.db.Exec("INSERT INTO bans (ip, banned_at) VALUES (?, ?)", "127.0.0.1", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = d.BanList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
d := createTestDBStruct(t)
|
||||
|
||||
err := d.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
29
internal/storage/migrations.go
Normal file
29
internal/storage/migrations.go
Normal file
@@ -0,0 +1,29 @@
|
||||
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,
|
||||
expired_at DATETIME
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
internal/storage/writer_test.go
Normal file
40
internal/storage/writer_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user