44 Commits

Author SHA1 Message Date
d3m0k1d
d9df055765 feat: full working metrics ready
All checks were successful
build / build (push) Successful in 2m21s
CD - BanForge Release / release (push) Successful in 4m3s
2026-02-23 18:03:20 +03:00
d3m0k1d
6897ea8753 feat: new cli command and new logic for rules on dir
All checks were successful
build / build (push) Successful in 2m25s
2026-02-23 17:02:39 +03:00
d3m0k1d
d534fc79d7 feat: logic rules switch from one file to rules.d and refactoring init cli func
All checks were successful
build / build (push) Successful in 2m23s
2026-02-23 00:26:52 +03:00
d3m0k1d
9ad0a3eb12 fix: create db files on init func
All checks were successful
build / build (push) Successful in 2m25s
2026-02-22 23:26:50 +03:00
d3m0k1d
d8712037f4 feat: fix gorutines on sshd and new validators on parsers
All checks were successful
build / build (push) Successful in 2m22s
2026-02-22 23:18:07 +03:00
d3m0k1d
aef2647a82 chore: fix deps
All checks were successful
build / build (push) Successful in 2m28s
CD - BanForge Release / release (push) Successful in 4m10s
2026-02-22 19:52:22 +03:00
d3m0k1d
c3b6708a98 fix: gofmt
Some checks failed
build / build (push) Has been cancelled
2026-02-22 19:48:37 +03:00
d3m0k1d
3acd0b899c fix: linter run and gosec fix
Some checks failed
build / build (push) Failing after 1m52s
2026-02-22 19:45:59 +03:00
d3m0k1d
3ac1250bfc feat: add metrics support 2026-02-22 19:45:47 +03:00
d3m0k1d
7bba444522 feat: upgrade max_retry logic and change version
All checks were successful
build / build (push) Successful in 2m9s
2026-02-22 18:27:21 +03:00
d3m0k1d
97eb626237 fix: update libs
Some checks failed
CD - BanForge Release / release (push) Failing after 2m24s
2026-02-22 17:21:20 +03:00
d3m0k1d
b7a1ac06d4 feat: new ver
All checks were successful
CD - BanForge Release / release (push) Successful in 3m42s
2026-02-22 16:13:51 +03:00
d3m0k1d
49f0acb777 docs: update add to example max retry
All checks were successful
build / build (push) Successful in 2m8s
2026-02-22 16:12:52 +03:00
d3m0k1d
a602207369 feat: full working max_retry logic
All checks were successful
build / build (push) Successful in 2m45s
2026-02-22 16:06:51 +03:00
d3m0k1d
8c0cfcdbe7 refactoring: method on reader req db
All checks were successful
build / build (push) Successful in 2m8s
2026-02-19 12:36:56 +03:00
d3m0k1d
35a1a89baf fix: run tests in storage
All checks were successful
build / build (push) Successful in 2m6s
2026-02-19 11:22:52 +03:00
d3m0k1d
f3387b169a fix: gosec
Some checks failed
build / build (push) Failing after 1m59s
2026-02-19 11:17:51 +03:00
d3m0k1d
5782072f91 fix: ci one more time
Some checks failed
build / build (push) Failing after 1m42s
2026-02-19 11:14:45 +03:00
d3m0k1d
7918b3efe6 feat: add new nosec flags for fix ci
Some checks failed
build / build (push) Failing after 1m38s
2026-02-19 11:09:59 +03:00
d3m0k1d
f628e24f58 fix: golangci fix
Some checks failed
build / build (push) Failing after 1m40s
2026-02-19 11:03:52 +03:00
d3m0k1d
7f54db0cd4 feat: add new method and for db req and add to template max retry
Some checks failed
build / build (push) Failing after 1m48s
2026-02-19 10:53:55 +03:00
Ilya Chernishev
2e9b307194 Merge pull request #1 from shinyzero00/master
All checks were successful
build / build (push) Successful in 2m25s
refactoring pr by shinyzero00
2026-02-15 13:17:01 +03:00
Ilya Chernishev
726594a712 Change return value to nil on successful IP block 2026-02-15 13:13:26 +03:00
Ilya Chernishev
b27038a59c Execute SQL statement to create table in database 2026-02-15 13:08:40 +03:00
Ilya Chernishev
72025dab7d Remove comment about potential failure in encoding
Removed commented-out question regarding error handling.
2026-02-15 12:59:20 +03:00
Ilya Chernishev
dd131477e2 fix ST1005 2026-02-15 12:51:18 +03:00
Ilya Chernishev
670aec449a fix ST1005 staticcheck 2026-02-15 12:49:57 +03:00
zero@thinky
fc37e641be refactor(internal/config): use CutSuffix 2026-02-15 04:56:22 +03:00
zero@thinky
361de03208 refactor(cmd/fw): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
a2268fda5d fix(cmd/fw): why to fucking log when it is printed by the only caller 2026-02-15 04:56:22 +03:00
zero@thinky
9dc0b6002e refactor(internal/config): error handling 2026-02-15 04:56:22 +03:00
zero@thinky
4953be3ef6 refactor(internal/storage/RequestWriter/WriteReq): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
c386a2d6bc refactor(internal/storage/RequestWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
dea03a6f70 refactor(*): what the fuck is that naming 2026-02-15 04:54:38 +03:00
zero@thinky
11f755c03c style(internal/storage/BanWriter): rm extra newline 2026-02-15 04:54:38 +03:00
zero@thinky
1c7a1c1778 refactor(internal/storage/BanWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
411574cabe refactor(internal/storage): generalization and deduplication 2026-02-15 04:28:34 +03:00
d3m0k1d
820c9410a1 feat: update docs for new commands
All checks were successful
build / build (push) Successful in 2m8s
CD - BanForge Release / release (push) Successful in 3m46s
2026-02-09 22:27:28 +03:00
d3m0k1d
6f261803a7 feat: add to cli commands for open/close ports on firewall
All checks were successful
build / build (push) Successful in 2m2s
2026-02-09 21:51:31 +03:00
d3m0k1d
aacc98668f feat: add logic for PortClose and PortOpen on interfaces
All checks were successful
build / build (push) Successful in 2m4s
2026-02-09 21:31:19 +03:00
d3m0k1d
9519eedf4f feat: add new interface method to firewals
All checks were successful
build / build (push) Successful in 3m9s
2026-02-09 19:50:06 +03:00
d3m0k1d
b8b9b227a9 Fix: daemon chanels
All checks were successful
build / build (push) Successful in 3m9s
CD - BanForge Release / release (push) Successful in 5m9s
2026-01-27 17:10:01 +03:00
d3m0k1d
08d3214f22 Fix: goimport linter fix
All checks were successful
build / build (push) Successful in 3m27s
2026-01-27 17:04:36 +03:00
d3m0k1d
6ebda76738 feat: Add apache support
Some checks failed
build / build (push) Failing after 2m48s
2026-01-27 16:59:32 +03:00
31 changed files with 1638 additions and 450 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/d3m0k1d/BanForge/internal/config" "github.com/d3m0k1d/BanForge/internal/config"
"github.com/d3m0k1d/BanForge/internal/judge" "github.com/d3m0k1d/BanForge/internal/judge"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/d3m0k1d/BanForge/internal/parser" "github.com/d3m0k1d/BanForge/internal/parser"
"github.com/d3m0k1d/BanForge/internal/storage" "github.com/d3m0k1d/BanForge/internal/storage"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -30,6 +31,11 @@ var DaemonCmd = &cobra.Command{
log.Error("Failed to create request writer", "error", err) log.Error("Failed to create request writer", "error", err)
os.Exit(1) os.Exit(1)
} }
reqDb_r, err := storage.NewRequestsRd()
if err != nil {
log.Error("Failed to create request reader", "error", err)
os.Exit(1)
}
banDb_r, err := storage.NewBanReader() banDb_r, err := storage.NewBanReader()
if err != nil { if err != nil {
log.Error("Failed to create ban reader", "error", err) log.Error("Failed to create ban reader", "error", err)
@@ -55,6 +61,14 @@ var DaemonCmd = &cobra.Command{
log.Error("Failed to load config", "error", err) log.Error("Failed to load config", "error", err)
os.Exit(1) os.Exit(1)
} }
if cfg.Metrics.Enabled {
go func() {
if err := metrics.StartMetricsServer(cfg.Metrics.Port); err != nil {
log.Error("Failed to start metrics server", "error", err)
}
}()
}
var b blocker.BlockerEngine var b blocker.BlockerEngine
fw := cfg.Firewall.Name fw := cfg.Firewall.Name
b = blocker.GetBlocker(fw, cfg.Firewall.Config) b = blocker.GetBlocker(fw, cfg.Firewall.Config)
@@ -63,7 +77,7 @@ var DaemonCmd = &cobra.Command{
log.Error("Failed to load rules", "error", err) log.Error("Failed to load rules", "error", err)
os.Exit(1) os.Exit(1)
} }
j := judge.New(banDb_r, banDb_w, b, resultCh, entryCh) j := judge.New(banDb_r, banDb_w, reqDb_r, b, resultCh, entryCh)
j.LoadRules(r) j.LoadRules(r)
go j.UnbanChecker() go j.UnbanChecker()
go j.Tribunal() go j.Tribunal()
@@ -112,6 +126,11 @@ var DaemonCmd = &cobra.Command{
ssh := parser.NewSshdParser() ssh := parser.NewSshdParser()
ssh.Parse(p.Events(), entryCh) ssh.Parse(p.Events(), entryCh)
} }
if svc.Name == "apache" {
log.Info("Starting apache parser", "service", serviceName)
ap := parser.NewApacheParser()
ap.Parse(p.Events(), entryCh)
}
}(pars, svc.Name) }(pars, svc.Name)
continue continue
} }
@@ -131,14 +150,18 @@ var DaemonCmd = &cobra.Command{
if svc.Name == "nginx" { if svc.Name == "nginx" {
log.Info("Starting nginx parser", "service", serviceName) log.Info("Starting nginx parser", "service", serviceName)
ng := parser.NewNginxParser() ng := parser.NewNginxParser()
ng.Parse(p.Events(), resultCh) ng.Parse(p.Events(), entryCh)
} }
if svc.Name == "ssh" { if svc.Name == "ssh" {
log.Info("Starting ssh parser", "service", serviceName) log.Info("Starting ssh parser", "service", serviceName)
ssh := parser.NewSshdParser() ssh := parser.NewSshdParser()
ssh.Parse(p.Events(), resultCh) ssh.Parse(p.Events(), entryCh)
}
if svc.Name == "apache" {
log.Info("Starting apache parser", "service", serviceName)
ap := parser.NewApacheParser()
ap.Parse(p.Events(), entryCh)
} }
}(pars, svc.Name) }(pars, svc.Name)

