Update screenshots and pre-fix the filter-test
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
Before Width: | Height: | Size: 872 KiB After Width: | Height: | Size: 857 KiB |
|
Before Width: | Height: | Size: 970 KiB After Width: | Height: | Size: 870 KiB |
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 828 KiB |
|
Before Width: | Height: | Size: 899 KiB After Width: | Height: | Size: 885 KiB |
|
Before Width: | Height: | Size: 848 KiB After Width: | Height: | Size: 911 KiB |
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 592 KiB |