Update screenshots and pre-fix the filter-test

This commit is contained in:
2025-11-12 19:09:01 +01:00
parent 3b118cb616
commit 0134b7de5e
13 changed files with 75 additions and 76 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -295,7 +295,8 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
} }
// Parse jail.d directory // Parse jail.d directory
jailDList, err := sc.runRemoteCommand(ctx, []string{"sudo", "bash", "-c", "for f in /etc/fail2ban/jail.d/*.conf; do [ -f \"$f\" ] && echo \"$f\"; done"}) jailDCmd := "sudo find /etc/fail2ban/jail.d -maxdepth 1 -name '*.conf' -type f"
jailDList, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", jailDCmd})
if err == nil && jailDList != "" { if err == nil && jailDList != "" {
for _, file := range strings.Split(jailDList, "\n") { for _, file := range strings.Split(jailDList, "\n") {
file = strings.TrimSpace(file) file = strings.TrimSpace(file)
@@ -351,15 +352,45 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
// GetFilters implements Connector. // GetFilters implements Connector.
func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) { func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) {
list, err := sc.runRemoteCommand(ctx, []string{"sudo", "bash", "-c", "for f in /etc/fail2ban/filter.d/*.conf; do [ -f \"$f\" ] && basename \"$f\" .conf; done"}) // Use find with sudo - execute sudo separately to avoid shell issues
// First try with sudo, if that fails, the error will be clear
list, err := sc.runRemoteCommand(ctx, []string{"sudo", "find", "/etc/fail2ban/filter.d", "-maxdepth", "1", "-type", "f"})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list filters: %w", err) return nil, fmt.Errorf("failed to list filters: %w", err)
} }
// Filter for .conf files and extract names in Go
var filters []string var filters []string
seen := make(map[string]bool) // Avoid duplicates
for _, line := range strings.Split(list, "\n") { for _, line := range strings.Split(list, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line != "" { if line == "" {
filters = append(filters, line) continue
}
// Only process .conf files - be strict about the extension
if !strings.HasSuffix(line, ".conf") {
continue
}
// Exclude backup files and other non-filter files
if strings.HasSuffix(line, ".conf.bak") ||
strings.HasSuffix(line, ".conf~") ||
strings.HasSuffix(line, ".conf.old") ||
strings.HasSuffix(line, ".conf.rpmnew") ||
strings.HasSuffix(line, ".conf.rpmsave") ||
strings.Contains(line, "README") {
continue
}
parts := strings.Split(line, "/")
if len(parts) > 0 {
filename := parts[len(parts)-1]
// Double-check it ends with .conf
if !strings.HasSuffix(filename, ".conf") {
continue
}
name := strings.TrimSuffix(filename, ".conf")
if name != "" && !seen[name] {
seen[name] = true
filters = append(filters, name)
}
} }
} }
return filters, nil return filters, nil
@@ -371,51 +402,40 @@ func (sc *SSHConnector) TestFilter(ctx context.Context, filterName string, logLi
return []string{}, nil return []string{}, nil
} }
// Read filter config remotely // Sanitize filter name to prevent path traversal
filterName = strings.TrimSpace(filterName)
if filterName == "" {
return nil, fmt.Errorf("filter name cannot be empty")
}
// Remove any path components
filterName = strings.ReplaceAll(filterName, "/", "")
filterName = strings.ReplaceAll(filterName, "..", "")
// Use fail2ban-regex with filter name directly - it handles everything
// Format: fail2ban-regex "log line" /etc/fail2ban/filter.d/filter-name.conf
filterPath := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", filterName) filterPath := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", filterName)
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", filterPath})
if err != nil {
return nil, fmt.Errorf("filter %s not found: %w", filterName, err)
}
// Extract failregex
var failregex string
scanner := bufio.NewScanner(strings.NewReader(content))
inFailregex := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[Definition]") {
inFailregex = true
continue
}
if inFailregex && strings.HasPrefix(line, "failregex") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
failregex = strings.TrimSpace(parts[1])
}
break
}
if inFailregex && strings.HasPrefix(line, "[") {
break
}
}
if failregex == "" {
return nil, fmt.Errorf("no failregex found in filter %s", filterName)
}
// Test each log line remotely
var matches []string var matches []string
for _, logLine := range logLines { for _, logLine := range logLines {
logLine = strings.TrimSpace(logLine)
if logLine == "" { if logLine == "" {
continue continue
} }
// Escape the log line and regex for shell // Use fail2ban-regex: log line as string, filter file path
// Escape the log line for shell safety
escapedLine := strconv.Quote(logLine) escapedLine := strconv.Quote(logLine)
escapedRegex := strconv.Quote(failregex) cmd := fmt.Sprintf("sudo fail2ban-regex %s %s", escapedLine, strconv.Quote(filterPath))
cmd := fmt.Sprintf("echo %s | sudo fail2ban-regex - %s", escapedLine, escapedRegex) out, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", cmd})
out, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd}) // fail2ban-regex returns success (exit 0) if the line matches
if err == nil && strings.Contains(out, "Success") { // Look for "Lines: 1 lines, 0 ignored, 1 matched" or similar success indicators
matches = append(matches, logLine) if err == nil {
// Check if output indicates a match
output := strings.ToLower(out)
if strings.Contains(output, "matched") ||
strings.Contains(output, "success") ||
strings.Contains(output, "1 matched") {
matches = append(matches, logLine)
}
} }
} }
return matches, nil return matches, nil