View File

@@ -13,14 +13,17 @@ import (
var ( var (
ttl_fw string ttl_fw string
port int
protocol string
) )
var UnbanCmd = &cobra.Command{ var UnbanCmd = &cobra.Command{
Use: "unban", Use: "unban",
Short: "Unban IP", Short: "Unban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := func() error {
if len(args) == 0 { if len(args) == 0 {
fmt.Println("IP can't be empty") return fmt.Errorf("IP can't be empty")
os.Exit(1)
} }
if ttl_fw == "" { if ttl_fw == "" {
ttl_fw = "1y" ttl_fw = "1y"
@@ -28,39 +31,38 @@ var UnbanCmd = &cobra.Command{
ip := args[0] ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil { if err != nil {
fmt.Println(err) return err
os.Exit(1)
} }
cfg, err := config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil { if err != nil {
fmt.Println(err) return err
os.Exit(1)
} }
fw := cfg.Firewall.Name fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config) b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" { if ip == "" {
fmt.Println("IP can't be empty") return fmt.Errorf("IP can't be empty")
os.Exit(1)
} }
if net.ParseIP(ip) == nil { if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP") return fmt.Errorf("invalid IP")
os.Exit(1)
} }
if err != nil { if err != nil {
fmt.Println(err) return err
os.Exit(1)
} }
err = b.Unban(ip) err = b.Unban(ip)
if err != nil { if err != nil {
fmt.Println(err) return err
os.Exit(1)
} }
err = db.RemoveBan(ip) err = db.RemoveBan(ip)
if err != nil {
return err
}
fmt.Println("IP unblocked successfully!")
return nil
}()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("IP unblocked successfully!")
}, },
} }
@@ -68,19 +70,64 @@ var BanCmd = &cobra.Command{
Use: "ban", Use: "ban",
Short: "Ban IP", Short: "Ban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := func() error {
if len(args) == 0 { if len(args) == 0 {
fmt.Println("IP can't be empty") return fmt.Errorf("IP can't be empty")
os.Exit(1)
} }
if ttl_fw == "" { if ttl_fw == "" {
ttl_fw = "1y" ttl_fw = "1y"
} }
ip := args[0] ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil {
return err
}
cfg, err := config.LoadConfig()
if err != nil {
return err
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
return fmt.Errorf("IP can't be empty")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid IP")
}
if err != nil {
return err
}
err = b.Ban(ip)
if err != nil {
return err
}
err = db.AddBan(ip, ttl_fw, "manual ban")
if err != nil {
return err
}
fmt.Println("IP blocked successfully!")
return nil
}()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
},
}
var PortCmd = &cobra.Command{
Use: "port",
Short: "Ports commands",
}
var PortOpenCmd = &cobra.Command{
Use: "open",
Short: "Open ports on firewall",
Run: func(cmd *cobra.Command, args []string) {
if protocol == "" {
fmt.Println("Protocol can't be empty")
os.Exit(1)
}
cfg, err := config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -88,32 +135,45 @@ var BanCmd = &cobra.Command{
} }
fw := cfg.Firewall.Name fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config) b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" { err = b.PortOpen(port, protocol)
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 { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err = b.Ban(ip) fmt.Println("Port opened successfully!")
},
}
var PortCloseCmd = &cobra.Command{
Use: "close",
Short: "Close ports on firewall",
Run: func(cmd *cobra.Command, args []string) {
if protocol == "" {
fmt.Println("Protocol can't be empty")
os.Exit(1)
}
cfg, err := config.LoadConfig()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err = db.AddBan(ip, ttl_fw, "manual ban") fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
err = b.PortClose(port, protocol)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("IP blocked successfully!") fmt.Println("Port closed successfully!")
}, },
} }
func FwRegister() { func FwRegister() {
BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time") BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time")
PortCmd.AddCommand(PortOpenCmd)
PortCmd.AddCommand(PortCloseCmd)
PortOpenCmd.Flags().IntVarP(&port, "port", "p", 0, "port number")
PortOpenCmd.Flags().StringVarP(&protocol, "protocol", "c", "", "protocol")
PortCloseCmd.Flags().IntVarP(&port, "port", "p", 0, "port number")
PortCloseCmd.Flags().StringVarP(&protocol, "protocol", "c", "", "protocol")
} }

View File

@@ -16,53 +16,11 @@ var InitCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Initializing BanForge...") 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() 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 {

View File

@@ -3,8 +3,10 @@ package command
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/d3m0k1d/BanForge/internal/config" "github.com/d3m0k1d/BanForge/internal/config"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -15,6 +17,8 @@ var (
status string status string
method string method string
ttl string ttl string
maxRetry int
editName string
) )
var RuleCmd = &cobra.Command{ var RuleCmd = &cobra.Command{
@@ -24,24 +28,25 @@ var RuleCmd = &cobra.Command{
var AddCmd = &cobra.Command{ var AddCmd = &cobra.Command{
Use: "add", Use: "add",
Short: "CLI interface for add new rule to file /etc/banforge/rules.toml", Short: "Add a new rule to /etc/banforge/rules.d/",
Long: "Creates a new rule file in /etc/banforge/rules.d/<name>.toml",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if name == "" { if name == "" {
fmt.Printf("Rule name can't be empty\n") fmt.Println("Rule name can't be empty (use -n flag)")
os.Exit(1) os.Exit(1)
} }
if service == "" { if service == "" {
fmt.Printf("Service name can't be empty\n") fmt.Println("Service name can't be empty (use -s flag)")
os.Exit(1) os.Exit(1)
} }
if path == "" && status == "" && method == "" { if path == "" && status == "" && method == "" {
fmt.Printf("At least 1 rule field must be filled in.") fmt.Println("At least one rule field must be filled: path, status, or method")
os.Exit(1) os.Exit(1)
} }
if ttl == "" { if ttl == "" {
ttl = "1y" ttl = "1y"
} }
err := config.NewRule(name, service, path, status, method, ttl) err := config.NewRule(name, service, path, status, method, ttl, maxRetry)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@@ -50,35 +55,103 @@ var AddCmd = &cobra.Command{
}, },
} }
var ListCmd = &cobra.Command{ var EditCmd = &cobra.Command{
Use: "list", Use: "edit",
Short: "List rules", Short: "Edit an existing rule",
Long: "Edit rule fields by name. Only specified fields will be updated.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
r, err := config.LoadRuleConfig() if editName == "" {
fmt.Println("Rule name is required (use -n flag)")
os.Exit(1)
}
if service == "" && path == "" && status == "" && method == "" {
fmt.Println("At least one field must be specified to edit: -s, -p, -c, or -m")
os.Exit(1)
}
err := config.EditRule(editName, service, path, status, method)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
for _, rule := range r { fmt.Println("Rule updated successfully!")
fmt.Printf( },
"Name: %s\nService: %s\nPath: %s\nStatus: %s\nMethod: %s\n\n", }
var RemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove a rule by name",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ruleName := args[0]
fileName := config.SanitizeRuleFilename(ruleName) + ".toml"
filePath := filepath.Join("/etc/banforge/rules.d", fileName)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
fmt.Printf("Rule '%s' not found\n", ruleName)
os.Exit(1)
}
if err := os.Remove(filePath); err != nil {
fmt.Printf("Failed to remove rule: %v\n", err)
os.Exit(1)
}
fmt.Printf("Rule '%s' removed successfully\n", ruleName)
},
}
var ListCmd = &cobra.Command{
Use: "list",
Short: "List all rules",
Run: func(cmd *cobra.Command, args []string) {
rules, err := config.LoadRuleConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if len(rules) == 0 {
fmt.Println("No rules found")
return
}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{
"Name", "Service", "Path", "Status", "Method", "MaxRetry", "BanTime",
})
for _, rule := range rules {
t.AppendRow(table.Row{
rule.Name, rule.Name,
rule.ServiceName, rule.ServiceName,
rule.Path, rule.Path,
rule.Status, rule.Status,
rule.Method, rule.Method,
) rule.MaxRetry,
rule.BanTime,
})
} }
t.Render()
}, },
} }
func RuleRegister() { func RuleRegister() {
RuleCmd.AddCommand(AddCmd) RuleCmd.AddCommand(AddCmd)
RuleCmd.AddCommand(EditCmd)
RuleCmd.AddCommand(RemoveCmd)
RuleCmd.AddCommand(ListCmd) RuleCmd.AddCommand(ListCmd)
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 (required)")
AddCmd.Flags().StringVarP(&path, "path", "p", "", "request path") AddCmd.Flags().StringVarP(&path, "path", "p", "", "request path")
AddCmd.Flags().StringVarP(&status, "status", "c", "", "status code") AddCmd.Flags().StringVarP(&status, "status", "c", "", "status code")
AddCmd.Flags().StringVarP(&method, "method", "m", "", "method") AddCmd.Flags().StringVarP(&method, "method", "m", "", "HTTP method")
AddCmd.Flags().StringVarP(&ttl, "ttl", "t", "", "ban time") AddCmd.Flags().StringVarP(&ttl, "ttl", "t", "", "ban time (e.g., 1h, 1d, 1y)")
AddCmd.Flags().IntVarP(&maxRetry, "max_retry", "r", 0, "max retry before ban")
EditCmd.Flags().StringVarP(&editName, "name", "n", "", "rule name to edit (required)")
EditCmd.Flags().StringVarP(&service, "service", "s", "", "new service name")
EditCmd.Flags().StringVarP(&path, "path", "p", "", "new path")
EditCmd.Flags().StringVarP(&status, "status", "c", "", "new status code")
EditCmd.Flags().StringVarP(&method, "method", "m", "", "new HTTP method")
} }

View File

@@ -0,0 +1,17 @@
package command
import (
"fmt"
"github.com/spf13/cobra"
)
var version = "0.5.2"
var VersionCmd = &cobra.Command{
Use: "version",
Short: "BanForge version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("BanForge version:", version)
},
}

View File

