8 Commits

Author SHA1 Message Date
d3m0k1d
8b6dc88233 chore: fix cd
All checks were successful
CD - BanForge Release / release (push) Successful in 28s
CI.yml / build (push) Successful in 3m43s
CD - BanForge Release / build (amd64, linux) (push) Successful in 3m8s
CD - BanForge Release / build (arm64, linux) (push) Successful in 2m8s
2026-01-14 14:40:48 +03:00
d3m0k1d
511b708737 chore: fix cd from fratifact to generic pakage
All checks were successful
CD - BanForge Release / release (push) Successful in 31s
CI.yml / build (push) Successful in 3m24s
CD - BanForge Release / build (amd64, linux) (push) Successful in 2m53s
CD - BanForge Release / build (arm64, linux) (push) Successful in 2m41s
2026-01-14 14:21:31 +03:00
d3m0k1d
803e9db7b4 chore: fix one more time
All checks were successful
CI.yml / build (push) Successful in 1m41s
2026-01-14 01:45:43 +03:00
d3m0k1d
12c40a5748 chore: Add upload artifacts
All checks were successful
CI.yml / build (push) Successful in 1m45s
2026-01-14 01:41:36 +03:00
d3m0k1d
24fe951e49 fix: judge creator, daemon logic
All checks were successful
CI.yml / build (push) Successful in 1m45s
feat: first version for alpha test daemon on server

fix: add second template for fix bug with slice

Fix: add chek if path exists

Fix: template one more time

feat: Add file db on init command

feat: add create dit

feat: Add to init command create table to db

feat: Add new logs for debug on server

feat: Add CD, first release version

chore:fix cd

fix: change artifact ver from v4->v2

fix: ci one more time

fix: ci
2026-01-14 01:21:30 +03:00
d3m0k1d
2d699af630 feat: add base daemon cli command
Some checks failed
CI.yml / build (push) Failing after 1m37s
2026-01-13 21:28:16 +03:00
d3m0k1d
17faaa5c27 Fix errchecl
All checks were successful
CI.yml / build (push) Successful in 1m45s
2026-01-13 21:03:50 +03:00
d3m0k1d
f0180b4bbe feat: fix db and recode judge 2026-01-13 21:03:10 +03:00
12 changed files with 322 additions and 60 deletions

67
.gitea/workflows/CD.yml Normal file
View 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

View File

@@ -3,8 +3,14 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"time"
"github.com/d3m0k1d/BanForge/internal/blocker"
"github.com/d3m0k1d/BanForge/internal/config" "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" "github.com/spf13/cobra"
) )
@@ -21,26 +27,169 @@ var initCmd = &cobra.Command{
Short: "Initialize BanForge", Short: "Initialize BanForge",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Initializing BanForge...") fmt.Println("Initializing BanForge...")
err := os.Mkdir("/var/log/banforge", 0750)
if err != nil { if _, err := os.Stat("/var/log/banforge"); err == nil {
fmt.Println(err) fmt.Println("/var/log/banforge already exists, skipping...")
os.Exit(1) } else if os.IsNotExist(err) {
} err := os.Mkdir("/var/log/banforge", 0750)
err = os.Mkdir("/etc/banforge", 0750) if err != nil {
if err != nil { fmt.Println(err)
fmt.Println(err) os.Exit(1)
os.Exit(1) }
} fmt.Println("Created /var/log/banforge")
err = config.CreateConf() } 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 { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("Config created")
err = config.FindFirewall() err = config.FindFirewall()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) 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() { func Execute() {
rootCmd.AddCommand(daemonCmd)
rootCmd.AddCommand(initCmd) rootCmd.AddCommand(initCmd)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) fmt.Println(err)

2
go.mod
View File

@@ -4,11 +4,11 @@ go 1.25.5
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
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-sqlite3 v1.14.33 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
) )

View File