View File

@@ -17,7 +17,6 @@
package fail2ban package fail2ban
import ( import (
"bufio"
"context" "context"
"fmt" "fmt"
"os" "os"
@@ -89,45 +88,25 @@ func TestFilterLocal(filterName string, logLines []string) ([]string, error) {
if _, err := os.Stat(filterPath); err != nil { if _, err := os.Stat(filterPath); err != nil {
return nil, fmt.Errorf("filter %s not found: %w", filterName, err) return nil, fmt.Errorf("filter %s not found: %w", filterName, err)
} }
// Read the filter config to extract the failregex // Use fail2ban-regex with filter file directly - it handles everything
content, err := os.ReadFile(filterPath) // Format: fail2ban-regex "log line" /etc/fail2ban/filter.d/filter-name.conf
if err != nil {
return nil, fmt.Errorf("failed to read filter config: %w", err)
}
// Extract failregex from the config
var failregex string
scanner := bufio.NewScanner(strings.NewReader(string(content)))
inFailregex := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[Definition]") {
inFailregex = true
continue
}
if inFailregex && strings.HasPrefix(line, "failregex") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
failregex = strings.TrimSpace(parts[1])
}
break
}
if inFailregex && strings.HasPrefix(line, "[") {
break
}
}
if failregex == "" {
return nil, fmt.Errorf("no failregex found in filter %s", filterName)
}
// Use fail2ban-regex to test
var matches []string var matches []string
for _, logLine := range logLines { for _, logLine := range logLines {
logLine = strings.TrimSpace(logLine)
if logLine == "" { if logLine == "" {
continue continue
} }
cmd := exec.Command("fail2ban-regex", logLine, failregex) cmd := exec.Command("fail2ban-regex", logLine, filterPath)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err == nil && strings.Contains(string(out), "Success") { output := strings.ToLower(string(out))
matches = append(matches, logLine) // fail2ban-regex returns success (exit 0) if the line matches
// Look for "matched" or "success" in output
if err == nil {
if strings.Contains(output, "matched") ||
strings.Contains(output, "success") ||
strings.Contains(output, "1 matched") {
matches = append(matches, logLine)
}
} }
} }
return matches, nil return matches, nil

View File

@@ -248,7 +248,7 @@
<!-- Textarea for log lines to test --> <!-- Textarea for log lines to test -->
<div class="mb-4"> <div class="mb-4">
<label for="logLinesTextarea" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.log_lines">Log Lines</label> <label for="logLinesTextarea" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.log_lines">Log Lines</label>
<textarea id="logLinesTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40" disabled <textarea id="logLinesTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40"
data-i18n-placeholder="filter_debug.log_lines_placeholder" placeholder="Enter log lines here..."></textarea> data-i18n-placeholder="filter_debug.log_lines_placeholder" placeholder="Enter log lines here..."></textarea>
</div> </div>

BIN
screenshots/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

After

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 KiB

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 828 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 KiB

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

After

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 592 KiB