Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9df055765 | ||
|
|
6897ea8753 | ||
|
|
d534fc79d7 | ||
|
|
9ad0a3eb12 | ||
|
|
d8712037f4 | ||
|
|
aef2647a82 | ||
|
|
c3b6708a98 | ||
|
|
3acd0b899c | ||
|
|
3ac1250bfc | ||
|
|
7bba444522 |
@@ -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"
|
||||||
@@ -60,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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,36 +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\n MaxRetry: %d\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.MaxRetry,
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.5.0"
|
var version = "0.5.2"
|
||||||
|
|
||||||
var VersionCmd = &cobra.Command{
|
var VersionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
|
|||||||
179
docs/cli.md
179
docs/cli.md
@@ -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,12 @@ 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
|
### version - Display BanForge version
|
||||||
|
|
||||||
@@ -21,6 +29,8 @@ banforge version
|
|||||||
**Description**
|
**Description**
|
||||||
This command displays the current version of the BanForge software.
|
This command displays the current version of the BanForge software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### daemon - Starts the BanForge daemon process
|
### daemon - Starts the BanForge daemon process
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -32,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>
|
||||||
@@ -41,9 +54,22 @@ 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 |
|
||||||
|
| ----------- | ------------------------------ |
|
||||||
|
| `-t`, `-ttl` | Ban duration (default: 1 year) |
|
||||||
|
|
||||||
### ports - Open and Close ports on firewall
|
**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
|
```shell
|
||||||
banforge open -port <port> -protocol <protocol>
|
banforge open -port <port> -protocol <protocol>
|
||||||
@@ -53,31 +79,148 @@ banforge close -port <port> -protocol <protocol>
|
|||||||
**Description**
|
**Description**
|
||||||
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
|
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
|
||||||
|
|
||||||
### list - Lists the IP addresses that are currently blocked
|
| 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`
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ Example:
|
|||||||
**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.
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -19,7 +19,6 @@ require (
|
|||||||
github.com/mattn/go-runewidth v0.0.20 // 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-20260218203240-3dfff04db8fa // indirect
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
|||||||
27
go.sum
27
go.sum
@@ -19,8 +19,6 @@ 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.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
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=
|
||||||
@@ -29,9 +27,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
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=
|
||||||
@@ -41,27 +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-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
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.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
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.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.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.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
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.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
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=
|
||||||
@@ -69,20 +56,16 @@ 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.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
|
||||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||||
|
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.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
|
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.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
|
||||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
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=
|
||||||
@@ -93,8 +76,6 @@ 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.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||||
modernc.org/sqlite v1.46.1/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=
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"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 {
|
||||||
@@ -23,20 +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
|
// #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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +50,24 @@ 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
|
// #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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +79,7 @@ func (f *Firewalld) PortOpen(port int, protocol string) error {
|
|||||||
return fmt.Errorf("invalid protocol")
|
return fmt.Errorf("invalid protocol")
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("open", protocol)
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"firewall-cmd",
|
"firewall-cmd",
|
||||||
"--zone=public",
|
"--zone=public",
|
||||||
@@ -79,12 +89,14 @@ func (f *Firewalld) PortOpen(port int, protocol string) error {
|
|||||||
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 port " + s + " " + string(output))
|
f.logger.Info("Add port " + s + " " + 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))
|
||||||
@@ -99,6 +111,7 @@ func (f *Firewalld) PortClose(port int, protocol string) error {
|
|||||||
return fmt.Errorf("invalid protocol")
|
return fmt.Errorf("invalid protocol")
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("close", protocol)
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"firewall-cmd",
|
"firewall-cmd",
|
||||||
"--zone=public",
|
"--zone=public",
|
||||||
@@ -107,11 +120,13 @@ func (f *Firewalld) PortClose(port int, protocol string) error {
|
|||||||
)
|
)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.logger.Info("Remove port " + s + " " + string(output))
|
f.logger.Info("Remove port " + s + " " + string(output))
|
||||||
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.logger.Info("Reload " + string(output))
|
f.logger.Info("Reload " + string(output))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"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 {
|
||||||
@@ -24,6 +25,7 @@ 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
|
||||||
@@ -36,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 {
|
||||||
@@ -54,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",
|
||||||
@@ -67,6 +72,7 @@ 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
|
||||||
@@ -79,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 {
|
||||||
@@ -97,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",
|
||||||
@@ -112,11 +121,13 @@ func (f *Iptables) PortOpen(port int, protocol string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("open", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command("iptables", "-A", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
cmd := exec.Command("iptables", "-A", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
||||||
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 port " + s + " " + string(output))
|
f.logger.Info("Add port " + s + " " + string(output))
|
||||||
@@ -128,6 +139,7 @@ func (f *Iptables) PortOpen(port int, protocol 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,11 +153,13 @@ func (f *Iptables) PortClose(port int, protocol string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("close", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command("iptables", "-D", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
cmd := exec.Command("iptables", "-D", "INPUT", "-p", protocol, "--dport", s, "-j", "ACCEPT")
|
||||||
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 port " + s + " " + string(output))
|
f.logger.Info("Add port " + s + " " + string(output))
|
||||||
@@ -157,6 +171,7 @@ func (f *Iptables) PortClose(port int, protocol 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"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 {
|
||||||
@@ -26,6 +27,7 @@ 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
|
// #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")
|
||||||
@@ -35,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,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
|
||||||
@@ -80,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,9 +183,11 @@ func (n *Nftables) PortOpen(port int, protocol string) error {
|
|||||||
if port >= 0 && port <= 65535 {
|
if port >= 0 && port <= 65535 {
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
if protocol != "tcp" && protocol != "udp" {
|
||||||
n.logger.Error("invalid protocol")
|
n.logger.Error("invalid protocol")
|
||||||
|
metrics.IncError()
|
||||||
return fmt.Errorf("invalid protocol")
|
return fmt.Errorf("invalid protocol")
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("open", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"nft",
|
"nft",
|
||||||
@@ -191,6 +204,7 @@ func (n *Nftables) PortOpen(port int, protocol string) error {
|
|||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.logger.Error(err.Error())
|
n.logger.Error(err.Error())
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n.logger.Info("Add port " + s + " " + string(output))
|
n.logger.Info("Add port " + s + " " + string(output))
|
||||||
@@ -199,6 +213,7 @@ func (n *Nftables) PortOpen(port int, protocol string) error {
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,9 +224,11 @@ func (n *Nftables) PortClose(port int, protocol string) error {
|
|||||||
if port >= 0 && port <= 65535 {
|
if port >= 0 && port <= 65535 {
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
if protocol != "tcp" && protocol != "udp" {
|
||||||
n.logger.Error("invalid protocol")
|
n.logger.Error("invalid protocol")
|
||||||
|
metrics.IncError()
|
||||||
return fmt.Errorf("invalid protocol")
|
return fmt.Errorf("invalid protocol")
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("close", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"nft",
|
"nft",
|
||||||
@@ -228,6 +245,7 @@ func (n *Nftables) PortClose(port int, protocol string) error {
|
|||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.logger.Error(err.Error())
|
n.logger.Error(err.Error())
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n.logger.Info("Add port " + s + " " + string(output))
|
n.logger.Info("Add port " + s + " " + string(output))
|
||||||
@@ -236,6 +254,7 @@ func (n *Nftables) PortClose(port int, protocol string) error {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"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 {
|
||||||
@@ -23,6 +24,7 @@ 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
|
// #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()
|
||||||
@@ -31,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 {
|
||||||
@@ -42,6 +46,7 @@ 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
|
// #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()
|
||||||
@@ -50,10 +55,12 @@ 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +68,17 @@ func (u *Ufw) PortOpen(port int, protocol string) error {
|
|||||||
if port >= 0 && port <= 65535 {
|
if port >= 0 && port <= 65535 {
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
if protocol != "tcp" && protocol != "udp" {
|
||||||
u.logger.Error("invalid protocol")
|
u.logger.Error("invalid protocol")
|
||||||
|
metrics.IncError()
|
||||||
return fmt.Errorf("invalid protocol")
|
return fmt.Errorf("invalid protocol")
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("open", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command("ufw", "allow", s+"/"+protocol)
|
cmd := exec.Command("ufw", "allow", s+"/"+protocol)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logger.Error(err.Error())
|
u.logger.Error(err.Error())
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.logger.Info("Add port " + s + " " + string(output))
|
u.logger.Info("Add port " + s + " " + string(output))
|
||||||
@@ -80,14 +90,17 @@ func (u *Ufw) PortClose(port int, protocol string) error {
|
|||||||
if port >= 0 && port <= 65535 {
|
if port >= 0 && port <= 65535 {
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
if protocol != "tcp" && protocol != "udp" {
|
||||||
u.logger.Error("invalid protocol")
|
u.logger.Error("invalid protocol")
|
||||||
|
metrics.IncError()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s := strconv.Itoa(port)
|
s := strconv.Itoa(port)
|
||||||
|
metrics.IncPortOperation("close", protocol)
|
||||||
// #nosec G204 - managed by system adminstartor
|
// #nosec G204 - managed by system adminstartor
|
||||||
cmd := exec.Command("ufw", "deny", s+"/"+protocol)
|
cmd := exec.Command("ufw", "deny", s+"/"+protocol)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logger.Error(err.Error())
|
u.logger.Error(err.Error())
|
||||||
|
metrics.IncError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.logger.Info("Add port " + s + " " + string(output))
|
u.logger.Info("Add port " + s + " " + string(output))
|
||||||
|
|||||||
@@ -1,127 +1,162 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"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 = errors.Join(err, file.Close())
|
if closeErr := file.Close(); closeErr != nil {
|
||||||
}()
|
fmt.Printf("warning: failed to close rule file: %v\n", closeErr)
|
||||||
cfg := Rules{Rules: r}
|
|
||||||
err = toml.NewEncoder(file).Encode(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := toml.NewEncoder(file).Encode(cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode rule: %w", 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 ss, ok := strings.CutSuffix(s, "y"); ok {
|
if ss, ok := strings.CutSuffix(s, "y"); ok {
|
||||||
years, err := strconv.Atoi(ss)
|
years, err := strconv.Atoi(ss)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,3 +32,8 @@ type Rule struct {
|
|||||||
MaxRetry int `toml:"max_retry"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,19 +86,23 @@ func (j *Judge) Tribunal() {
|
|||||||
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)
|
||||||
|
metrics.IncLogParsed()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
exceeded, err := j.db_rq.IsMaxRetryExceeded(entry.IP, rule.MaxRetry)
|
exceeded, err := j.db_rq.IsMaxRetryExceeded(entry.IP, rule.MaxRetry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
j.logger.Error("Failed to check retry count", "ip", entry.IP, "error", err)
|
j.logger.Error("Failed to check retry count", "ip", entry.IP, "error", err)
|
||||||
|
metrics.IncError()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !exceeded {
|
if !exceeded {
|
||||||
j.logger.Info("Max retry not exceeded", "ip", entry.IP)
|
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)
|
||||||
@@ -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,6 +133,7 @@ func (j *Judge) Tribunal() {
|
|||||||
"ban_time",
|
"ban_time",
|
||||||
rule.BanTime,
|
rule.BanTime,
|
||||||
)
|
)
|
||||||
|
metrics.IncBan(rule.ServiceName)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,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
131
internal/metrics/metrics.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ func (p *ApacheParser) Parse(eventCh <-chan Event, resultCh chan<- *storage.LogE
|
|||||||
Status: status,
|
Status: status,
|
||||||
Method: method,
|
Method: method,
|
||||||
}
|
}
|
||||||
|
metrics.IncParserEvent("apache")
|
||||||
p.logger.Info(
|
p.logger.Info(
|
||||||
"Parsed apache log entry",
|
"Parsed apache log entry",
|
||||||
"ip", matches[1],
|
"ip", matches[1],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,8 +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) {
|
||||||
// #nosec G204 - managed by system adminstartor
|
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 {
|
||||||
@@ -47,7 +100,11 @@ func NewScannerTail(path string) (*Scanner, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewScannerJournald(unit string) (*Scanner, error) {
|
func NewScannerJournald(unit string) (*Scanner, error) {
|
||||||
// #nosec G204 - managed by system adminstartor
|
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 {
|
||||||
@@ -81,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(),
|
||||||
}
|
}
|
||||||
@@ -88,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -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,8 +179,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,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() {
|
||||||
@@ -194,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,11 +54,16 @@ func NewRequestsRd() (*RequestReader, error) {
|
|||||||
|
|
||||||
func (r *RequestReader) IsMaxRetryExceeded(ip string, maxRetry int) (bool, error) {
|
func (r *RequestReader) IsMaxRetryExceeded(ip string, maxRetry int) (bool, error) {
|
||||||
var count int
|
var count int
|
||||||
|
if maxRetry == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
err := r.db.QueryRow("SELECT COUNT(*) FROM requests WHERE ip = ?", ip).Scan(&count)
|
err := r.db.QueryRow("SELECT COUNT(*) FROM requests WHERE ip = ?", ip).Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("error query count: " + err.Error())
|
r.logger.Error("error query count: " + err.Error())
|
||||||
|
metrics.IncError()
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
r.logger.Info("Current request count for IP", "ip", ip, "count", count, "maxRetry", maxRetry)
|
r.logger.Info("Current request count for IP", "ip", ip, "count", count, "maxRetry", maxRetry)
|
||||||
|
metrics.IncDBOperation("select", "requests")
|
||||||
return count >= maxRetry, nil
|
return count >= maxRetry, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
|
func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
|
||||||
@@ -61,6 +63,9 @@ func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.logger.Error(fmt.Errorf("failed to insert entry: %w", err).Error())
|
db.logger.Error(fmt.Errorf("failed to insert entry: %w", err).Error())
|
||||||
|
metrics.IncError()
|
||||||
|
} else {
|
||||||
|
metrics.IncRequestCount(entry.Service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +74,7 @@ func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
batch = batch[:0]
|
batch = batch[:0]
|
||||||
|
metrics.IncDBOperation("insert", "requests")
|
||||||
return err
|
return err
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user