Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3c0dbeaa | ||
|
|
46dc54f5a7 | ||
|
|
a1321300cb | ||
|
|
9eb1fa36c4 | ||
|
|
c954e929c8 | ||
|
|
1225c9323a | ||
|
|
847002129d | ||
|
|
6f24088069 | ||
|
|
03305a06f6 | ||
|
|
31184e009b | ||
|
|
914168f80f | ||
|
|
3a61371e58 | ||
|
|
d7d49ec0ed | ||
|
|
59e4393e82 | ||
|
|
bd73ba24e8 | ||
|
|
28d1410d62 | ||
|
|
680973df3d |
@@ -13,55 +13,33 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- name: Install syft
|
||||||
- name: Create Release
|
run: curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
|
||||||
env:
|
- name: Checkout
|
||||||
TOKEN: ${{ secrets.TOKEN }}
|
uses: actions/checkout@v6
|
||||||
run: |
|
- name: Go setup
|
||||||
TAG="${{ gitea.ref_name }}"
|
uses: actions/setup-go@v6
|
||||||
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:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
cache: false
|
cache: false
|
||||||
- run: go mod tidy
|
- name: Install deps
|
||||||
- run: go test ./...
|
run: go mod tidy
|
||||||
- name: Build ${{ matrix.goos }}-${{ matrix.arch }}
|
- 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:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
|
||||||
GOARCH: ${{ matrix.arch }}
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
run: go build -o banforge-${{ matrix.goos }}-${{ matrix.arch }} ./cmd/banforge
|
GITEA_TOKEN: ${{ secrets.TOKEN }}
|
||||||
- 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,4 +1,4 @@
|
|||||||
name: CI.yml
|
name: build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
bin/
|
bin/
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ linters:
|
|||||||
- govet
|
- govet
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- gosec
|
- gosec
|
||||||
|
- nilerr
|
||||||
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- 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"]
|
||||||
|
|
||||||
3
Makefile
3
Makefile
@@ -28,3 +28,6 @@ test:
|
|||||||
|
|
||||||
test-cover:
|
test-cover:
|
||||||
go test -cover ./...
|
go test -cover ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run --fix
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Log-based IPS system written in Go for Linux-based system.
|
|||||||
|
|
||||||
[](https://pkg.go.dev/github.com/d3m0k1d/BanForge)
|
[](https://pkg.go.dev/github.com/d3m0k1d/BanForge)
|
||||||
[](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)
|
[](https://github.com/d3m0k1d/BanForge/blob/master/LICENSE)
|
||||||
|
[](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/actions)
|
||||||
|

|
||||||
# Table of contents
|
# Table of contents
|
||||||
1. [Overview](#overview)
|
1. [Overview](#overview)
|
||||||
2. [Requirements](#requirements)
|
2. [Requirements](#requirements)
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
@@ -13,5 +13,9 @@ Restart=always
|
|||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=banforge
|
SyslogIdentifier=banforge
|
||||||
|
|
||||||
|
TimeoutStopSec=90
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/d3m0k1d/BanForge/internal/blocker"
|
"github.com/d3m0k1d/BanForge/internal/blocker"
|
||||||
@@ -17,6 +20,8 @@ var DaemonCmd = &cobra.Command{
|
|||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
Short: "Run BanForge daemon process",
|
Short: "Run BanForge daemon process",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
defer stop()
|
||||||
log := logger.New(false)
|
log := logger.New(false)
|
||||||
log.Info("Starting BanForge daemon")
|
log.Info("Starting BanForge daemon")
|
||||||
db, err := storage.NewDB()
|
db, err := storage.NewDB()
|
||||||
@@ -45,6 +50,7 @@ var DaemonCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
j := judge.New(db, b)
|
j := judge.New(db, b)
|
||||||
j.LoadRules(r)
|
j.LoadRules(r)
|
||||||
|
go j.UnbanChecker()
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -56,7 +62,15 @@ var DaemonCmd = &cobra.Command{
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
for _, svc := range cfg.Service {
|
for _, svc := range cfg.Service {
|
||||||
log.Info("Processing service", "name", svc.Name, "enabled", svc.Enabled, "path", svc.LogPath)
|
log.Info(
|
||||||
|
"Processing service",
|
||||||
|
"name",
|
||||||
|
svc.Name,
|
||||||
|
"enabled",
|
||||||
|
svc.Enabled,
|
||||||
|
"path",
|
||||||
|
svc.LogPath,
|
||||||
|
)
|
||||||
|
|
||||||
if !svc.Enabled {
|
if !svc.Enabled {
|
||||||
log.Info("Service disabled, skipping", "name", svc.Name)
|
log.Info("Service disabled, skipping", "name", svc.Name)
|
||||||
@@ -77,6 +91,7 @@ var DaemonCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
go pars.Start()
|
go pars.Start()
|
||||||
|
defer pars.Stop()
|
||||||
go func(p *parser.Scanner, serviceName string) {
|
go func(p *parser.Scanner, serviceName string) {
|
||||||
log.Info("Starting nginx parser", "service", serviceName)
|
log.Info("Starting nginx parser", "service", serviceName)
|
||||||
ng := parser.NewNginxParser()
|
ng := parser.NewNginxParser()
|
||||||
@@ -85,7 +100,7 @@ var DaemonCmd = &cobra.Command{
|
|||||||
go storage.Write(db, resultCh)
|
go storage.Write(db, resultCh)
|
||||||
}(pars, svc.Name)
|
}(pars, svc.Name)
|
||||||
}
|
}
|
||||||
|
<-ctx.Done()
|
||||||
select {}
|
log.Info("Shutdown signal received")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ var (
|
|||||||
path string
|
path string
|
||||||
status string
|
status string
|
||||||
method string
|
method string
|
||||||
|
ttl string
|
||||||
)
|
)
|
||||||
|
|
||||||
var RuleCmd = &cobra.Command{
|
var RuleCmd = &cobra.Command{
|
||||||
@@ -37,8 +38,10 @@ var AddCmd = &cobra.Command{
|
|||||||
fmt.Printf("At least 1 rule field must be filled in.")
|
fmt.Printf("At least 1 rule field must be filled in.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if ttl == "" {
|
||||||
err := config.NewRule(name, service, path, status, method)
|
ttl = "1y"
|
||||||
|
}
|
||||||
|
err := config.NewRule(name, service, path, status, method, ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -57,7 +60,14 @@ var ListCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
for _, rule := range r {
|
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)
|
fmt.Printf(
|
||||||
|
"Name: %s\nService: %s\nPath: %s\nStatus: %s\nMethod: %s\n\n",
|
||||||
|
rule.Name,
|
||||||
|
rule.ServiceName,
|
||||||
|
rule.Path,
|
||||||
|
rule.Status,
|
||||||
|
rule.Method,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -68,6 +78,7 @@ func RuleRegister() {
|
|||||||
AddCmd.Flags().StringVarP(&name, "name", "n", "", "rule name (required)")
|
AddCmd.Flags().StringVarP(&name, "name", "n", "", "rule name (required)")
|
||||||
AddCmd.Flags().StringVarP(&service, "service", "s", "", "service name")
|
AddCmd.Flags().StringVarP(&service, "service", "s", "", "service name")
|
||||||
AddCmd.Flags().StringVarP(&path, "path", "p", "", "request path")
|
AddCmd.Flags().StringVarP(&path, "path", "p", "", "request path")
|
||||||
AddCmd.Flags().StringVarP(&status, "status", "c", "", "HTTP status code")
|
AddCmd.Flags().StringVarP(&status, "status", "c", "", "status code")
|
||||||
AddCmd.Flags().StringVarP(&method, "method", "m", "", "HTTP method")
|
AddCmd.Flags().StringVarP(&method, "method", "m", "", "method")
|
||||||
|
AddCmd.Flags().StringVarP(&ttl, "ttl", "t", "", "ban time")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func Execute() {
|
|||||||
rootCmd.AddCommand(command.RuleCmd)
|
rootCmd.AddCommand(command.RuleCmd)
|
||||||
rootCmd.AddCommand(command.BanCmd)
|
rootCmd.AddCommand(command.BanCmd)
|
||||||
rootCmd.AddCommand(command.UnbanCmd)
|
rootCmd.AddCommand(command.UnbanCmd)
|
||||||
|
rootCmd.AddCommand(command.BanListCmd)
|
||||||
command.RuleRegister()
|
command.RuleRegister()
|
||||||
command.FwRegister()
|
command.FwRegister()
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
|||||||
10
docs/cli.md
10
docs/cli.md
@@ -31,6 +31,14 @@ banforge unban <ip>
|
|||||||
**Description**
|
**Description**
|
||||||
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
|
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
|
### rule - Manages detection rules
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -48,4 +56,6 @@ These command help you to create and manage detection rules in CLI interface.
|
|||||||
| -p -path | - |
|
| -p -path | - |
|
||||||
| -m -method | - |
|
| -m -method | - |
|
||||||
| -c -status | - |
|
| -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.
|
You must specify at least 1 of the optional flags to create a rule.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -4,11 +4,16 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
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/mattn/go-sqlite3 v1.14.33
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
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
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
19
go.sum
19
go.sum
@@ -1,15 +1,34 @@
|
|||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/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 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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=
|
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/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=
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
@@ -22,7 +25,14 @@ func LoadRuleConfig() ([]Rule, error) {
|
|||||||
return cfg.Rules, nil
|
return cfg.Rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRule(Name string, ServiceName string, Path string, Status string, Method string) error {
|
func NewRule(
|
||||||
|
Name string,
|
||||||
|
ServiceName string,
|
||||||
|
Path string,
|
||||||
|
Status string,
|
||||||
|
Method string,
|
||||||
|
ttl string,
|
||||||
|
) error {
|
||||||
r, err := LoadRuleConfig()
|
r, err := LoadRuleConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r = []Rule{}
|
r = []Rule{}
|
||||||
@@ -31,7 +41,17 @@ func NewRule(Name string, ServiceName string, Path string, Status string, Method
|
|||||||
fmt.Printf("Rule name can't be empty\n")
|
fmt.Printf("Rule name can't be empty\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r = append(r, Rule{Name: Name, ServiceName: ServiceName, Path: Path, Status: Status, Method: Method})
|
r = append(
|
||||||
|
r,
|
||||||
|
Rule{
|
||||||
|
Name: Name,
|
||||||
|
ServiceName: ServiceName,
|
||||||
|
Path: Path,
|
||||||
|
Status: Status,
|
||||||
|
Method: Method,
|
||||||
|
BanTime: ttl,
|
||||||
|
},
|
||||||
|
)
|
||||||
file, err := os.Create("/etc/banforge/rules.toml")
|
file, err := os.Create("/etc/banforge/rules.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -104,3 +124,31 @@ func EditRule(Name string, ServiceName string, Path string, Status string, Metho
|
|||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const Base_config = `
|
|||||||
[firewall]
|
[firewall]
|
||||||
name = ""
|
name = ""
|
||||||
config = "/etc/nftables.conf"
|
config = "/etc/nftables.conf"
|
||||||
ban_time = 1200
|
|
||||||
|
|
||||||
[[service]]
|
[[service]]
|
||||||
name = "nginx"
|
name = "nginx"
|
||||||
@@ -18,7 +17,4 @@ enabled = true
|
|||||||
name = "nginx"
|
name = "nginx"
|
||||||
log_path = "/var/log/nginx/access.log"
|
log_path = "/var/log/nginx/access.log"
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// TODO: fix types for use 1 or any services"
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
Config string `toml:"config"`
|
Config string `toml:"config"`
|
||||||
BanTime int `toml:"ban_time"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -28,4 +27,5 @@ type Rule struct {
|
|||||||
Path string `toml:"path"`
|
Path string `toml:"path"`
|
||||||
Status string `toml:"status"`
|
Status string `toml:"status"`
|
||||||
Method string `toml:"method"`
|
Method string `toml:"method"`
|
||||||
|
BanTime string `toml:"ban_time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package judge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/d3m0k1d/BanForge/internal/blocker"
|
"github.com/d3m0k1d/BanForge/internal/blocker"
|
||||||
"github.com/d3m0k1d/BanForge/internal/config"
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
@@ -48,10 +49,18 @@ func (j *Judge) ProcessUnviewed() error {
|
|||||||
j.logger.Error(fmt.Sprintf("Failed to close database connection: %v", err))
|
j.logger.Error(fmt.Sprintf("Failed to close database connection: %v", err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var entry storage.LogEntry
|
var entry storage.LogEntry
|
||||||
err = rows.Scan(&entry.ID, &entry.Service, &entry.IP, &entry.Path, &entry.Status, &entry.Method, &entry.IsViewed, &entry.CreatedAt)
|
err = rows.Scan(
|
||||||
|
&entry.ID,
|
||||||
|
&entry.Service,
|
||||||
|
&entry.IP,
|
||||||
|
&entry.Path,
|
||||||
|
&entry.Status,
|
||||||
|
&entry.Method,
|
||||||
|
&entry.IsViewed,
|
||||||
|
&entry.CreatedAt,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
j.logger.Error(fmt.Sprintf("Failed to scan database row: %v", err))
|
j.logger.Error(fmt.Sprintf("Failed to scan database row: %v", err))
|
||||||
continue
|
continue
|
||||||
@@ -64,12 +73,29 @@ func (j *Judge) ProcessUnviewed() error {
|
|||||||
(rule.Status == "" || entry.Status == rule.Status) &&
|
(rule.Status == "" || entry.Status == rule.Status) &&
|
||||||
(rule.Path == "" || entry.Path == rule.Path) {
|
(rule.Path == "" || entry.Path == rule.Path) {
|
||||||
|
|
||||||
j.logger.Info(fmt.Sprintf("Rule matched for IP: %s, Service: %s", entry.IP, entry.Service))
|
j.logger.Info(
|
||||||
err = j.Blocker.Ban(entry.IP)
|
fmt.Sprintf(
|
||||||
|
"Rule matched for IP: %s, Service: %s",
|
||||||
|
entry.IP,
|
||||||
|
entry.Service,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ban_status, err := j.db.IsBanned(entry.IP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
j.logger.Error(fmt.Sprintf("Failed to ban IP: %v", err))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
j.logger.Info(fmt.Sprintf("IP banned: %s", entry.IP))
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,3 +116,24 @@ func (j *Judge) ProcessUnviewed() error {
|
|||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,17 @@ func (p *NginxParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEn
|
|||||||
Method: method,
|
Method: method,
|
||||||
IsViewed: false,
|
IsViewed: false,
|
||||||
}
|
}
|
||||||
p.logger.Info("Parsed nginx log entry", "ip", matches[1], "path", path, "status", status, "method", method)
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,7 +19,10 @@ type DB struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDB() (*DB, error) {
|
func NewDB() (*DB, error) {
|
||||||
db, err := sql.Open("sqlite3", "/var/lib/banforge/storage.db?mode=rwc&_journal_mode=WAL&_busy_timeout=10000&cache=shared")
|
db, err := sql.Open(
|
||||||
|
"sqlite3",
|
||||||
|
"/var/lib/banforge/storage.db?mode=rwc&_journal_mode=WAL&_busy_timeout=10000&cache=shared",
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -46,7 +55,9 @@ func (d *DB) CreateTable() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) SearchUnViewed() (*sql.Rows, error) {
|
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")
|
rows, err := d.db.Query(
|
||||||
|
"SELECT id, service, ip, path, status, method, viewed, created_at FROM requests WHERE viewed = 0",
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Error("Failed to query database")
|
d.logger.Error("Failed to query database")
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -62,3 +73,96 @@ func (d *DB) MarkAsViewed(id int) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -167,6 +167,72 @@ func TestSearchUnViewed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestClose(t *testing.T) {
|
||||||
d := createTestDBStruct(t)
|
d := createTestDBStruct(t)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS bans (
|
|||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
ip TEXT UNIQUE NOT NULL,
|
ip TEXT UNIQUE NOT NULL,
|
||||||
reason TEXT,
|
reason TEXT,
|
||||||
banned_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
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_service ON requests(service);
|
||||||
|
|||||||
Reference in New Issue
Block a user