// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package honeypot import ( "os" "path/filepath" "runtime" "testing" "time" ) func newTestCredStore(t *testing.T) CredStore { t.Helper() s, err := NewFileCredStore(t.TempDir()) if err != nil { t.Fatalf("NewFileCredStore: %v", err) } return s } // TestFileCredStoreAppendListFilter 覆盖追加、时间倒序、IP 过滤。 func TestFileCredStoreAppendListFilter(t *testing.T) { s := newTestCredStore(t) now := time.Now() if err := s.Append(CredRecord{Time: now.Add(-2 * time.Hour), IP: "1.2.3.4", Username: "admin", Password: "a"}); err != nil { t.Fatalf("Append: %v", err) } if err := s.Append(CredRecord{Time: now, IP: "5.6.7.8", Username: "root", Password: "b"}); err != nil { t.Fatalf("Append: %v", err) } recs, err := s.ListSince(now.Add(-3*time.Hour), "", 10) if err != nil { t.Fatalf("ListSince: %v", err) } if len(recs) != 2 { t.Fatalf("记录数 = %d, want 2", len(recs)) } if recs[0].IP != "5.6.7.8" { t.Errorf("首条 IP = %q, want 5.6.7.8(应按时间倒序)", recs[0].IP) } // IP 过滤 only, err := s.ListSince(now.Add(-3*time.Hour), "1.2.3.4", 10) if err != nil { t.Fatalf("ListSince(ip): %v", err) } if len(only) != 1 || only[0].Username != "admin" { t.Fatalf("IP 过滤结果 = %+v, want 仅 1 条 admin", only) } // 时间窗过滤 recent, err := s.ListSince(now.Add(-time.Minute), "", 10) if err != nil { t.Fatalf("ListSince(since): %v", err) } if len(recent) != 1 { t.Fatalf("近 1 分钟记录数 = %d, want 1", len(recent)) } } // TestFileCredStoreSkipsCorruptLines 确保半行/坏行不会让整天数据报废。 func TestFileCredStoreSkipsCorruptLines(t *testing.T) { dir := t.TempDir() s, err := NewFileCredStore(dir) if err != nil { t.Fatalf("NewFileCredStore: %v", err) } now := time.Now() name := filepath.Join(dir, credFileName(now)) content := `{"time":"` + now.Format(time.RFC3339Nano) + `","ip":"1.1.1.1","username":"a"}` + "\n" + `{这行不是合法 JSON` + "\n" + "\n" + `{"time":"` + now.Add(time.Second).Format(time.RFC3339Nano) + `","ip":"2.2.2.2","username":"b"}` + "\n" if err := os.WriteFile(name, []byte(content), 0o600); err != nil { t.Fatal(err) } recs, err := s.ListSince(now.Add(-time.Minute), "", 10) if err != nil { t.Fatalf("ListSince: %v", err) } if len(recs) != 2 { t.Fatalf("记录数 = %d, want 2(坏行与空行应被跳过)", len(recs)) } } // TestFileCredStoreTopCredentials 覆盖凭据排行榜的计数与排序。 func TestFileCredStoreTopCredentials(t *testing.T) { s := newTestCredStore(t) now := time.Now() for i := 0; i < 3; i++ { if err := s.Append(CredRecord{Time: now, IP: "1.1.1.1", Username: "admin", Password: "a"}); err != nil { t.Fatal(err) } } if err := s.Append(CredRecord{Time: now, IP: "2.2.2.2", Username: "root", Password: "b"}); err != nil { t.Fatal(err) } stats, err := s.TopCredentials(now.Add(-time.Hour), 10) if err != nil { t.Fatalf("TopCredentials: %v", err) } if len(stats) != 2 { t.Fatalf("组合数 = %d, want 2", len(stats)) } if stats[0].Username != "admin" || stats[0].Password != "a" || stats[0].Count != 3 { t.Errorf("首位 = %+v, want admin/a ×3", stats[0]) } } // TestFileCredStorePurgeBefore 覆盖按天删文件,且不碰边界当天。 func TestFileCredStorePurgeBefore(t *testing.T) { s := newTestCredStore(t) now := time.Now() for _, d := range []int{-10, -5, 0} { if err := s.Append(CredRecord{Time: now.AddDate(0, 0, d), IP: "1.1.1.1"}); err != nil { t.Fatal(err) } } files, _, err := s.Size() if err != nil { t.Fatal(err) } if files != 3 { t.Fatalf("文件数 = %d, want 3(按天分文件)", files) } removed, err := s.PurgeBefore(now.AddDate(0, 0, -7)) if err != nil { t.Fatalf("PurgeBefore: %v", err) } if removed != 1 { t.Errorf("删除文件数 = %d, want 1(只应删 10 天前那份)", removed) } files, _, err = s.Size() if err != nil { t.Fatal(err) } if files != 2 { t.Errorf("剩余文件数 = %d, want 2", files) } // 剩下的记录仍可读 recs, err := s.ListSince(now.AddDate(0, 0, -20), "", 10) if err != nil { t.Fatal(err) } if len(recs) != 2 { t.Errorf("剩余记录数 = %d, want 2", len(recs)) } } // TestFileCredStorePermissions 验证目录 0700 / 文件 0600。 // // 记录里是明文账号密码,同机其他用户不该能读。Windows 上 os.Chmod 语义不同, // 跳过该用例(部署环境是 Linux)。 func TestFileCredStorePermissions(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Windows 不支持 POSIX 权限位") } dir := filepath.Join(t.TempDir(), "creds") s, err := NewFileCredStore(dir) if err != nil { t.Fatal(err) } if err := s.Append(CredRecord{Time: time.Now(), IP: "1.1.1.1", Username: "a"}); err != nil { t.Fatal(err) } di, err := os.Stat(dir) if err != nil { t.Fatal(err) } if perm := di.Mode().Perm(); perm != 0o700 { t.Errorf("目录权限 = %#o, want 0700", perm) } files, err := filepath.Glob(filepath.Join(dir, credFilePrefix+"*"+credFileSuffix)) if err != nil || len(files) != 1 { t.Fatalf("凭据文件数 = %d, want 1(%v)", len(files), err) } fi, err := os.Stat(files[0]) if err != nil { t.Fatal(err) } if perm := fi.Mode().Perm(); perm != 0o600 { t.Errorf("文件权限 = %#o, want 0600", perm) } } // TestFileCredStoreEmptyDir 验证空目录配置会被明确拒绝。 func TestFileCredStoreEmptyDir(t *testing.T) { if _, err := NewFileCredStore(" "); err == nil { t.Fatal("空目录应返回错误") } }