@@ -13,7 +13,6 @@ var rootCmd = &cobra.Command{
Use: "banforge", Use: "banforge",
Short: "IPS log-based written on Golang", Short: "IPS log-based written on Golang",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
}, },
} }
@@ -28,6 +27,8 @@ func Execute() {
rootCmd.AddCommand(command.BanCmd) rootCmd.AddCommand(command.BanCmd)
rootCmd.AddCommand(command.UnbanCmd) rootCmd.AddCommand(command.UnbanCmd)
rootCmd.AddCommand(command.BanListCmd) rootCmd.AddCommand(command.BanListCmd)
rootCmd.AddCommand(command.VersionCmd)
rootCmd.AddCommand(command.PortCmd)
command.RuleRegister() command.RuleRegister()
command.FwRegister() command.FwRegister()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {

View File

@@ -1,8 +1,11 @@
# CLI commands BanForge # CLI commands BanForge
BanForge provides a command-line interface (CLI) to manage IP blocking, BanForge provides a command-line interface (CLI) to manage IP blocking,
configure detection rules, and control the daemon process. configure detection rules, and control the daemon process.
## Commands ## Commands
### init - create a deps file
### init - Create configuration files
```shell ```shell
banforge init banforge init
@@ -10,7 +13,24 @@ banforge init
**Description** **Description**
This command creates the necessary directories and base configuration files This command creates the necessary directories and base configuration files
required for the daemon to operate. required for the daemon to operate:
- `/etc/banforge/config.toml` — main configuration
- `/etc/banforge/rules.toml` — default rules file
- `/etc/banforge/rules.d/` — directory for individual rule files
---
### version - Display BanForge version
```shell
banforge version
```
**Description**
This command displays the current version of the BanForge software.
---
### daemon - Starts the BanForge daemon process ### daemon - Starts the BanForge daemon process
```shell ```shell
@@ -22,7 +42,10 @@ This command starts the BanForge daemon process in the background.
The daemon continuously monitors incoming requests, detects anomalies, The daemon continuously monitors incoming requests, detects anomalies,
and applies firewall rules in real-time. and applies firewall rules in real-time.
---
### firewall - Manages firewall rules ### firewall - Manages firewall rules
```shell ```shell
banforge ban <ip> banforge ban <ip>
banforge unban <ip> banforge unban <ip>
@@ -31,32 +54,173 @@ 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.
Flag -t or -ttl add bantime if not used default ban 1 year | Flag | Description |
### list - Lists the IP addresses that are currently blocked | ----------- | ------------------------------ |
| `-t`, `-ttl` | Ban duration (default: 1 year) |
**Examples:**
```bash
# Ban IP for 1 hour
banforge ban 192.168.1.100 -t 1h
# Unban IP
banforge unban 192.168.1.100
```
---
### ports - Open and close ports on firewall
```shell
banforge open -port <port> -protocol <protocol>
banforge close -port <port> -protocol <protocol>
```
**Description**
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
| Flag | Required | Description |
| ------------- | -------- | ------------------------ |
| `-port` | + | Port number (e.g., 80) |
| `-protocol` | + | Protocol (tcp/udp) |
**Examples:**
```bash
# Open port 80 for TCP
banforge open -port 80 -protocol tcp
# Close port 443
banforge close -port 443 -protocol tcp
```
---
### list - List blocked IP addresses
```shell ```shell
banforge list banforge list
``` ```
**Description** **Description**
This command output table of IP addresses that are currently blocked This command outputs a table of IP addresses that are currently blocked.
### rule - Manages detection rules ---
### rule - Manage detection rules
Rules are stored in `/etc/banforge/rules.d/` as individual `.toml` files.
#### Add a new rule
```shell
banforge rule add -n <name> -s <service> [options]
```
**Flags:**
| Flag | Required | Description |
| ------------------- | -------- | ---------------------------------------- |
| `-n`, `--name` | + | Rule name (used as filename) |
| `-s`, `--service` | + | Service name (nginx, apache, ssh, etc.) |
| `-p`, `--path` | - | Request path to match |
| `-m`, `--method` | - | HTTP method (GET, POST, etc.) |
| `-c`, `--status` | - | HTTP status code (403, 404, etc.) |
| `-t`, `--ttl` | - | Ban duration (default: 1y) |
| `-r`, `--max_retry` | - | Max retries before ban (default: 0) |
**Note:** At least one of `-p`, `-m`, or `-c` must be specified.
**Examples:**
```bash
# Ban on 403 status
banforge rule add -n "Forbidden" -s nginx -c 403 -t 30m
# Ban on path pattern
banforge rule add -n "Admin Access" -s nginx -p "/admin/*" -t 2h -r 3
# SSH brute force protection
banforge rule add -n "SSH Bruteforce" -s ssh -c "Failed" -t 1h -r 5
```
---
#### List all rules
```shell ```shell
banforge rule add -n rule.name -c 403
banforge rule list banforge rule list
``` ```
**Description** **Description**
These command help you to create and manage detection rules in CLI interface. Displays all configured rules in a table format.
| Flag | Required | **Example output:**
| ----------- | -------- | ```
| -n -name | + | +------------------+---------+--------+--------+--------+----------+---------+
| -s -service | + | | NAME | SERVICE | PATH | STATUS | METHOD | MAXRETRY | BANTIME |
| -p -path | - | +------------------+---------+--------+--------+--------+----------+---------+
| -m -method | - | | SSH Bruteforce | ssh | | Failed | | 5 | 1h |
| -c -status | - | | Nginx 404 | nginx | | 404 | | 3 | 30m |
| -t -ttl | -(if not used default ban 1 year) | | Admin Panel | nginx | /admin | | | 2 | 2h |
+------------------+---------+--------+--------+--------+----------+---------+
```
You must specify at least 1 of the optional flags to create a rule. ---
#### Edit an existing rule
```shell
banforge rule edit -n <name> [options]
```
**Description**
Edit fields of an existing rule. Only specified fields will be updated.
| Flag | Required | Description |
| ------------------- | -------- | ------------------------------- |
| `-n`, `--name` | + | Rule name to edit |
| `-s`, `--service` | - | New service name |
| `-p`, `--path` | - | New path |
| `-m`, `--method` | - | New method |
| `-c`, `--status` | - | New status code |
**Examples:**
```bash
# Update ban time for existing rule
banforge rule edit -n "SSH Bruteforce" -t 2h
# Change status code
banforge rule edit -n "Forbidden" -c 403
```
---
#### Remove a rule
```shell
banforge rule remove <name>
```
**Description**
Permanently delete a rule by name.
**Example:**
```bash
banforge rule remove "Old Rule"
```
---
## Ban time format
Use the following suffixes for ban duration:
| Suffix | Duration |
| ------ | -------- |
| `s` | Seconds |
| `m` | Minutes |
| `h` | Hours |
| `d` | Days |
| `M` | Months (30 days) |
| `y` | Years (365 days) |
**Examples:** `30s`, `5m`, `2h`, `1d`, `1M`, `1y`

View File

@@ -40,10 +40,12 @@ Example:
service = "nginx" service = "nginx"
path = "" path = ""
status = "304" status = "304"
max_retry = 3
method = "" method = ""
ban_time = "1m" ban_time = "1m"
``` ```
**Description** **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. 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". ban_time require in format "1m", "1h", "1d", "1M", "1y".
If you want to ban all requests to PHP files (e.g., path = "*.php") or requests to the admin panel (e.g., path = "/admin/*") If you want to ban all requests to PHP files (e.g., path = "*.php") or requests to the admin panel (e.g., path = "/admin/*").
If max_retry = 0 ban on first request.

14
go.mod
View File

@@ -7,23 +7,23 @@ require (
github.com/jedib0t/go-pretty/v6 v6.7.8 github.com/jedib0t/go-pretty/v6 v6.7.8
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
modernc.org/sqlite v1.44.3 modernc.org/sqlite v1.46.1
) )
require ( require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.34.0 // indirect
modernc.org/libc v1.67.6 // indirect modernc.org/libc v1.68.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

45
go.sum
View File

@@ -1,5 +1,7 @@
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/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,17 +19,14 @@ github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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=
@@ -37,19 +36,19 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -57,18 +56,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -77,8 +76,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -1,9 +1,12 @@
package blocker package blocker
import ( import (
"fmt"
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
type Firewalld struct { type Firewalld struct {
@@ -21,19 +24,24 @@ func (f *Firewalld) Ban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncBanAttempt("firewalld")
// #nosec G204 - ip is validated
cmd := exec.Command("firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent") cmd := exec.Command("firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
f.logger.Error(err.Error()) f.logger.Error(err.Error())
metrics.IncError()
return err return err
} }
f.logger.Info("Add source " + ip + " " + string(output)) f.logger.Info("Add source " + ip + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput() output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil { if err != nil {
f.logger.Error(err.Error()) f.logger.Error(err.Error())
metrics.IncError()
return err return err
} }
f.logger.Info("Reload " + string(output)) f.logger.Info("Reload " + string(output))
metrics.IncBan("firewalld")
return nil return nil
} }
@@ -42,19 +50,87 @@ func (f *Firewalld) Unban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncUnbanAttempt("firewalld")
// #nosec G204 - ip is validated
cmd := exec.Command("firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent") cmd := exec.Command("firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
f.logger.Error(err.Error()) f.logger.Error(err.Error())
metrics.IncError()
return err return err
} }
f.logger.Info("Remove source " + ip + " " + string(output)) f.logger.Info("Remove source " + ip + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput() output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil { if err != nil {
f.logger.Error(err.Error()) f.logger.Error(err.Error())
metrics.IncError()
return err return err
} }
f.logger.Info("Reload " + string(output)) f.logger.Info("Reload " + string(output))
metrics.IncUnban("firewalld")
return nil
}
func (f *Firewalld) PortOpen(port int, protocol string) error {
// #nosec G204 - handle is extracted from Firewalld output and validated
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
f.logger.Error("invalid protocol")
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
metrics.IncPortOperation("open", protocol)
cmd := exec.Command(
"firewall-cmd",
"--zone=public",
"--add-port="+s+"/"+protocol,
"--permanent",
)
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
metrics.IncError()
return err
}
f.logger.Info("Add port " + s + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
metrics.IncError()
return err
}
f.logger.Info("Reload " + string(output))
}
return nil
}
func (f *Firewalld) PortClose(port int, protocol string) error {
// #nosec G204 - handle is extracted from nftables output and validated
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
metrics.IncPortOperation("close", protocol)
cmd := exec.Command(
"firewall-cmd",
"--zone=public",
"--remove-port="+s+"/"+protocol,
"--permanent",
)
output, err := cmd.CombinedOutput()
if err != nil {
metrics.IncError()
return err
}
f.logger.Info("Remove port " + s + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
metrics.IncError()
return err
}
f.logger.Info("Reload " + string(output))
}
return nil return nil
} }

View File

