- Implement tests for invalid JSON input in notification template creation, update, and preview endpoints. - Enhance uptime handler tests to cover sync success and error scenarios for delete and list operations. - Update routes to include backup service in certificate handler initialization. - Introduce certificate usage check before deletion in the certificate service, preventing deletion of certificates in use. - Update certificate service tests to validate new behavior regarding certificate deletion. - Add new tests for security service to verify break glass token generation and validation. - Enhance frontend certificate list component to prevent deletion of certificates in use and ensure proper backup creation. - Create unit tests for the CertificateList component to validate deletion logic and error handling.
295 lines
8.0 KiB
Go
295 lines
8.0 KiB
Go
package services
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/stretchr/testify/assert"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupSecurityTestDB(t *testing.T) *gorm.DB {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
assert.NoError(t, err)
|
|
|
|
err = db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.SecurityRuleSet{})
|
|
assert.NoError(t, err)
|
|
|
|
return db
|
|
}
|
|
|
|
func TestSecurityService_Upsert_ValidateAdminWhitelist(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Invalid CIDR in admin whitelist should fail
|
|
cfg := &models.SecurityConfig{Name: "default", Enabled: true, AdminWhitelist: "invalid-cidr"}
|
|
err := svc.Upsert(cfg)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, ErrInvalidAdminCIDR, err)
|
|
|
|
// Valid CIDR should succeed
|
|
cfg.AdminWhitelist = "192.168.1.0/24, 10.0.0.1"
|
|
err = svc.Upsert(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify stored
|
|
got, err := svc.Get()
|
|
assert.NoError(t, err)
|
|
assert.True(t, strings.Contains(got.AdminWhitelist, "192.168.1.0/24"))
|
|
}
|
|
|
|
func TestSecurityService_BreakGlassTokenLifecycle(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Create record
|
|
cfg := &models.SecurityConfig{Name: "default", Enabled: false}
|
|
err := svc.Upsert(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
token, err := svc.GenerateBreakGlassToken("default")
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, token)
|
|
|
|
// Verify valid token returns true
|
|
ok, err := svc.VerifyBreakGlassToken("default", token)
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
// Invalid token fails
|
|
ok, err = svc.VerifyBreakGlassToken("default", "wrongtoken")
|
|
assert.Error(t, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestSecurityService_LogDecisionAndList(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
dec := &models.SecurityDecision{Source: "manual", Action: "block", IP: "1.2.3.4", Host: "example.com", RuleID: "manual-1", Details: "test manual block"}
|
|
err := svc.LogDecision(dec)
|
|
assert.NoError(t, err)
|
|
|
|
list, err := svc.ListDecisions(10)
|
|
assert.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(list), 1)
|
|
assert.Equal(t, "manual", list[0].Source)
|
|
}
|
|
|
|
func TestSecurityService_UpsertRuleSet(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
rs := &models.SecurityRuleSet{Name: "owasp-crs", SourceURL: "https://example.com/owasp.rules", Mode: "owasp", Content: "rule: 1"}
|
|
err := svc.UpsertRuleSet(rs)
|
|
assert.NoError(t, err)
|
|
|
|
list, err := svc.ListRuleSets()
|
|
assert.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(list), 1)
|
|
assert.Equal(t, "owasp-crs", list[0].Name)
|
|
}
|
|
|
|
func TestSecurityService_UpsertRuleSet_ContentTooLarge(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Create a string slightly larger than 2MB
|
|
large := strings.Repeat("x", 2*1024*1024+1)
|
|
rs := &models.SecurityRuleSet{Name: "big-crs", Content: large}
|
|
err := svc.UpsertRuleSet(rs)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestSecurityService_DeleteRuleSet(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
rs := &models.SecurityRuleSet{Name: "owasp-crs", Content: "rule: 1"}
|
|
err := svc.UpsertRuleSet(rs)
|
|
assert.NoError(t, err)
|
|
|
|
// Get list and pick ID
|
|
list, err := svc.ListRuleSets()
|
|
assert.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(list), 1)
|
|
|
|
id := list[0].ID
|
|
// Delete
|
|
err = svc.DeleteRuleSet(id)
|
|
assert.NoError(t, err)
|
|
// Ensure no rulesets left
|
|
list, err = svc.ListRuleSets()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, len(list))
|
|
}
|
|
|
|
func TestSecurityService_Upsert_RejectExternalMode(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// External mode should be rejected by validation
|
|
cfg := &models.SecurityConfig{Name: "default", Enabled: true, CrowdSecMode: "external"}
|
|
err := svc.Upsert(cfg)
|
|
assert.Error(t, err)
|
|
|
|
// Unknown mode should also be rejected
|
|
cfg.CrowdSecMode = "unknown"
|
|
err = svc.Upsert(cfg)
|
|
assert.Error(t, err)
|
|
|
|
// Local mode should be accepted
|
|
cfg.CrowdSecMode = "local"
|
|
err = svc.Upsert(cfg)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestSecurityService_GenerateBreakGlassToken_NewConfig(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Generate token for non-existent config (should create it)
|
|
token, err := svc.GenerateBreakGlassToken("newconfig")
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, token)
|
|
assert.Greater(t, len(token), 20) // Should be hex-encoded 24 bytes = 48 chars
|
|
|
|
// Verify the token works
|
|
ok, err := svc.VerifyBreakGlassToken("newconfig", token)
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
|
|
// Verify config was created with hash
|
|
var cfg models.SecurityConfig
|
|
err = db.Where("name = ?", "newconfig").First(&cfg).Error
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, cfg.BreakGlassHash)
|
|
}
|
|
|
|
func TestSecurityService_GenerateBreakGlassToken_UpdateExisting(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Create initial config
|
|
cfg := &models.SecurityConfig{Name: "default", Enabled: true}
|
|
err := svc.Upsert(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Generate first token
|
|
token1, err := svc.GenerateBreakGlassToken("default")
|
|
assert.NoError(t, err)
|
|
|
|
// Generate second token (should replace first)
|
|
token2, err := svc.GenerateBreakGlassToken("default")
|
|
assert.NoError(t, err)
|
|
assert.NotEqual(t, token1, token2)
|
|
|
|
// First token should no longer work
|
|
ok, err := svc.VerifyBreakGlassToken("default", token1)
|
|
assert.Error(t, err)
|
|
assert.False(t, ok)
|
|
|
|
// Second token should work
|
|
ok, err = svc.VerifyBreakGlassToken("default", token2)
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestSecurityService_VerifyBreakGlassToken_NoConfig(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Verify against non-existent config
|
|
ok, err := svc.VerifyBreakGlassToken("nonexistent", "anytoken")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, ErrSecurityConfigNotFound, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestSecurityService_VerifyBreakGlassToken_NoHash(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Create config without break-glass hash
|
|
cfg := &models.SecurityConfig{Name: "default", Enabled: true, BreakGlassHash: ""}
|
|
err := svc.Upsert(cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify should fail with no hash
|
|
ok, err := svc.VerifyBreakGlassToken("default", "anytoken")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, ErrBreakGlassInvalid, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestSecurityService_VerifyBreakGlassToken_WrongToken(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Generate valid token
|
|
token, err := svc.GenerateBreakGlassToken("default")
|
|
assert.NoError(t, err)
|
|
|
|
// Try various wrong tokens
|
|
testCases := []string{
|
|
"",
|
|
"wrongtoken",
|
|
"x" + token,
|
|
token[:len(token)-1],
|
|
strings.ToUpper(token),
|
|
}
|
|
|
|
for _, wrongToken := range testCases {
|
|
ok, err := svc.VerifyBreakGlassToken("default", wrongToken)
|
|
assert.Error(t, err, "Token should fail: %s", wrongToken)
|
|
assert.Equal(t, ErrBreakGlassInvalid, err)
|
|
assert.False(t, ok)
|
|
}
|
|
}
|
|
|
|
func TestSecurityService_Get_NotFound(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Get from empty database
|
|
cfg, err := svc.Get()
|
|
assert.Error(t, err)
|
|
assert.Equal(t, ErrSecurityConfigNotFound, err)
|
|
assert.Nil(t, cfg)
|
|
}
|
|
|
|
func TestSecurityService_Upsert_PreserveBreakGlassHash(t *testing.T) {
|
|
db := setupSecurityTestDB(t)
|
|
svc := NewSecurityService(db)
|
|
|
|
// Generate token
|
|
token, err := svc.GenerateBreakGlassToken("default")
|
|
assert.NoError(t, err)
|
|
|
|
// Get the hash
|
|
var cfg models.SecurityConfig
|
|
err = db.Where("name = ?", "default").First(&cfg).Error
|
|
assert.NoError(t, err)
|
|
originalHash := cfg.BreakGlassHash
|
|
|
|
// Update other fields
|
|
cfg.Enabled = true
|
|
cfg.AdminWhitelist = "10.0.0.0/8"
|
|
err = svc.Upsert(&cfg)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify hash is preserved
|
|
var updated models.SecurityConfig
|
|
err = db.Where("name = ?", "default").First(&updated).Error
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, originalHash, updated.BreakGlassHash)
|
|
|
|
// Original token should still work
|
|
ok, err := svc.VerifyBreakGlassToken("default", token)
|
|
assert.NoError(t, err)
|
|
assert.True(t, ok)
|
|
}
|