Compare commits
8 Commits
b2d03a4008
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6dc88233 | ||
|
|
511b708737 | ||
|
|
803e9db7b4 | ||
|
|
12c40a5748 | ||
|
|
24fe951e49 | ||
|
|
2d699af630 | ||
|
|
17faaa5c27 | ||
|
|
f0180b4bbe |
67
.gitea/workflows/CD.yml
Normal file
67
.gitea/workflows/CD.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
name: CD - BanForge Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Create Release
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
run: |
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
REPO="${{ gitea.repository }}"
|
||||
SERVER="${{ gitea.server_url }}"
|
||||
curl -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tag_name": "'$TAG'",
|
||||
"name": "Release '$TAG'",
|
||||
"body": "# BanForge '$TAG'\n\nIntrusion Prevention System",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}' \
|
||||
"$SERVER/api/v1/repos/$REPO/releases"
|
||||
|
||||
build:
|
||||
needs: release
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
arch: amd64
|
||||
- goos: linux
|
||||
arch: arm64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: false
|
||||
- run: go mod tidy
|
||||
- run: go test ./...
|
||||
- name: Build ${{ matrix.goos }}-${{ matrix.arch }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: go build -o banforge-${{ matrix.goos }}-${{ matrix.arch }} ./cmd/banforge
|
||||
- name: Upload ${{ matrix.goos }}-${{ matrix.arch }}
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
run: |
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
FILE="banforge-${{ matrix.goos }}-${{ matrix.arch }}"
|
||||
curl --user d3m0k1d:$TOKEN \
|
||||
--upload-file $FILE \
|
||||
https://gitea.d3m0k1d.ru/api/packages/d3m0k1d/generic/banforge/$TAG/$FILE
|
||||
@@ -3,8 +3,14 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/blocker"
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
"github.com/d3m0k1d/BanForge/internal/judge"
|
||||
"github.com/d3m0k1d/BanForge/internal/logger"
|
||||
"github.com/d3m0k1d/BanForge/internal/parser"
|
||||
"github.com/d3m0k1d/BanForge/internal/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -21,26 +27,169 @@ var initCmd = &cobra.Command{
|
||||
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)
|
||||
}
|
||||
err = config.CreateConf()
|
||||
|
||||
if _, err := os.Stat("/var/log/banforge"); err == nil {
|
||||
fmt.Println("/var/log/banforge already exists, skipping...")
|
||||
} else if os.IsNotExist(err) {
|
||||
err := os.Mkdir("/var/log/banforge", 0750)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Created /var/log/banforge")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := os.Stat("/var/lib/banforge"); err == nil {
|
||||
fmt.Println("/var/lib/banforge already exists, skipping...")
|
||||
} else if os.IsNotExist(err) {
|
||||
err := os.Mkdir("/var/lib/banforge", 0750)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Created /var/lib/banforge")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/etc/banforge"); err == nil {
|
||||
fmt.Println("/etc/banforge already exists, skipping...")
|
||||
} else if os.IsNotExist(err) {
|
||||
err := os.Mkdir("/etc/banforge", 0750)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Created /etc/banforge")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := config.CreateConf()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Config created")
|
||||
|
||||
err = config.FindFirewall()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
db, err := storage.NewDB()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = db.CreateTable()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
fmt.Println("Firewall detected and configured")
|
||||
|
||||
fmt.Println("BanForge initialized successfully!")
|
||||
},
|
||||
}
|
||||
|
||||
var daemonCmd = &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Run BanForge daemon process",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log := logger.New(false)
|
||||
log.Info("Starting BanForge daemon")
|
||||
db, err := storage.NewDB()
|
||||
if err != nil {
|
||||
log.Error("Failed to create database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
log.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
}()
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Error("Failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var b blocker.BlockerEngine
|
||||
fw := cfg.Firewall.Name
|
||||
switch fw {
|
||||
case "ufw":
|
||||
b = blocker.NewUfw(log)
|
||||
case "iptables":
|
||||
b = blocker.NewIptables(log, cfg.Firewall.Config)
|
||||
case "nftables":
|
||||
b = blocker.NewNftables(log, cfg.Firewall.Config)
|
||||
case "firewalld":
|
||||
b = blocker.NewFirewalld(log)
|
||||
default:
|
||||
log.Error("Unknown firewall", "firewall", fw)
|
||||
os.Exit(1)
|
||||
}
|
||||
r, err := config.LoadRuleConfig()
|
||||
if err != nil {
|
||||
log.Error("Failed to load rules", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
j := judge.New(db, b)
|
||||
j.LoadRules(r)
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := j.ProcessUnviewed(); err != nil {
|
||||
log.Error("Failed to process unviewed", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, svc := range cfg.Service {
|
||||
log.Info("Processing service", "name", svc.Name, "enabled", svc.Enabled, "path", svc.LogPath)
|
||||
|
||||
if !svc.Enabled {
|
||||
log.Info("Service disabled, skipping", "name", svc.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if svc.Name != "nginx" {
|
||||
log.Info("Only nginx supported, skipping", "name", svc.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Starting parser for service", "name", svc.Name, "path", svc.LogPath)
|
||||
|
||||
pars, err := parser.NewScanner(svc.LogPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to create scanner", "service", svc.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go pars.Start()
|
||||
go func(p *parser.Scanner, serviceName string) {
|
||||
log.Info("Starting nginx parser", "service", serviceName)
|
||||
ng := parser.NewNginxParser()
|
||||
resultCh := make(chan *storage.LogEntry, 100)
|
||||
ng.Parse(p.Events(), resultCh)
|
||||
go storage.Write(db, resultCh)
|
||||
}(pars, svc.Name)
|
||||
}
|
||||
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +198,7 @@ func Init() {
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
rootCmd.AddCommand(initCmd)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,11 +4,11 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/spf13/cobra v1.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
)
|
||||
|
||||
@@ -50,6 +50,14 @@ func CreateConf() error {
|
||||
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 {
|
||||
@@ -117,3 +125,12 @@ func FindFirewall() error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,8 +9,16 @@ name = ""
|
||||
config = "/etc/nftables.conf"
|
||||
ban_time = 1200
|
||||
|
||||
[service]
|
||||
[[service]]
|
||||
name = "nginx"
|
||||
log_path = "/var/log/nginx/access.log"
|
||||
enabled = true
|
||||
|
||||
[[service]]
|
||||
name = "nginx"
|
||||
log_path = "/var/log/nginx/access.log"
|
||||
enabled = false
|
||||
|
||||
`
|
||||
|
||||
// TODO: fix types for use 1 or any services"
|
||||
|
||||
@@ -13,8 +13,8 @@ type Service struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Firewall Firewall `toml:"firewall"`
|
||||
Service Service `toml:"service"`
|
||||
Firewall Firewall `toml:"firewall"`
|
||||
Service []Service `toml:"service"`
|
||||
}
|
||||
|
||||
// Rules
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package Judge
|
||||
package judge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
type Judge struct {
|
||||
db *storage.DB
|
||||
logger *logger.Logger
|
||||
Blocker *blocker.BlockerEngine
|
||||
Blocker blocker.BlockerEngine
|
||||
rulesByService map[string][]config.Rule
|
||||
}
|
||||
|
||||
func New(db *storage.DB) *Judge {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +36,11 @@ func (j *Judge) LoadRules(rules []config.Rule) {
|
||||
j.logger.Info("Rules loaded and indexed by service")
|
||||
}
|
||||
|
||||
func (j *Judge) ProcessUnviewed() ([]storage.LogEntry, error) {
|
||||
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 nil, err
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = rows.Close()
|
||||
@@ -48,8 +49,6 @@ func (j *Judge) ProcessUnviewed() ([]storage.LogEntry, error) {
|
||||
}
|
||||
}()
|
||||
|
||||
var entries []storage.LogEntry
|
||||
|
||||
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)
|
||||
@@ -57,13 +56,37 @@ func (j *Judge) ProcessUnviewed() ([]storage.LogEntry, error) {
|
||||
j.logger.Error(fmt.Sprintf("Failed to scan database row: %v", err))
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
|
||||
rules, serviceExists := j.rulesByService[entry.Service]
|
||||
if serviceExists {
|
||||
for _, rule := range rules {
|
||||
if (rule.Method == "" || entry.Method == rule.Method) &&
|
||||
(rule.Status == "" || entry.Status == rule.Status) &&
|
||||
(rule.Path == "" || entry.Path == rule.Path) {
|
||||
|
||||
j.logger.Info(fmt.Sprintf("Rule matched for IP: %s, Service: %s", entry.IP, entry.Service))
|
||||
err = j.Blocker.Ban(entry.IP)
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to ban IP: %v", err))
|
||||
}
|
||||
j.logger.Info(fmt.Sprintf("IP banned: %s", entry.IP))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = j.db.MarkAsViewed(entry.ID)
|
||||
if err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Failed to mark entry as viewed: %v", err))
|
||||
} else {
|
||||
j.logger.Info(fmt.Sprintf("Entry marked as viewed: ID=%d", entry.ID))
|
||||
}
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
j.logger.Error(fmt.Sprintf("Error iterating rows: %v", err))
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,12 +35,14 @@ func (p *NginxParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEn
|
||||
method := matches[3]
|
||||
|
||||
resultCh <- &storage.LogEntry{
|
||||
Service: "nginx",
|
||||
IP: matches[1],
|
||||
Path: &path,
|
||||
Status: &status,
|
||||
Method: &method,
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -52,6 +52,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")
|
||||
|
||||
@@ -49,3 +49,12 @@ func (d *DB) SearchUnViewed() (*sql.Rows, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,19 +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"`
|
||||
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"`
|
||||
ID int `db:"id"`
|
||||
IP string `db:"ip"`
|
||||
Reason string `db:"reason"`
|
||||
BannedAt string `db:"banned_at"`
|
||||
}
|
||||
|
||||
@@ -6,28 +6,13 @@ import (
|
||||
|
||||
func Write(db *DB, resultCh <-chan *LogEntry) {
|
||||
for result := range resultCh {
|
||||
path := ""
|
||||
if result.Path != nil {
|
||||
path = *result.Path
|
||||
}
|
||||
|
||||
status := ""
|
||||
if result.Status != nil {
|
||||
status = *result.Status
|
||||
}
|
||||
|
||||
method := ""
|
||||
if result.Method != nil {
|
||||
method = *result.Method
|
||||
}
|
||||
|
||||
_, err := db.db.Exec(
|
||||
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
result.Service,
|
||||
result.IP,
|
||||
path,
|
||||
method,
|
||||
status,
|
||||
result.Path,
|
||||
result.Method,
|
||||
result.Status,
|
||||
time.Now().Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user