@@ -10,6 +10,8 @@ type BlockerEngine interface {
Ban(ip string) error Ban(ip string) error
Unban(ip string) error Unban(ip string) error
Setup(config string) error Setup(config string) error
PortOpen(port int, protocol string) error
PortClose(port int, protocol string) error
} }
func GetBlocker(fw string, config string) BlockerEngine { func GetBlocker(fw string, config string) BlockerEngine {

View File

@@ -2,8 +2,10 @@ package blocker
import ( import (
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
type Iptables struct { type Iptables struct {
@@ -23,10 +25,12 @@ func (f *Iptables) Ban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncBanAttempt("iptables")
err = validateConfigPath(f.config) err = validateConfigPath(f.config)
if err != nil { if err != nil {
return err return err
} }
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd := exec.Command("iptables", "-A", "INPUT", "-s", ip, "-j", "DROP") cmd := exec.Command("iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -34,11 +38,13 @@ func (f *Iptables) Ban(ip string) error {
"ip", ip, "ip", ip,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
f.logger.Info("IP banned", f.logger.Info("IP banned",
"ip", ip, "ip", ip,
"output", string(output)) "output", string(output))
metrics.IncBan("iptables")
err = validateConfigPath(f.config) err = validateConfigPath(f.config)
if err != nil { if err != nil {
@@ -52,6 +58,7 @@ func (f *Iptables) Ban(ip string) error {
"config_path", f.config, "config_path", f.config,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
f.logger.Info("config saved", f.logger.Info("config saved",
@@ -65,10 +72,12 @@ func (f *Iptables) Unban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncUnbanAttempt("iptables")
err = validateConfigPath(f.config) err = validateConfigPath(f.config)
if err != nil { if err != nil {
return err return err
} }
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd := exec.Command("iptables", "-D", "INPUT", "-s", ip, "-j", "DROP") cmd := exec.Command("iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -76,11 +85,13 @@ func (f *Iptables) Unban(ip string) error {
"ip", ip, "ip", ip,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
f.logger.Info("IP unbanned", f.logger.Info("IP unbanned",
"ip", ip, "ip", ip,
"output", string(output)) "output", string(output))
metrics.IncUnban("iptables")
err = validateConfigPath(f.config) err = validateConfigPath(f.config)
if err != nil { if err != nil {
@@ -94,6 +105,7 @@ func (f *Iptables) Unban(ip string) error {
"config_path", f.config, "config_path", f.config,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
f.logger.Info("config saved", f.logger.Info("config saved",
@@ -102,6 +114,70 @@ func (f *Iptables) Unban(ip string) error {
return nil return nil
} }
func (f *Iptables) PortOpen(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
f.logger.Error("invalid protocol")
return nil
}
s := strconv.Itoa(port)
metrics.IncPortOperation("open", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command("iptables", "-A", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
metrics.IncError()
return err
}
f.logger.Info("Add port " + s + " " + string(output))
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd = exec.Command("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))
metrics.IncError()
return err
}
}
return nil
}
func (f *Iptables) PortClose(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
f.logger.Error("invalid protocol")
return nil
}
s := strconv.Itoa(port)
metrics.IncPortOperation("close", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command("iptables", "-D", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
metrics.IncError()
return err
}
f.logger.Info("Add port " + s + " " + string(output))
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd = exec.Command("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))
metrics.IncError()
return err
}
}
return nil
}
func (f *Iptables) Setup(config string) error { func (f *Iptables) Setup(config string) error {
return nil return nil
} }

View File

@@ -3,9 +3,11 @@ package blocker
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
type Nftables struct { type Nftables struct {
@@ -25,7 +27,8 @@ func (n *Nftables) Ban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncBanAttempt("nftables")
// #nosec G204 - ip is validated
cmd := exec.Command("nft", "add", "rule", "inet", "banforge", "banned", cmd := exec.Command("nft", "add", "rule", "inet", "banforge", "banned",
"ip", "saddr", ip, "drop") "ip", "saddr", ip, "drop")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
@@ -34,16 +37,19 @@ func (n *Nftables) Ban(ip string) error {
"ip", ip, "ip", ip,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
n.logger.Info("IP banned", "ip", ip) n.logger.Info("IP banned", "ip", ip)
metrics.IncBan("nftables")
err = saveNftablesConfig(n.config) err = saveNftablesConfig(n.config)
if err != nil { if err != nil {
n.logger.Error("failed to save config", n.logger.Error("failed to save config",
"config_path", n.config, "config_path", n.config,
"error", err.Error()) "error", err.Error())
metrics.IncError()
return err return err
} }
@@ -56,17 +62,20 @@ func (n *Nftables) Unban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncUnbanAttempt("nftables")
handle, err := n.findRuleHandle(ip) handle, err := n.findRuleHandle(ip)
if err != nil { if err != nil {
n.logger.Error("failed to find rule handle", n.logger.Error("failed to find rule handle",
"ip", ip, "ip", ip,
"error", err.Error()) "error", err.Error())
metrics.IncError()
return err return err
} }
if handle == "" { if handle == "" {
n.logger.Warn("no rule found for IP", "ip", ip) n.logger.Warn("no rule found for IP", "ip", ip)
metrics.IncError()
return fmt.Errorf("no rule found for IP %s", ip) return fmt.Errorf("no rule found for IP %s", ip)
} }
// #nosec G204 - handle is extracted from nftables output and validated // #nosec G204 - handle is extracted from nftables output and validated
@@ -79,16 +88,19 @@ func (n *Nftables) Unban(ip string) error {
"handle", handle, "handle", handle,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return err return err
} }
n.logger.Info("IP unbanned", "ip", ip, "handle", handle) n.logger.Info("IP unbanned", "ip", ip, "handle", handle)
metrics.IncUnban("nftables")
err = saveNftablesConfig(n.config) err = saveNftablesConfig(n.config)
if err != nil { if err != nil {
n.logger.Error("failed to save config", n.logger.Error("failed to save config",
"config_path", n.config, "config_path", n.config,
"error", err.Error()) "error", err.Error())
metrics.IncError()
return err return err
} }
@@ -112,6 +124,7 @@ func (n *Nftables) Setup(config string) error {
} }
} }
` `
// #nosec G204 - config is managed by adminstartor
cmd := exec.Command("tee", config) cmd := exec.Command("tee", config)
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
@@ -134,7 +147,7 @@ func (n *Nftables) Setup(config string) error {
if err = cmd.Wait(); err != nil { if err = cmd.Wait(); err != nil {
return fmt.Errorf("failed to save config: %w", err) return fmt.Errorf("failed to save config: %w", err)
} }
// #nosec G204 - config is managed by adminstartor
cmd = exec.Command("nft", "-f", config) cmd = exec.Command("nft", "-f", config)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -166,6 +179,89 @@ func (n *Nftables) findRuleHandle(ip string) (string, error) {
return "", nil return "", nil
} }
func (n *Nftables) PortOpen(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
n.logger.Error("invalid protocol")
metrics.IncError()
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
metrics.IncPortOperation("open", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command(
"nft",
"add",
"rule",
"inet",
"banforge",
"input",
protocol,
"dport",
s,
"accept",
)
output, err := cmd.CombinedOutput()
if err != nil {
n.logger.Error(err.Error())
metrics.IncError()
return err
}
n.logger.Info("Add port " + s + " " + string(output))
err = saveNftablesConfig(n.config)
if err != nil {
n.logger.Error("failed to save config",
"config_path", n.config,
"error", err.Error())
metrics.IncError()
return err
}
}
return nil
}
func (n *Nftables) PortClose(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
n.logger.Error("invalid protocol")
metrics.IncError()
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
metrics.IncPortOperation("close", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command(
"nft",
"add",
"rule",
"inet",
"banforge",
"input",
protocol,
"dport",
s,
"drop",
)
output, err := cmd.CombinedOutput()
if err != nil {
n.logger.Error(err.Error())
metrics.IncError()
return err
}
n.logger.Info("Add port " + s + " " + string(output))
err = saveNftablesConfig(n.config)
if err != nil {
n.logger.Error("failed to save config",
"config_path", n.config,
"error", err.Error())
metrics.IncError()
return err
}
}
return nil
}
func saveNftablesConfig(configPath string) error { func saveNftablesConfig(configPath string) error {
err := validateConfigPath(configPath) err := validateConfigPath(configPath)
if err != nil { if err != nil {
@@ -177,7 +273,7 @@ func saveNftablesConfig(configPath string) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to get nftables ruleset: %w", err) return fmt.Errorf("failed to get nftables ruleset: %w", err)
} }
// #nosec G204 - managed by system adminstartor
cmd = exec.Command("tee", configPath) cmd = exec.Command("tee", configPath)
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {

View File

@@ -3,8 +3,10 @@ package blocker
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
type Ufw struct { type Ufw struct {
@@ -22,7 +24,8 @@ func (u *Ufw) Ban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncBanAttempt("ufw")
// #nosec G204 - ip is validated
cmd := exec.Command("ufw", "--force", "deny", "from", ip) cmd := exec.Command("ufw", "--force", "deny", "from", ip)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -30,10 +33,12 @@ func (u *Ufw) Ban(ip string) error {
"ip", ip, "ip", ip,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return fmt.Errorf("failed to ban IP %s: %w", ip, err) return fmt.Errorf("failed to ban IP %s: %w", ip, err)
} }
u.logger.Info("IP banned", "ip", ip, "output", string(output)) u.logger.Info("IP banned", "ip", ip, "output", string(output))
metrics.IncBan("ufw")
return nil return nil
} }
func (u *Ufw) Unban(ip string) error { func (u *Ufw) Unban(ip string) error {
@@ -41,7 +46,8 @@ func (u *Ufw) Unban(ip string) error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncUnbanAttempt("ufw")
// #nosec G204 - ip is validated
cmd := exec.Command("ufw", "--force", "delete", "deny", "from", ip) cmd := exec.Command("ufw", "--force", "delete", "deny", "from", ip)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -49,10 +55,56 @@ func (u *Ufw) Unban(ip string) error {
"ip", ip, "ip", ip,
"error", err.Error(), "error", err.Error(),
"output", string(output)) "output", string(output))
metrics.IncError()
return fmt.Errorf("failed to unban IP %s: %w", ip, err) return fmt.Errorf("failed to unban IP %s: %w", ip, err)
} }
u.logger.Info("IP unbanned", "ip", ip, "output", string(output)) u.logger.Info("IP unbanned", "ip", ip, "output", string(output))
metrics.IncUnban("ufw")
return nil
}
func (u *Ufw) PortOpen(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
u.logger.Error("invalid protocol")
metrics.IncError()
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
metrics.IncPortOperation("open", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command("ufw", "allow", s+"/"+protocol)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error(err.Error())
metrics.IncError()
return err
}
u.logger.Info("Add port " + s + " " + string(output))
}
return nil
}
func (u *Ufw) PortClose(port int, protocol string) error {
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
u.logger.Error("invalid protocol")
metrics.IncError()
return nil
}
s := strconv.Itoa(port)
metrics.IncPortOperation("close", protocol)
// #nosec G204 - managed by system adminstartor
cmd := exec.Command("ufw", "deny", s+"/"+protocol)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error(err.Error())
metrics.IncError()
return err
}
u.logger.Info("Add port " + s + " " + string(output))
}
return nil return nil
} }

View File

@@ -3,147 +3,179 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/d3m0k1d/BanForge/internal/logger"
) )
func LoadRuleConfig() ([]Rule, error) { func LoadRuleConfig() ([]Rule, error) {
log := logger.New(false) const rulesDir = "/etc/banforge/rules.d"
var cfg Rules var cfg Rules
_, err := toml.DecodeFile("/etc/banforge/rules.toml", &cfg) files, err := os.ReadDir(rulesDir)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("failed to decode config: %v", err)) return nil, fmt.Errorf("failed to read rules directory: %w", err)
return nil, err }
for _, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".toml") {
continue
}
filePath := filepath.Join(rulesDir, file.Name())
var fileCfg Rules
if _, err := toml.DecodeFile(filePath, &fileCfg); err != nil {
return nil, fmt.Errorf("failed to parse rule file %s: %w", filePath, err)
}
cfg.Rules = append(cfg.Rules, fileCfg.Rules...)
} }
log.Info(fmt.Sprintf("loaded %d rules", len(cfg.Rules)))
return cfg.Rules, nil return cfg.Rules, nil
} }
func NewRule( func NewRule(
Name string, name string,
ServiceName string, serviceName string,
Path string, path string,
Status string, status string,
Method string, method string,
ttl string, ttl string,
maxRetry int,
) error { ) error {
r, err := LoadRuleConfig() if name == "" {
if err != nil { return fmt.Errorf("rule name can't be empty")
r = []Rule{}
} }
if Name == "" {
fmt.Printf("Rule name can't be empty\n") rule := Rule{
return nil Name: name,
} ServiceName: serviceName,
r = append( Path: path,
r, Status: status,
Rule{ Method: method,
Name: Name,
ServiceName: ServiceName,
Path: Path,
Status: Status,
Method: Method,
BanTime: ttl, BanTime: ttl,
}, MaxRetry: maxRetry,
) }
file, err := os.Create("/etc/banforge/rules.toml")
filePath := filepath.Join("/etc/banforge/rules.d", SanitizeRuleFilename(name)+".toml")
if _, err := os.Stat(filePath); err == nil {
return fmt.Errorf("rule with name '%s' already exists", name)
}
cfg := Rules{Rules: []Rule{rule}}
// #nosec G304 - validate by sanitizeRuleFilename
file, err := os.Create(filePath)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create rule file: %w", err)
} }
defer func() { defer func() {
err = file.Close() if closeErr := file.Close(); closeErr != nil {
if err != nil { fmt.Printf("warning: failed to close rule file: %v\n", closeErr)
fmt.Println(err)
} }
}() }()
cfg := Rules{Rules: r}
err = toml.NewEncoder(file).Encode(cfg) if err := toml.NewEncoder(file).Encode(cfg); err != nil {
if err != nil { return fmt.Errorf("failed to encode rule: %w", err)
return err
} }
return nil return nil
} }
func EditRule(Name string, ServiceName string, Path string, Status string, Method string) error { func EditRule(name string, serviceName string, path string, status string, method string) error {
if Name == "" { if name == "" {
return fmt.Errorf("Rule name can't be empty") return fmt.Errorf("rule name can't be empty")
} }
r, err := LoadRuleConfig() rules, err := LoadRuleConfig()
if err != nil { if err != nil {
return fmt.Errorf("rules is empty, please use 'banforge add rule' or create rules.toml") return fmt.Errorf("failed to load rules: %w", err)
} }
found := false found := false
for i, rule := range r { var updatedRule *Rule
if rule.Name == Name { for i, rule := range rules {
if rule.Name == name {
found = true found = true
updatedRule = &rules[i]
if ServiceName != "" { if serviceName != "" {
r[i].ServiceName = ServiceName updatedRule.ServiceName = serviceName
} }
if Path != "" { if path != "" {
r[i].Path = Path updatedRule.Path = path
} }
if Status != "" { if status != "" {
r[i].Status = Status updatedRule.Status = status
} }
if Method != "" { if method != "" {
r[i].Method = Method updatedRule.Method = method
} }
break break
} }
} }
if !found { if !found {
return fmt.Errorf("rule '%s' not found", Name) return fmt.Errorf("rule '%s' not found", name)
} }
file, err := os.Create("/etc/banforge/rules.toml") filePath := filepath.Join("/etc/banforge/rules.d", SanitizeRuleFilename(name)+".toml")
cfg := Rules{Rules: []Rule{*updatedRule}}
// #nosec G304 - validate by sanitizeRuleFilename
file, err := os.Create(filePath)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to update rule file: %w", err)
} }
defer func() { defer func() {
err = file.Close() if closeErr := file.Close(); closeErr != nil {
if err != nil { fmt.Printf("warning: failed to close rule file: %v\n", closeErr)
fmt.Println(err)
} }
}() }()
cfg := Rules{Rules: r}
if err := toml.NewEncoder(file).Encode(cfg); err != nil { if err := toml.NewEncoder(file).Encode(cfg); err != nil {
return fmt.Errorf("failed to encode config: %w", err) return fmt.Errorf("failed to encode updated rule: %w", err)
} }
return nil return nil
} }
func SanitizeRuleFilename(name string) string {
result := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '-' || r == '_' {
return r
}
return '_'
}, name)
return strings.ToLower(result)
}
func ParseDurationWithYears(s string) (time.Duration, error) { func ParseDurationWithYears(s string) (time.Duration, error) {
if strings.HasSuffix(s, "y") { if ss, ok := strings.CutSuffix(s, "y"); ok {
years, err := strconv.Atoi(strings.TrimSuffix(s, "y")) years, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return time.Duration(years) * 365 * 24 * time.Hour, nil return time.Duration(years) * 365 * 24 * time.Hour, nil
} }
if strings.HasSuffix(s, "M") { if ss, ok := strings.CutSuffix(s, "M"); ok {
months, err := strconv.Atoi(strings.TrimSuffix(s, "M")) months, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return time.Duration(months) * 30 * 24 * time.Hour, nil return time.Duration(months) * 30 * 24 * time.Hour, nil
} }
if strings.HasSuffix(s, "d") { if ss, ok := strings.CutSuffix(s, "d"); ok {
days, err := strconv.Atoi(strings.TrimSuffix(s, "d")) days, err := strconv.Atoi(ss)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -16,6 +16,24 @@ const (
ConfigFile = "config.toml" ConfigFile = "config.toml"
) )
func createFileWithPermissions(path string, perm os.FileMode) error {
// #nosec G304 - path is controlled by config package not user
file, err := os.Create(path)
if err != nil {
return err
}
if err := os.Chmod(path, perm); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return nil
}
func CreateConf() error { func CreateConf() error {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
return fmt.Errorf("you must be root to run this command, use sudo/doas") return fmt.Errorf("you must be root to run this command, use sudo/doas")
@@ -28,53 +46,49 @@ func CreateConf() error {
return nil return nil
} }
file, err := os.Create("/etc/banforge/config.toml") if err := os.MkdirAll(ConfigDir, 0750); err != nil {
if err != nil { return fmt.Errorf("failed to create config directory: %w", err)
return fmt.Errorf("failed to create config file: %w", err)
} }
defer func() {
err = file.Close() if err := os.WriteFile(configPath, []byte(Base_config), 0600); err != nil {
if err != nil {
fmt.Println(err)
}
}()
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) return fmt.Errorf("failed to write config file: %w", err)
} }
fmt.Printf(" Config file created: %s\n", configPath) fmt.Printf("Config file created: %s\n", configPath)
file, err = os.Create("/etc/banforge/rules.toml")
if err != nil { rulesDir := filepath.Join(ConfigDir, "rules.d")
return fmt.Errorf("failed to create rules file: %w", err) if err := os.MkdirAll(rulesDir, 0750); err != nil {
return fmt.Errorf("failed to create rules directory: %w", err)
} }
file, err = os.Create("/var/lib/banforge/storage.db") fmt.Printf("Rules directory created: %s\n", rulesDir)
if err != nil {
return fmt.Errorf("failed to create database file: %w", err) bansDBDir := filepath.Dir("/var/lib/banforge/bans.db")
if err := os.MkdirAll(bansDBDir, 0750); err != nil {
return fmt.Errorf("failed to create bans database directory: %w", err)
} }
err = os.Chmod("/var/lib/banforge/storage.db", 0600)
if err != nil { reqDBDir := filepath.Dir("/var/lib/banforge/requests.db")
return fmt.Errorf("failed to set permissions: %w", err) if err := os.MkdirAll(reqDBDir, 0750); err != nil {
return fmt.Errorf("failed to create requests database directory: %w", err)
} }
defer func() {
err = file.Close() bansDBPath := "/var/lib/banforge/bans.db"
if err != nil { if err := createFileWithPermissions(bansDBPath, 0600); err != nil {
fmt.Println(err) return fmt.Errorf("failed to create bans database file: %w", err)
} }
}() fmt.Printf("Bans database file created: %s\n", bansDBPath)
if err := os.Chmod(configPath, 0600); err != nil {
return fmt.Errorf("failed to set permissions: %w", err) reqDBPath := "/var/lib/banforge/requests.db"
if err := createFileWithPermissions(reqDBPath, 0600); err != nil {
return fmt.Errorf("failed to create requests database file: %w", err)
} }
fmt.Printf(" Rules file created: %s\n", configPath) fmt.Printf("Requests database file created: %s\n", reqDBPath)
return nil return nil
} }
func FindFirewall() error { func FindFirewall() error {
if os.Getegid() != 0 { if os.Geteuid() != 0 {
fmt.Printf("Firewall settings needs sudo privileges\n") return fmt.Errorf("firewall settings needs sudo privileges")
os.Exit(1)
} }
firewalls := []string{"nft", "firewall-cmd", "iptables", "ufw"} firewalls := []string{"nft", "firewall-cmd", "iptables", "ufw"}
@@ -107,10 +121,7 @@ func FindFirewall() error {
encoder := toml.NewEncoder(file) encoder := toml.NewEncoder(file)
if err := encoder.Encode(cfg); err != nil { if err := encoder.Encode(cfg); err != nil {
err = file.Close() _ = file.Close()
if err != nil {
return fmt.Errorf("failed to close file: %w", err)
}
return fmt.Errorf("failed to encode config: %w", err) return fmt.Errorf("failed to encode config: %w", err)
} }

View File

@@ -8,6 +8,10 @@ const Base_config = `
name = "" name = ""
config = "/etc/nftables.conf" config = "/etc/nftables.conf"
[metrics]
enabled = false
port = 2122
[[service]] [[service]]
name = "nginx" name = "nginx"
logging = "file" logging = "file"

View File

@@ -14,6 +14,7 @@ type Service struct {
type Config struct { type Config struct {
Firewall Firewall `toml:"firewall"` Firewall Firewall `toml:"firewall"`
Metrics Metrics `toml:"metrics"`
Service []Service `toml:"service"` Service []Service `toml:"service"`
} }
@@ -28,5 +29,11 @@ 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"`
MaxRetry int `toml:"max_retry"`
BanTime string `toml:"ban_time"` BanTime string `toml:"ban_time"`
} }
type Metrics struct {
Enabled bool `toml:"enabled"`
Port int `toml:"port"`
}

View File

@@ -8,12 +8,14 @@ import (
"github.com/d3m0k1d/BanForge/internal/blocker" "github.com/d3m0k1d/BanForge/internal/blocker"
"github.com/d3m0k1d/BanForge/internal/config" "github.com/d3m0k1d/BanForge/internal/config"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/d3m0k1d/BanForge/internal/storage" "github.com/d3m0k1d/BanForge/internal/storage"
) )
type Judge struct { type Judge struct {
db_r *storage.BanReader db_r *storage.BanReader
db_w *storage.BanWriter db_w *storage.BanWriter
db_rq *storage.RequestReader
logger *logger.Logger logger *logger.Logger
Blocker blocker.BlockerEngine Blocker blocker.BlockerEngine
rulesByService map[string][]config.Rule rulesByService map[string][]config.Rule
@@ -24,6 +26,7 @@ type Judge struct {
func New( func New(
db_r *storage.BanReader, db_r *storage.BanReader,
db_w *storage.BanWriter, db_w *storage.BanWriter,
db_rq *storage.RequestReader,
b blocker.BlockerEngine, b blocker.BlockerEngine,
resultCh chan *storage.LogEntry, resultCh chan *storage.LogEntry,
entryCh chan *storage.LogEntry, entryCh chan *storage.LogEntry,
@@ -31,6 +34,7 @@ func New(
return &Judge{ return &Judge{
db_w: db_w, db_w: db_w,
db_r: db_r, db_r: db_r,
db_rq: db_rq,
logger: logger.New(false), logger: logger.New(false),
rulesByService: make(map[string][]config.Rule), rulesByService: make(map[string][]config.Rule),
Blocker: b, Blocker: b,
@@ -75,31 +79,32 @@ func (j *Judge) Tribunal() {
methodMatch := rule.Method == "" || entry.Method == rule.Method methodMatch := rule.Method == "" || entry.Method == rule.Method
statusMatch := rule.Status == "" || entry.Status == rule.Status statusMatch := rule.Status == "" || entry.Status == rule.Status
pathMatch := matchPath(entry.Path, rule.Path) pathMatch := matchPath(entry.Path, rule.Path)
j.logger.Debug(
"Testing rule",
"rule", rule.Name,
"method_match", methodMatch,
"status_match", statusMatch,
"path_match", pathMatch,
)
if methodMatch && statusMatch && pathMatch { if methodMatch && statusMatch && pathMatch {
ruleMatched = true ruleMatched = true
j.logger.Info("Rule matched", "rule", rule.Name, "ip", entry.IP) j.logger.Info("Rule matched", "rule", rule.Name, "ip", entry.IP)
j.resultCh <- entry
banned, err := j.db_r.IsBanned(entry.IP) banned, err := j.db_r.IsBanned(entry.IP)
if err != nil { if err != nil {
j.logger.Error("Failed to check ban status", "ip", entry.IP, "error", err) j.logger.Error("Failed to check ban status", "ip", entry.IP, "error", err)
metrics.IncError()
break break
} }
if banned { if banned {
j.logger.Info("IP already banned", "ip", entry.IP) j.logger.Info("IP already banned", "ip", entry.IP)
j.resultCh <- entry metrics.IncLogParsed()
break
}
exceeded, err := j.db_rq.IsMaxRetryExceeded(entry.IP, rule.MaxRetry)
if err != nil {
j.logger.Error("Failed to check retry count", "ip", entry.IP, "error", err)
metrics.IncError()
break
}
if !exceeded {
j.logger.Info("Max retry not exceeded", "ip", entry.IP)
metrics.IncLogParsed()
break break
} }
err = j.db_w.AddBan(entry.IP, rule.BanTime, rule.Name) err = j.db_w.AddBan(entry.IP, rule.BanTime, rule.Name)
if err != nil { if err != nil {
j.logger.Error( j.logger.Error(
@@ -116,6 +121,7 @@ func (j *Judge) Tribunal() {
if err := j.Blocker.Ban(entry.IP); err != nil { if err := j.Blocker.Ban(entry.IP); err != nil {
j.logger.Error("Failed to ban IP at firewall", "ip", entry.IP, "error", err) j.logger.Error("Failed to ban IP at firewall", "ip", entry.IP, "error", err)
metrics.IncError()
break break
} }
j.logger.Info( j.logger.Info(
@@ -127,7 +133,7 @@ func (j *Judge) Tribunal() {
"ban_time", "ban_time",
rule.BanTime, rule.BanTime,
) )
j.resultCh <- entry metrics.IncBan(rule.ServiceName)
break break
} }
} }
@@ -148,12 +154,16 @@ func (j *Judge) UnbanChecker() {
ips, err := j.db_w.RemoveExpiredBans() ips, err := j.db_w.RemoveExpiredBans()
if err != nil { if err != nil {
j.logger.Error(fmt.Sprintf("Failed to check expired bans: %v", err)) j.logger.Error(fmt.Sprintf("Failed to check expired bans: %v", err))
metrics.IncError()
continue continue
} }
for _, ip := range ips { for _, ip := range ips {
if err := j.Blocker.Unban(ip); err != nil { if err := j.Blocker.Unban(ip); err != nil {
j.logger.Error(fmt.Sprintf("Failed to unban IP at firewall: %v", err)) j.logger.Error(fmt.Sprintf("Failed to unban IP at firewall: %v", err))
metrics.IncError()
} else {
metrics.IncUnban("judge")
} }
} }
} }

131
internal/metrics/metrics.go Normal file
View File

@@ -0,0 +1,131 @@
package metrics
import (
"fmt"
"log"
"net/http"
"strconv"
"sync"
"time"
)
var (
metricsMu sync.RWMutex
metrics = make(map[string]int64)
)
func IncBan(service string) {
metricsMu.Lock()
metrics["ban_count"]++
metrics[service+"_bans"]++
metricsMu.Unlock()
}
func IncUnban(service string) {
metricsMu.Lock()
metrics["unban_count"]++
metrics[service+"_unbans"]++
metricsMu.Unlock()
}
func IncRuleMatched(rule_name string) {
metricsMu.Lock()
metrics[rule_name+"_rule_matched"]++
metricsMu.Unlock()
}
func IncLogParsed() {
metricsMu.Lock()
metrics["log_parsed"]++
metricsMu.Unlock()
}
func IncError() {
metricsMu.Lock()
metrics["error_count"]++
metricsMu.Unlock()
}
func IncBanAttempt(firewall string) {
metricsMu.Lock()
metrics["ban_attempt_count"]++
metrics[firewall+"_ban_attempts"]++
metricsMu.Unlock()
}
func IncUnbanAttempt(firewall string) {
metricsMu.Lock()
metrics["unban_attempt_count"]++
metrics[firewall+"_unban_attempts"]++
metricsMu.Unlock()
}
func IncPortOperation(operation string, protocol string) {
metricsMu.Lock()
key := "port_" + operation + "_" + protocol
metrics[key]++
metricsMu.Unlock()
}
func IncParserEvent(service string) {
metricsMu.Lock()
metrics[service+"_parsed_events"]++
metricsMu.Unlock()
}
func IncScannerEvent(service string) {
metricsMu.Lock()
metrics[service+"_scanner_events"]++
metricsMu.Unlock()
}
func IncDBOperation(operation string, table string) {
metricsMu.Lock()
key := "db_" + operation + "_" + table
metrics[key]++
metricsMu.Unlock()
}
func IncRequestCount(service string) {
metricsMu.Lock()
metrics[service+"_request_count"]++
metricsMu.Unlock()
}
func MetricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
metricsMu.RLock()
snapshot := make(map[string]int64, len(metrics))
for k, v := range metrics {
snapshot[k] = v
}
metricsMu.RUnlock()
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
for name, value := range snapshot {
metricName := name + "_total"
_, _ = fmt.Fprintf(w, "# TYPE %s counter\n", metricName)
_, _ = fmt.Fprintf(w, "%s %d\n", metricName, value)
}
})
}
func StartMetricsServer(port int) error {
mux := http.NewServeMux()
mux.Handle("/metrics", MetricsHandler())
server := &http.Server{
Addr: "localhost:" + strconv.Itoa(port),
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
log.Printf("Starting metrics server on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("metrics server failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,63 @@
package parser
import (
"regexp"
"github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/d3m0k1d/BanForge/internal/storage"
)
type ApacheParser struct {
pattern *regexp.Regexp
logger *logger.Logger
}
func NewApacheParser() *ApacheParser {
pattern := regexp.MustCompile(
`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+-\s+-\s+\[(.*?)\]\s+"(\w+)\s+(.*?)\s+HTTP/[\d.]+"\s+(\d+)\s+(\d+|-)\s+"(.*?)"\s+"(.*?)"`,
)
// Groups:
// 1: IP
// 2: Timestamp
// 3: Method (GET, POST, etc.)
// 4: Path
// 5: Status Code (200, 404, 403...)
// 6: Response Size
// 7: Referer
// 8: User-Agent
return &ApacheParser{
pattern: pattern,
logger: logger.New(false),
}
}
func (p *ApacheParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEntry) {
// Group 1: IP, Group 2: Timestamp, Group 3: Method, Group 4: Path, Group 5: Status
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: "apache",
IP: matches[1],
Path: path,
Status: status,
Method: method,
}
metrics.IncParserEvent("apache")
p.logger.Info(
"Parsed apache log entry",
"ip", matches[1],
"path", path,
"status", status,
"method", method,
)
}
}

View File

@@ -4,6 +4,7 @@ import (
"regexp" "regexp"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/d3m0k1d/BanForge/internal/storage" "github.com/d3m0k1d/BanForge/internal/storage"
) )
@@ -40,6 +41,7 @@ func (p *NginxParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEn
Status: status, Status: status,
Method: method, Method: method,
} }
metrics.IncParserEvent("nginx")
p.logger.Info( p.logger.Info(
"Parsed nginx log entry", "Parsed nginx log entry",
"ip", "ip",

View File

@@ -2,11 +2,16 @@ package parser
import ( import (
"bufio" "bufio"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp"
"strings"
"time" "time"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
type Event struct { type Event struct {
@@ -23,7 +28,56 @@ type Scanner struct {
pollDelay time.Duration pollDelay time.Duration
} }
func validateLogPath(path string) error {
if path == "" {
return fmt.Errorf("log path cannot be empty")
}
if !filepath.IsAbs(path) {
return fmt.Errorf("log path must be absolute: %s", path)
}
if strings.Contains(path, "..") {
return fmt.Errorf("log path contains '..': %s", path)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("log file does not exist: %s", path)
}
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("failed to stat log file: %w", err)
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("log path is a symlink: %s", path)
}
return nil
}
func validateJournaldUnit(unit string) error {
if unit == "" {
return fmt.Errorf("journald unit cannot be empty")
}
if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(unit) {
return fmt.Errorf("invalid journald unit name: %s", unit)
}
if strings.HasPrefix(unit, "-") {
return fmt.Errorf("journald unit cannot start with '-': %s", unit)
}
return nil
}
func NewScannerTail(path string) (*Scanner, error) { func NewScannerTail(path string) (*Scanner, error) {
if err := validateLogPath(path); err != nil {
return nil, fmt.Errorf("invalid log path: %w", err)
}
// #nosec G204 - path is validated above via validateLogPath()
cmd := exec.Command("tail", "-F", "-n", "10", path) cmd := exec.Command("tail", "-F", "-n", "10", path)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@@ -46,6 +100,11 @@ func NewScannerTail(path string) (*Scanner, error) {
} }
func NewScannerJournald(unit string) (*Scanner, error) { func NewScannerJournald(unit string) (*Scanner, error) {
if err := validateJournaldUnit(unit); err != nil {
return nil, fmt.Errorf("invalid journald unit: %w", err)
}
// #nosec G204 - unit is validated above via validateJournaldUnit()
cmd := exec.Command("journalctl", "-u", unit, "-f", "-n", "0", "-o", "short", "--no-pager") cmd := exec.Command("journalctl", "-u", unit, "-f", "-n", "0", "-o", "short", "--no-pager")
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@@ -79,6 +138,7 @@ func (s *Scanner) Start() {
default: default:
if s.scanner.Scan() { if s.scanner.Scan() {
metrics.IncScannerEvent("scanner")
s.ch <- Event{ s.ch <- Event{
Data: s.scanner.Text(), Data: s.scanner.Text(),
} }
@@ -86,6 +146,7 @@ func (s *Scanner) Start() {
} 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")
metrics.IncError()
return return
} }
} }

View File

@@ -2,6 +2,7 @@ package parser
import ( import (
"os" "os"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -281,3 +282,201 @@ func BenchmarkScanner(b *testing.B) {
<-scanner.Events() <-scanner.Events()
} }
} }
func TestValidateLogPath(t *testing.T) {
tests := []struct {
name string
path string
setup func() (string, func())
wantErr bool
errMsg string
}{
{
name: "empty path",
path: "",
wantErr: true,
errMsg: "log path cannot be empty",
},
{
name: "relative path",
path: "logs/test.log",
wantErr: true,
errMsg: "log path must be absolute",
},
{
name: "path with traversal",
path: "/var/log/../etc/passwd",
wantErr: true,
errMsg: "log path contains '..'",
},
{
name: "non-existent file",
path: "/var/log/nonexistent.log",
wantErr: true,
errMsg: "log file does not exist",
},
{
name: "valid file",
path: "/tmp/test-valid.log",
setup: func() (string, func()) {
_, _ = os.Create("/tmp/test-valid.log")
return "/tmp/test-valid.log", func() { os.Remove("/tmp/test-valid.log") }
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cleanup func()
if tt.setup != nil {
tt.path, cleanup = tt.setup()
defer cleanup()
}
err := validateLogPath(tt.path)
if (err != nil) != tt.wantErr {
t.Errorf("validateLogPath() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr && tt.errMsg != "" && err != nil {
if !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("validateLogPath() error = %v, want message containing %q", err, tt.errMsg)
}
}
})
}
}
func TestValidateJournaldUnit(t *testing.T) {
tests := []struct {
name string
unit string
wantErr bool
errMsg string
}{
{
name: "empty unit",
unit: "",
wantErr: true,
errMsg: "journald unit cannot be empty",
},
{
name: "unit starting with dash",
unit: "-dangerous",
wantErr: true,
errMsg: "journald unit cannot start with '-'",
},
{
name: "unit with special chars",
unit: "test;rm -rf /",
wantErr: true,
errMsg: "invalid journald unit name",
},
{
name: "unit with spaces",
unit: "test unit",
wantErr: true,
errMsg: "invalid journald unit name",
},
{
name: "valid unit simple",
unit: "nginx",
wantErr: false,
},
{
name: "valid unit with dash",
unit: "ssh-agent",
wantErr: false,
},
{
name: "valid unit with dot",
unit: "systemd-journald.service",
wantErr: false,
},
{
name: "valid unit with underscore",
unit: "my_service",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateJournaldUnit(tt.unit)
if (err != nil) != tt.wantErr {
t.Errorf("validateJournaldUnit() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr && tt.errMsg != "" && err != nil {
if !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("validateJournaldUnit() error = %v, want message containing %q", err, tt.errMsg)
}
}
})
}
}
func TestNewScannerTailValidation(t *testing.T) {
tests := []struct {
name string
path string
wantErr bool
}{
{
name: "empty path",
path: "",
wantErr: true,
},
{
name: "relative path",
path: "test.log",
wantErr: true,
},
{
name: "non-existent path",
path: "/nonexistent/path/file.log",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewScannerTail(tt.path)
if (err != nil) != tt.wantErr {
t.Errorf("NewScannerTail() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestNewScannerJournaldValidation(t *testing.T) {
tests := []struct {
name string
unit string
wantErr bool
}{
{
name: "empty unit",
unit: "",
wantErr: true,
},
{
name: "unit with semicolon",
unit: "test;rm -rf /",
wantErr: true,
},
{
name: "unit starting with dash",
unit: "-dangerous",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewScannerJournald(tt.unit)
if (err != nil) != tt.wantErr {
t.Errorf("NewScannerJournald() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"regexp" "regexp"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/d3m0k1d/BanForge/internal/storage" "github.com/d3m0k1d/BanForge/internal/storage"
) )
@@ -24,7 +25,6 @@ func NewSshdParser() *SshdParser {
func (p *SshdParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEntry) { func (p *SshdParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEntry) {
// Group 1: Timestamp, Group 2: hostame, Group 3: pid, Group 4: Method auth, Group 5: User, Group 6: IP, Group 7: port // Group 1: Timestamp, Group 2: hostame, Group 3: pid, Group 4: Method auth, Group 5: User, Group 6: IP, Group 7: port
go func() {
for event := range eventCh { for event := range eventCh {
matches := p.pattern.FindStringSubmatch(event.Data) matches := p.pattern.FindStringSubmatch(event.Data)
if matches == nil { if matches == nil {
@@ -37,6 +37,7 @@ func (p *SshdParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEnt
Status: "Failed", Status: "Failed",
Method: matches[4], // method auth Method: matches[4], // method auth
} }
metrics.IncParserEvent("ssh")
p.logger.Info( p.logger.Info(
"Parsed ssh log entry", "Parsed ssh log entry",
"ip", "ip",
@@ -49,5 +50,4 @@ func (p *SshdParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogEnt
"Failed", "Failed",
) )
} }
}()
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/d3m0k1d/BanForge/internal/config" "github.com/d3m0k1d/BanForge/internal/config"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@@ -21,7 +22,7 @@ type BanWriter struct {
func NewBanWriter() (*BanWriter, error) { func NewBanWriter() (*BanWriter, error) {
db, err := sql.Open( db, err := sql.Open(
"sqlite", "sqlite",
"/var/lib/banforge/bans.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)", buildSqliteDsn(banDBPath, pragmas),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -37,6 +38,7 @@ func (d *BanWriter) CreateTable() error {
if err != nil { if err != nil {
return err return err
} }
metrics.IncDBOperation("create_table", "bans")
d.logger.Info("Created tables") d.logger.Info("Created tables")
return nil return nil
} }
@@ -45,6 +47,7 @@ func (d *BanWriter) AddBan(ip string, ttl string, reason string) error {
duration, err := config.ParseDurationWithYears(ttl) duration, err := config.ParseDurationWithYears(ttl)
if err != nil { if err != nil {
d.logger.Error("Invalid duration format", "ttl", ttl, "error", err) d.logger.Error("Invalid duration format", "ttl", ttl, "error", err)
metrics.IncError()
return fmt.Errorf("invalid duration: %w", err) return fmt.Errorf("invalid duration: %w", err)
} }
@@ -60,9 +63,11 @@ func (d *BanWriter) AddBan(ip string, ttl string, reason string) error {
) )
if err != nil { if err != nil {
d.logger.Error("Failed to add ban", "error", err) d.logger.Error("Failed to add ban", "error", err)
metrics.IncError()
return err return err
} }
metrics.IncDBOperation("insert", "bans")
return nil return nil
} }
@@ -70,8 +75,10 @@ func (d *BanWriter) RemoveBan(ip string) error {
_, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip) _, err := d.db.Exec("DELETE FROM bans WHERE ip = ?", ip)
if err != nil { if err != nil {
d.logger.Error("Failed to remove ban", "error", err) d.logger.Error("Failed to remove ban", "error", err)
metrics.IncError()
return err return err
} }
metrics.IncDBOperation("delete", "bans")
return nil return nil
} }
@@ -85,6 +92,7 @@ func (w *BanWriter) RemoveExpiredBans() ([]string, error) {
) )
if err != nil { if err != nil {
w.logger.Error("Failed to get expired bans", "error", err) w.logger.Error("Failed to get expired bans", "error", err)
metrics.IncError()
return nil, err return nil, err
} }
defer func() { defer func() {
@@ -113,6 +121,7 @@ func (w *BanWriter) RemoveExpiredBans() ([]string, error) {
) )
if err != nil { if err != nil {
w.logger.Error("Failed to remove expired bans", "error", err) w.logger.Error("Failed to remove expired bans", "error", err)
metrics.IncError()
return nil, err return nil, err
} }
@@ -123,6 +132,7 @@ func (w *BanWriter) RemoveExpiredBans() ([]string, error) {
if rowsAffected > 0 { if rowsAffected > 0 {
w.logger.Info("Removed expired bans", "count", rowsAffected, "ips", len(ips)) w.logger.Info("Removed expired bans", "count", rowsAffected, "ips", len(ips))
metrics.IncDBOperation("delete_expired", "bans")
} }
return ips, nil return ips, nil
@@ -169,13 +179,14 @@ func (d *BanReader) IsBanned(ip string) (bool, error) {
return false, nil return false, nil
} }
if err != nil { if err != nil {
metrics.IncError()
return false, fmt.Errorf("failed to check ban status: %w", err) return false, fmt.Errorf("failed to check ban status: %w", err)
} }
metrics.IncDBOperation("select", "bans")
return true, nil return true, nil
} }
func (d *BanReader) BanList() error { func (d *BanReader) BanList() error {
var count int var count int
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
@@ -184,6 +195,7 @@ func (d *BanReader) BanList() error {
rows, err := d.db.Query("SELECT ip, banned_at, reason, expired_at FROM bans") rows, err := d.db.Query("SELECT ip, banned_at, reason, expired_at FROM bans")
if err != nil { if err != nil {
d.logger.Error("Failed to get ban list", "error", err) d.logger.Error("Failed to get ban list", "error", err)
metrics.IncError()
return err return err
} }
for rows.Next() { for rows.Next() {
@@ -195,12 +207,14 @@ func (d *BanReader) BanList() error {
err := rows.Scan(&ip, &bannedAt, &reason, &expiredAt) err := rows.Scan(&ip, &bannedAt, &reason, &expiredAt)
if err != nil { if err != nil {
d.logger.Error("Failed to get ban list", "error", err) d.logger.Error("Failed to get ban list", "error", err)
metrics.IncError()
return err return err
} }
t.AppendRow(table.Row{count, ip, bannedAt, reason, expiredAt}) t.AppendRow(table.Row{count, ip, bannedAt, reason, expiredAt})
} }
t.Render() t.Render()
metrics.IncDBOperation("select", "bans")
return nil return nil
} }

View File

@@ -2,55 +2,60 @@ package storage
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"strings"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func CreateTables() error { const (
// Requests DB DBDir = "/var/lib/banforge/"
db_r, err := sql.Open("sqlite", ReqDBPath = DBDir + "requests.db"
"/var/lib/banforge/requests.db?"+ banDBPath = DBDir + "bans.db"
"mode=rwc&"+ )
"_pragma=journal_mode(WAL)&"+
"_pragma=busy_timeout(30000)&"+
"_pragma=synchronous(NORMAL)")
if err != nil {
return fmt.Errorf("failed to open requests db: %w", err)
}
defer func() {
err = db_r.Close()
if err != nil {
fmt.Println(err)
}
}()
_, err = db_r.Exec(CreateRequestsTable) var pragmas = map[string]string{
if err != nil { `journal_mode`: `wal`,
return fmt.Errorf("failed to create requests table: %w", err) `synchronous`: `normal`,
} `busy_timeout`: `30000`,
// also consider these
// Bans DB // `temp_store`: `memory`,
db_b, err := sql.Open("sqlite", // `cache_size`: `1000000000`,
"/var/lib/banforge/bans.db?"+ }
"mode=rwc&"+
"_pragma=journal_mode(WAL)&"+ func buildSqliteDsn(path string, pragmas map[string]string) string {
"_pragma=busy_timeout(30000)&"+ pragmastrs := make([]string, len(pragmas))
"_pragma=synchronous(FULL)") i := 0
if err != nil { for k, v := range pragmas {
return fmt.Errorf("failed to open bans db: %w", err) pragmastrs[i] = (fmt.Sprintf(`pragma=%s(%s)`, k, v))
} i++
defer func() { }
err = db_b.Close() return path + "?" + "mode=rwc&" + strings.Join(pragmastrs, "&")
if err != nil { }
fmt.Println(err)
} func initDB(dsn, sqlstr string) (err error) {
}() db, err := sql.Open("sqlite", dsn)
if err != nil {
_, err = db_b.Exec(CreateBansTable) return fmt.Errorf("failed to open %q: %w", dsn, err)
if err != nil { }
return fmt.Errorf("failed to create bans table: %w", err) defer func() {
} closeErr := db.Close()
fmt.Println("Tables created successfully!") if closeErr != nil {
return nil err = errors.Join(err, fmt.Errorf("failed to close %q: %w", dsn, closeErr))
}
}()
_, err = db.Exec(sqlstr)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return err
}
func CreateTables() (err error) {
// Requests DB
err1 := initDB(buildSqliteDsn(ReqDBPath, pragmas), CreateRequestsTable)
err2 := initDB(buildSqliteDsn(banDBPath, pragmas), CreateBansTable)
return errors.Join(err1, err2)
} }

View File

@@ -4,18 +4,19 @@ import (
"database/sql" "database/sql"
"github.com/d3m0k1d/BanForge/internal/logger" "github.com/d3m0k1d/BanForge/internal/logger"
"github.com/d3m0k1d/BanForge/internal/metrics"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
type Request_Writer struct { type RequestWriter struct {
logger *logger.Logger logger *logger.Logger
db *sql.DB db *sql.DB
} }
func NewRequestsWr() (*Request_Writer, error) { func NewRequestsWr() (*RequestWriter, error) {
db, err := sql.Open( db, err := sql.Open(
"sqlite", "sqlite",
"/var/lib/banforge/requests.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)", buildSqliteDsn(ReqDBPath, pragmas),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -23,8 +24,46 @@ func NewRequestsWr() (*Request_Writer, error) {
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1) db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) db.SetConnMaxLifetime(0)
return &Request_Writer{ return &RequestWriter{
logger: logger.New(false), logger: logger.New(false),
db: db, db: db,
}, nil }, nil
} }
type RequestReader struct {
logger *logger.Logger
db *sql.DB
}
func NewRequestsRd() (*RequestReader, error) {
db, err := sql.Open(
"sqlite",
buildSqliteDsn(ReqDBPath, pragmas),
)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0)
return &RequestReader{
logger: logger.New(false),
db: db,
}, nil
}
func (r *RequestReader) IsMaxRetryExceeded(ip string, maxRetry int) (bool, error) {
var count int
if maxRetry == 0 {
return true, nil
}
err := r.db.QueryRow("SELECT COUNT(*) FROM requests WHERE ip = ?", ip).Scan(&count)
if err != nil {
r.logger.Error("error query count: " + err.Error())
metrics.IncError()
return false, err
}
r.logger.Info("Current request count for IP", "ip", ip, "count", count, "maxRetry", maxRetry)
metrics.IncDBOperation("select", "requests")
return count >= maxRetry, nil
}

View File

@@ -1,10 +1,15 @@
package storage package storage
import ( import (
"database/sql"
"errors"
"fmt"
"time" "time"
"github.com/d3m0k1d/BanForge/internal/metrics"
) )
func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) { func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
db.logger.Info("Starting log writer") db.logger.Info("Starting log writer")
const batchSize = 100 const batchSize = 100
const flushInterval = 1 * time.Second const flushInterval = 1 * time.Second
@@ -14,29 +19,36 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
defer ticker.Stop() defer ticker.Stop()
flush := func() { flush := func() {
defer db.logger.Debug("Flushed batch", "count", len(batch))
err := func() (err error) {
if len(batch) == 0 { if len(batch) == 0 {
return return nil
} }
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
db.logger.Error("Failed to begin transaction", "error", err) return fmt.Errorf("failed to begin transaction: %w", err)
return
} }
defer func() {
if rollbackErr := tx.Rollback(); rollbackErr != nil &&
!errors.Is(rollbackErr, sql.ErrTxDone) {
err = errors.Join(
err,
fmt.Errorf("failed to rollback transaction: %w", rollbackErr),
)
}
}()
stmt, err := tx.Prepare( stmt, err := tx.Prepare(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
) )
if err != nil { if err != nil {
db.logger.Error("Failed to prepare statement", "error", err) err = fmt.Errorf("failed to prepare statement: %w", err)
if rollbackErr := tx.Rollback(); rollbackErr != nil { return err
db.logger.Error("Failed to rollback transaction", "error", rollbackErr)
}
return
} }
defer func() { defer func() {
if closeErr := stmt.Close(); closeErr != nil { if closeErr := stmt.Close(); closeErr != nil {
db.logger.Error("Failed to close statement", "error", closeErr) err = errors.Join(err, fmt.Errorf("failed to close statement: %w", closeErr))
} }
}() }()
@@ -50,17 +62,24 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
time.Now().Format(time.RFC3339), time.Now().Format(time.RFC3339),
) )
if err != nil { if err != nil {
db.logger.Error("Failed to insert entry", "error", err) db.logger.Error(fmt.Errorf("failed to insert entry: %w", err).Error())
metrics.IncError()
} else {
metrics.IncRequestCount(entry.Service)
} }
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
db.logger.Error("Failed to commit transaction", "error", err) return fmt.Errorf("failed to commit transaction: %w", err)
return
} }
db.logger.Debug("Flushed batch", "count", len(batch))
batch = batch[:0] batch = batch[:0]
metrics.IncDBOperation("insert", "requests")
return err
}()
if err != nil {
db.logger.Error(err.Error())
}
} }
for { for {
@@ -81,3 +100,13 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
} }
} }
} }
func (w *RequestWriter) GetRequestCount() (int, error) {
var count int
err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count)
return count, err
}
func (w *RequestWriter) Close() error {
return w.db.Close()
}

View File

@@ -277,7 +277,7 @@ func TestWrite_ChannelClosed(t *testing.T) {
} }
} }
func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) { func NewRequestWriterWithDBPath(dbPath string) (*RequestWriter, error) {
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)") db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -285,13 +285,13 @@ func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) {
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1) db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) db.SetConnMaxLifetime(0)
return &Request_Writer{ return &RequestWriter{
logger: logger.New(false), logger: logger.New(false),
db: db, db: db,
}, nil }, nil
} }
func (w *Request_Writer) CreateTable() error { func (w *RequestWriter) CreateTable() error {
_, err := w.db.Exec(CreateRequestsTable) _, err := w.db.Exec(CreateRequestsTable)
if err != nil { if err != nil {
return err return err
@@ -299,21 +299,3 @@ func (w *Request_Writer) CreateTable() error {
w.logger.Info("Created requests table") w.logger.Info("Created requests table")
return nil return nil
} }
func (w *Request_Writer) Close() error {
w.logger.Info("Closing request database connection")
err := w.db.Close()
if err != nil {
return err
}
return nil
}
func (w *Request_Writer) GetRequestCount() (int, error) {
var count int
err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}