chore: add golangci-lint config and fix all lint issues

Enable 15 additional linters (gosec, errorlint, gocritic, modernize,
misspell, bodyclose, sqlclosecheck, nilerr, unconvert, durationcheck,
sloglint, wastedassign, usestdlibvars) with sensible exclusion rules.

Fix all findings: errors.Is for error comparisons, run() pattern in
main to avoid exitAfterDefer, ReadHeaderTimeout for Slowloris
protection, bounds check in escape sequence reader, WaitGroup.Go,
slices.Contains, range-over-int loops, and http.MethodGet constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 21:43:49 +01:00
parent 0ad6f4cb6a
commit d4380c0aea
10 changed files with 134 additions and 62 deletions

View File

@@ -36,7 +36,7 @@ func TestStaticCredentialsWrongPassword(t *testing.T) {
func TestRejectionBeforeThreshold(t *testing.T) {
a := newTestAuth(3, time.Hour)
for i := 0; i < 2; i++ {
for i := range 2 {
d := a.Authenticate("1.2.3.4", "user", "pass")
if d.Accepted {
t.Fatalf("attempt %d should be rejected", i+1)
@@ -49,7 +49,7 @@ func TestRejectionBeforeThreshold(t *testing.T) {
func TestThresholdAcceptance(t *testing.T) {
a := newTestAuth(3, time.Hour)
for i := 0; i < 2; i++ {
for i := range 2 {
d := a.Authenticate("1.2.3.4", "user", "pass")
if d.Accepted {
t.Fatalf("attempt %d should be rejected", i+1)
@@ -65,7 +65,7 @@ func TestPerIPIsolation(t *testing.T) {
a := newTestAuth(3, time.Hour)
// IP1 gets 2 failures.
for i := 0; i < 2; i++ {
for range 2 {
a.Authenticate("1.1.1.1", "user", "pass")
}
@@ -157,12 +157,10 @@ func TestConcurrentAccess(t *testing.T) {
a := newTestAuth(5, time.Hour)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 100 {
wg.Go(func() {
a.Authenticate("1.2.3.4", "user", "pass")
}()
})
}
wg.Wait()
}

View File

@@ -87,7 +87,7 @@ func TestScorer_OutputIgnored(t *testing.T) {
now := time.Now()
// Only output events — should not affect score.
for i := 0; i < 100; i++ {
for range 100 {
s.RecordEvent(now, DirOutput, []byte("some output\n"))
now = now.Add(10 * time.Millisecond)
}
@@ -103,25 +103,21 @@ func TestScorer_ThreadSafety(t *testing.T) {
now := time.Now()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(offset int) {
defer wg.Done()
for j := 0; j < 100; j++ {
ts := now.Add(time.Duration(offset*100+j) * time.Millisecond)
for i := range 10 {
wg.Go(func() {
for j := range 100 {
ts := now.Add(time.Duration(i*100+j) * time.Millisecond)
s.RecordEvent(ts, DirInput, []byte("a"))
}
}(i)
})
}
// Concurrently read score.
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
wg.Go(func() {
for range 50 {
_ = s.Score()
}
}()
})
wg.Wait()

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"log/slog"
"net/http"
"slices"
"sync"
"time"
@@ -98,12 +99,7 @@ func (n *Notifier) shouldSend(wh config.WebhookNotifyConfig, eventType string) b
if len(wh.Events) == 0 {
return true // empty = all events
}
for _, ev := range wh.Events {
if ev == eventType {
return true
}
}
return false
return slices.Contains(wh.Events, eventType)
}
func (n *Notifier) send(ctx context.Context, wh config.WebhookNotifyConfig, payload webhookPayload) {

View File

@@ -2,6 +2,7 @@ package bash
import (
"context"
"errors"
"fmt"
"io"
"strings"
@@ -55,7 +56,7 @@ func (b *BashShell) Handle(ctx context.Context, sess *shell.SessionContext, rw i
}
line, err := readLine(ctx, rw)
if err == io.EOF {
if errors.Is(err, io.EOF) {
fmt.Fprint(rw, "logout\r\n")
return nil
}
@@ -81,7 +82,9 @@ func (b *BashShell) Handle(ctx context.Context, sess *shell.SessionContext, rw i
// Log command and output to store.
if sess.Store != nil {
sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output)
if err := sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output); err != nil {
return fmt.Errorf("append session log: %w", err)
}
}
if result.exit {
@@ -145,8 +148,7 @@ func readLine(ctx context.Context, rw io.ReadWriter) (string, error) {
// Read and discard the rest of the escape sequence.
// Most are 3 bytes: ESC [ X (arrow keys, etc.)
next := make([]byte, 1)
rw.Read(next)
if next[0] == '[' {
if n, _ := rw.Read(next); n > 0 && next[0] == '[' {
rw.Read(next) // read the final byte
}

View File

@@ -3,6 +3,7 @@ package bash
import (
"bytes"
"context"
"errors"
"io"
"strings"
"testing"
@@ -108,7 +109,7 @@ func TestReadLineCtrlD(t *testing.T) {
ctx := context.Background()
_, err := readLine(ctx, rw)
if err != io.EOF {
if !errors.Is(err, io.EOF) {
t.Fatalf("expected io.EOF, got %v", err)
}
}

View File

@@ -205,11 +205,7 @@ func TestDeleteRecordsBefore(t *testing.T) {
}
func TestNewSQLiteStoreCreatesFile(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "subdir", "test.db")
// Parent directory doesn't exist yet; SQLite should create it.
// Actually, SQLite doesn't create parent dirs, but the file itself.
// Use a path in the temp dir directly.
dbPath = filepath.Join(t.TempDir(), "test.db")
dbPath := filepath.Join(t.TempDir(), "test.db")
store, err := NewSQLiteStore(dbPath)
if err != nil {
t.Fatalf("creating store: %v", err)

View File

@@ -37,17 +37,17 @@ func seedData(t *testing.T, store Store) {
ctx := context.Background()
// Login attempts: root/toor from two IPs, admin/admin from one IP.
for i := 0; i < 5; i++ {
for range 5 {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1"); err != nil {
t.Fatalf("seeding attempt: %v", err)
}
}
for i := 0; i < 3; i++ {
for range 3 {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.2"); err != nil {
t.Fatalf("seeding attempt: %v", err)
}
}
for i := 0; i < 2; i++ {
for range 2 {
if err := store.RecordLoginAttempt(ctx, "admin", "admin", "10.0.0.1"); err != nil {
t.Fatalf("seeding attempt: %v", err)
}

View File

@@ -27,7 +27,7 @@ func newSeededTestServer(t *testing.T) *Server {
store := storage.NewMemoryStore()
ctx := context.Background()
for i := 0; i < 5; i++ {
for range 5 {
if err := store.RecordLoginAttempt(ctx, "root", "toor", "10.0.0.1"); err != nil {
t.Fatalf("seeding attempt: %v", err)
}
@@ -54,7 +54,7 @@ func newSeededTestServer(t *testing.T) *Server {
func TestDashboardHandler(t *testing.T) {
t.Run("empty store", func(t *testing.T) {
srv := newTestServer(t)
req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
@@ -73,7 +73,7 @@ func TestDashboardHandler(t *testing.T) {
t.Run("with data", func(t *testing.T) {
srv := newSeededTestServer(t)
req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
@@ -93,7 +93,7 @@ func TestDashboardHandler(t *testing.T) {
func TestFragmentStats(t *testing.T) {
srv := newSeededTestServer(t)
req := httptest.NewRequest("GET", "/fragments/stats", nil)
req := httptest.NewRequest(http.MethodGet, "/fragments/stats", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
@@ -113,7 +113,7 @@ func TestFragmentStats(t *testing.T) {
func TestFragmentActiveSessions(t *testing.T) {
srv := newSeededTestServer(t)
req := httptest.NewRequest("GET", "/fragments/active-sessions", nil)
req := httptest.NewRequest(http.MethodGet, "/fragments/active-sessions", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
@@ -144,7 +144,7 @@ func TestStaticAssets(t *testing.T) {
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
req := httptest.NewRequest("GET", tt.path, nil)
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)