@@ -50,6 +50,14 @@ func CreateConf() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to create rules file: %w", err) 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() { defer func() {
err = file.Close() err = file.Close()
if err != nil { if err != nil {
@@ -117,3 +125,12 @@ func FindFirewall() error {
return fmt.Errorf("firewall not found") 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
}

View File

@@ -9,8 +9,16 @@ name = ""
config = "/etc/nftables.conf" config = "/etc/nftables.conf"
ban_time = 1200 ban_time = 1200
[service] [[service]]
name = "nginx" name = "nginx"
log_path = "/var/log/nginx/access.log" log_path = "/var/log/nginx/access.log"
enabled = true enabled = true
[[service]]
name = "nginx"
log_path = "/var/log/nginx/access.log"
enabled = false
` `
// TODO: fix types for use 1 or any services"

View File

@@ -13,8 +13,8 @@ type Service struct {
} }
type Config struct { type Config struct {
Firewall Firewall `toml:"firewall"` Firewall Firewall `toml:"firewall"`
Service Service `toml:"service"` Service []Service `toml:"service"`
} }
// Rules // Rules

View File

@@ -1,4 +1,4 @@
package Judge package judge
import ( import (
"fmt" "fmt"
@@ -12,15 +12,16 @@ import (
type Judge struct { type Judge struct {
db *storage.DB db *storage.DB
logger *logger.Logger logger *logger.Logger
Blocker *blocker.BlockerEngine Blocker blocker.BlockerEngine
rulesByService map[string][]config.Rule rulesByService map[string][]config.Rule
} }
func New(db *storage.DB) *Judge { func New(db *storage.DB, b blocker.BlockerEngine) *Judge {
return &Judge{ return &Judge{
db: db, db: db,
logger: logger.New(false), logger: logger.New(false),
rulesByService: make(map[string][]config.Rule), 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") 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() rows, err := j.db.SearchUnViewed()
if err != nil { if err != nil {
j.logger.Error(fmt.Sprintf("Failed to query database: %v", err)) j.logger.Error(fmt.Sprintf("Failed to query database: %v", err))
return nil, err return err
} }
defer func() { defer func() {
err = rows.Close() err = rows.Close()
@@ -48,8 +49,6 @@ func (j *Judge) ProcessUnviewed() ([]storage.LogEntry, error) {
} }
}() }()
var entries []storage.LogEntry
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)
@@ -57,13 +56,37 @@ func (j *Judge) ProcessUnviewed() ([]storage.LogEntry, error) {
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
} }
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 { if err = rows.Err(); err != nil {
j.logger.Error(fmt.Sprintf("Error iterating rows: %v", err)) j.logger.Error(fmt.Sprintf("Error iterating rows: %v", err))
return nil, err return err
} }
return entries, nil return nil
} }

View File

@@ -35,12 +35,14 @@ func (p *NginxParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEn
method := matches[3] method := matches[3]
resultCh <- &storage.LogEntry{ resultCh <- &storage.LogEntry{
Service: "nginx", Service: "nginx",
IP: matches[1], IP: matches[1],
Path: &path, Path: path,
Status: &status, Status: status,
Method: &method, Method: method,
IsViewed: false,
} }
p.logger.Info("Parsed nginx log entry", "ip", matches[1], "path", path, "status", status, "method", method)
} }
}() }()
} }

View File

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

View File

@@ -49,3 +49,12 @@ func (d *DB) SearchUnViewed() (*sql.Rows, error) {
} }
return rows, nil return rows, nil
} }
func (d *DB) MarkAsViewed(id int) error {
_, err := d.db.Exec("UPDATE requests SET viewed = 1 WHERE id = ?", id)
if err != nil {
d.logger.Error("Failed to mark as viewed", "error", err)
return err
}
return nil
}

View File

@@ -1,19 +1,19 @@
package storage package storage
type LogEntry struct { type LogEntry struct {
ID int `db:"id"` ID int `db:"id"`
Service string `db:"service"` Service string `db:"service"`
IP string `db:"ip"` IP string `db:"ip"`
Path *string `db:"path"` Path string `db:"path"`
Status *string `db:"status"` Status string `db:"status"`
Method *string `db:"method"` Method string `db:"method"`
IsViewed *bool `db:"viewed"` IsViewed bool `db:"viewed"`
CreatedAt string `db:"created_at"` CreatedAt string `db:"created_at"`
} }
type Ban struct { type Ban struct {
ID int `db:"id"` ID int `db:"id"`
IP string `db:"ip"` IP string `db:"ip"`
Reason *string `db:"reason"` Reason string `db:"reason"`
BannedAt string `db:"banned_at"` BannedAt string `db:"banned_at"`
} }

View File

@@ -6,28 +6,13 @@ import (
func Write(db *DB, resultCh <-chan *LogEntry) { func Write(db *DB, resultCh <-chan *LogEntry) {
for result := range resultCh { 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( _, err := db.db.Exec(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
result.Service, result.Service,
result.IP, result.IP,
path, result.Path,
method, result.Method,
status, result.Status,
time.Now().Format(time.RFC3339), time.Now().Format(time.RFC3339),
) )
if err != nil { if err != nil {