package auth import ( "bytes" "fmt" "os" "os/exec" "strconv" "strings" ) type FaillockStatus struct { Enabled bool Failures int MaxTries int Remaining int LockedOut bool } func GetFaillockStatus(user string) (FaillockStatus, error) { s := FaillockStatus{} data, err := os.ReadFile("/etc/security/faillock.conf") if err != nil { return s, nil } s.Enabled = true s.MaxTries = 3 for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "deny") { parts := strings.Split(line, "=") if len(parts) == 2 { if n, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { s.MaxTries = n } } } } var out bytes.Buffer cmd := exec.Command("faillock", "--user", user) cmd.Stdout = &out cmd.Stderr = &out if err := cmd.Run(); err != nil { return s, fmt.Errorf("faillock: %w", err) } s.Failures = countFaillockLines(out.String()) s.Remaining = s.MaxTries - s.Failures if s.Remaining < 0 { s.Remaining = 0 } s.LockedOut = s.Remaining <= 0 return s, nil } func ResetFaillock(user string) error { return exec.Command("faillock", "--user", user, "--reset").Run() } func countFaillockLines(out string) int { n := 0 for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "When") || strings.HasPrefix(line, "Source") || strings.HasPrefix(line, "Date") { continue } if strings.Contains(line, ":") { continue } n++ } return n }