diff --git a/internal/fail2ban/jail_management.go b/internal/fail2ban/jail_management.go index c6bac74..095b6a8 100644 --- a/internal/fail2ban/jail_management.go +++ b/internal/fail2ban/jail_management.go @@ -1088,19 +1088,80 @@ func TestLogpathWithResolution(logpath string) (originalPath, resolvedPath strin return originalPath, resolvedPath, files, nil } -// ExtractLogpathFromJailConfig extracts the logpath value from jail configuration content. +// ExtractLogpathFromJailConfig extracts the logpath value(s) from jail configuration content. +// Supports multiple logpaths in a single line (space-separated) or multiple lines. +// Fail2ban supports both formats: +// +// logpath = /var/log/file1.log /var/log/file2.log +// logpath = /var/log/file1.log +// /var/log/file2.log +// +// Returns all logpaths joined by newlines. func ExtractLogpathFromJailConfig(jailContent string) string { + var logpaths []string scanner := bufio.NewScanner(strings.NewReader(jailContent)) + inLogpathLine := false + currentLogpath := "" + for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) + // Skip comments + if strings.HasPrefix(line, "#") { + if inLogpathLine && currentLogpath != "" { + // End of logpath block at comment, process current logpath + paths := strings.Fields(currentLogpath) + logpaths = append(logpaths, paths...) + currentLogpath = "" + inLogpathLine = false + } + continue + } + + // Check if this line starts with logpath = if strings.HasPrefix(strings.ToLower(line), "logpath") { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { - return strings.TrimSpace(parts[1]) + logpathValue := strings.TrimSpace(parts[1]) + if logpathValue != "" { + currentLogpath = logpathValue + inLogpathLine = true + } } + } else if inLogpathLine { + // Continuation line (indented or starting with space) + // Fail2ban allows continuation lines for logpath + if line != "" && !strings.Contains(line, "=") { + // This is a continuation line, append to current logpath + currentLogpath += " " + line + } else { + // End of logpath block, process current logpath + if currentLogpath != "" { + // Split by spaces to handle multiple logpaths in one line + paths := strings.Fields(currentLogpath) + logpaths = append(logpaths, paths...) + currentLogpath = "" + } + inLogpathLine = false + } + } else if inLogpathLine && line == "" { + // Empty line might end the logpath block + if currentLogpath != "" { + paths := strings.Fields(currentLogpath) + logpaths = append(logpaths, paths...) + currentLogpath = "" + } + inLogpathLine = false } } - return "" + + // Process any remaining logpath + if currentLogpath != "" { + paths := strings.Fields(currentLogpath) + logpaths = append(logpaths, paths...) + } + + // Join multiple logpaths with newlines + return strings.Join(logpaths, "\n") } // ExtractFilterFromJailConfig extracts the filter name from jail configuration content. @@ -1183,7 +1244,6 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error { "bantime": fmt.Sprintf("bantime = %s", settings.Bantime), "findtime": fmt.Sprintf("findtime = %s", settings.Findtime), "maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry), - "destemail": fmt.Sprintf("destemail = %s", settings.Destemail), "banaction": fmt.Sprintf("banaction = %s", banaction), "banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllports), } @@ -1197,7 +1257,7 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error { var newLines []string newLines = append(newLines, strings.Split(strings.TrimRight(config.JailLocalBanner(), "\n"), "\n")...) newLines = append(newLines, "[DEFAULT]") - for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail", "banaction", "banaction_allports"} { + for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} { newLines = append(newLines, keysToUpdate[key]) } newLines = append(newLines, "") @@ -1270,14 +1330,14 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error { // If DEFAULT section wasn't found, create it at the beginning if !defaultSectionFound { defaultLines := []string{"[DEFAULT]"} - for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"} { + for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} { defaultLines = append(defaultLines, keysToUpdate[key]) } defaultLines = append(defaultLines, "") outputLines = append(defaultLines, outputLines...) } else { // Add any missing keys to the DEFAULT section - for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail", "banaction", "banaction_allports"} { + for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} { if !keysUpdated[key] { // Find the DEFAULT section and insert after it for i, line := range outputLines { diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 66538d6..0d6e781 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -678,8 +678,13 @@ func ListSSHKeysHandler(c *gin.Context) { continue } name := entry.Name() - if strings.HasPrefix(name, "id_") || strings.HasSuffix(name, ".pem") || strings.HasSuffix(name, ".key") { - keys = append(keys, filepath.Join(dir, name)) + // Only include private keys, not public keys (.pub files) + // SSH requires the private key file, not the public key + if (strings.HasPrefix(name, "id_") && !strings.HasSuffix(name, ".pub")) || + strings.HasSuffix(name, ".pem") || + (strings.HasSuffix(name, ".key") && !strings.HasSuffix(name, ".pub")) { + keyPath := filepath.Join(dir, name) + keys = append(keys, keyPath) } } if len(keys) == 0 { @@ -1394,17 +1399,93 @@ func TestLogpathHandler(c *gin.Context) { return } - // Test the logpath with variable resolution - originalPath, resolvedPath, files, err := conn.TestLogpathWithResolution(c.Request.Context(), originalLogpath) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test logpath: " + err.Error()}) - return + // Get server type to determine test strategy + server := conn.Server() + isLocalServer := server.Type == "local" + + // Split logpath by newlines and spaces (Fail2ban supports multiple logpaths separated by spaces or newlines) + // First split by newlines, then split each line by spaces + var logpaths []string + for _, line := range strings.Split(originalLogpath, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Split by spaces to handle multiple logpaths in one line + paths := strings.Fields(line) + logpaths = append(logpaths, paths...) + } + + var allResults []map[string]interface{} + + for _, logpathLine := range logpaths { + logpathLine = strings.TrimSpace(logpathLine) + if logpathLine == "" { + continue + } + + if isLocalServer { + // For local servers: only test in fail2ban-ui container (container can only see mounted paths) + // Resolve variables first + resolvedPath, err := fail2ban.ResolveLogpathVariables(logpathLine) + if err != nil { + allResults = append(allResults, map[string]interface{}{ + "logpath": logpathLine, + "resolved_path": "", + "found": false, + "files": []string{}, + "error": err.Error(), + }) + continue + } + + if resolvedPath == "" { + resolvedPath = logpathLine + } + + // Test in fail2ban-ui container + files, localErr := fail2ban.TestLogpath(resolvedPath) + + allResults = append(allResults, map[string]interface{}{ + "logpath": logpathLine, + "resolved_path": resolvedPath, + "found": len(files) > 0, + "files": files, + "error": func() string { + if localErr != nil { + return localErr.Error() + } + return "" + }(), + }) + } else { + // For SSH/Agent servers: test on remote server (via connector) + _, resolvedPath, filesOnRemote, err := conn.TestLogpathWithResolution(c.Request.Context(), logpathLine) + if err != nil { + allResults = append(allResults, map[string]interface{}{ + "logpath": logpathLine, + "resolved_path": resolvedPath, + "found": false, + "files": []string{}, + "error": err.Error(), + }) + continue + } + + allResults = append(allResults, map[string]interface{}{ + "logpath": logpathLine, + "resolved_path": resolvedPath, + "found": len(filesOnRemote) > 0, + "files": filesOnRemote, + "error": "", + }) + } } c.JSON(http.StatusOK, gin.H{ - "original_logpath": originalPath, - "resolved_logpath": resolvedPath, - "files": files, + "original_logpath": originalLogpath, + "is_local_server": isLocalServer, + "results": allResults, }) } @@ -1916,7 +1997,6 @@ func UpdateSettingsHandler(c *gin.Context) { oldSettings.Bantime != newSettings.Bantime || oldSettings.Findtime != newSettings.Findtime || oldSettings.Maxretry != newSettings.Maxretry || - oldSettings.Destemail != newSettings.Destemail || oldSettings.Banaction != newSettings.Banaction || oldSettings.BanactionAllports != newSettings.BanactionAllports @@ -2142,8 +2222,8 @@ func ApplyFail2banSettings(jailLocalPath string) error { fmt.Sprintf("bantime = %s", s.Bantime), fmt.Sprintf("findtime = %s", s.Findtime), fmt.Sprintf("maxretry = %d", s.Maxretry), - fmt.Sprintf("destemail = %s", s.Destemail), - //fmt.Sprintf("sender = %s", s.Sender), + fmt.Sprintf("banaction = %s", s.Banaction), + fmt.Sprintf("banaction_allports = %s", s.BanactionAllports), "", } content := strings.Join(newLines, "\n")