fix: Add unit tests for emergency bypass and backup service validation
This commit is contained in:
@@ -381,6 +381,46 @@ func TestEmergencySecurityReset_ClearsBlockDecisions(t *testing.T) {
|
||||
assert.Equal(t, "allow", remaining[0].Action)
|
||||
}
|
||||
|
||||
func TestEmergencySecurityReset_MiddlewarePrevalidatedBypass(t *testing.T) {
|
||||
db := setupEmergencyTestDB(t)
|
||||
handler := NewEmergencyHandler(db)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
|
||||
c.Set("emergency_bypass", true)
|
||||
handler.SecurityReset(c)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestEmergencySecurityReset_MiddlewareBypass_ResetFailure(t *testing.T) {
|
||||
db := setupEmergencyTestDB(t)
|
||||
handler := NewEmergencyHandler(db)
|
||||
|
||||
stdDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, stdDB.Close())
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
|
||||
c.Set("emergency_bypass", true)
|
||||
handler.SecurityReset(c)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestLogEnhancedAudit(t *testing.T) {
|
||||
// Setup
|
||||
db := setupEmergencyTestDB(t)
|
||||
|
||||
@@ -33,6 +33,30 @@ func TestEmergencyBypass_NoToken(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestEmergencyBypass_InvalidClientIP(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
|
||||
|
||||
router := gin.New()
|
||||
managementCIDRs := []string{"127.0.0.0/8"}
|
||||
router.Use(EmergencyBypass(managementCIDRs, nil))
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
_, exists := c.Get("emergency_bypass")
|
||||
assert.False(t, exists, "Emergency bypass flag should not be set for invalid client IP")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "ok"})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set(EmergencyTokenHeader, "test-token-that-meets-minimum-length-requirement-32-chars")
|
||||
req.RemoteAddr = "invalid-remote-addr"
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestEmergencyBypass_ValidToken(t *testing.T) {
|
||||
// Test that valid token from allowed IP sets bypass flag
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
@@ -244,3 +244,22 @@ func TestMiddleware_ACLDisabledDoesNotBlock(t *testing.T) {
|
||||
// Disabled ACL should not block
|
||||
require.False(t, ctx.IsAborted())
|
||||
}
|
||||
|
||||
func TestMiddleware_EmergencyBypassSkipsChecks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := setupDB(t)
|
||||
c := cerberus.New(config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled"}, db)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/secure", nil)
|
||||
req.RemoteAddr = "203.0.113.10:1234"
|
||||
ctx.Request = req
|
||||
ctx.Set("emergency_bypass", true)
|
||||
|
||||
mw := c.Middleware()
|
||||
mw(ctx)
|
||||
|
||||
require.False(t, ctx.IsAborted(), "middleware should short-circuit when emergency_bypass=true")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package crowdsec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -168,6 +171,22 @@ func TestHubCacheLoadInvalidSlug(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHubCacheLoadMetadataReadError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseDir := t.TempDir()
|
||||
cache, err := NewHubCache(baseDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
slugDir := filepath.Join(baseDir, "crowdsecurity", "demo")
|
||||
require.NoError(t, os.MkdirAll(slugDir, 0o750))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(slugDir, "metadata.json"), 0o750))
|
||||
|
||||
_, err = cache.Load(context.Background(), "crowdsecurity/demo")
|
||||
require.Error(t, err)
|
||||
require.False(t, errors.Is(err, ErrCacheMiss))
|
||||
}
|
||||
|
||||
func TestHubCacheExistsContextCanceled(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache, err := NewHubCache(t.TempDir(), time.Hour)
|
||||
|
||||
@@ -1713,6 +1713,41 @@ func TestHubHTTPErrorCanFallback(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestHubServiceFetchWithFallbackStopsOnNonFallbackError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewHubService(nil, nil, t.TempDir())
|
||||
attempts := 0
|
||||
svc.HTTPClient = &http.Client{Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
attempts++
|
||||
return newResponse(http.StatusBadRequest, "bad request"), nil
|
||||
})}
|
||||
|
||||
_, _, err := svc.fetchWithFallback(context.Background(), []string{"https://hub.crowdsec.net/a", "https://raw.githubusercontent.com/crowdsecurity/hub/master/b"})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, attempts)
|
||||
}
|
||||
|
||||
func TestHubServiceFetchWithFallbackRetriesWhenErrorCanFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewHubService(nil, nil, t.TempDir())
|
||||
attempts := 0
|
||||
svc.HTTPClient = &http.Client{Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
attempts++
|
||||
if attempts == 1 {
|
||||
return newResponse(http.StatusServiceUnavailable, "unavailable"), nil
|
||||
}
|
||||
return newResponse(http.StatusOK, "ok"), nil
|
||||
})}
|
||||
|
||||
data, used, err := svc.fetchWithFallback(context.Background(), []string{"https://hub.crowdsec.net/a", "https://raw.githubusercontent.com/crowdsecurity/hub/master/b"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ok", string(data))
|
||||
require.Equal(t, "https://raw.githubusercontent.com/crowdsecurity/hub/master/b", used)
|
||||
require.Equal(t, 2, attempts)
|
||||
}
|
||||
|
||||
// TestValidateHubURL_EdgeCases tests additional edge cases for SSRF protection
|
||||
func TestValidateHubURL_EdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -90,3 +91,49 @@ func TestBackupService_ExtractDatabaseFromBackup_ExtractWalFailure(t *testing.T)
|
||||
_, err = svc.extractDatabaseFromBackup(zipPath)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBackupService_UnzipWithSkip_RejectsPathTraversal(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
destDir := filepath.Join(tmp, "data")
|
||||
require.NoError(t, os.MkdirAll(destDir, 0o700))
|
||||
|
||||
zipPath := filepath.Join(tmp, "path-traversal.zip")
|
||||
zipFile := openZipInTempDir(t, tmp, zipPath)
|
||||
writer := zip.NewWriter(zipFile)
|
||||
|
||||
entry, err := writer.Create("../escape.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = entry.Write([]byte("evil"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, writer.Close())
|
||||
require.NoError(t, zipFile.Close())
|
||||
|
||||
svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"}
|
||||
err = svc.unzipWithSkip(zipPath, destDir, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid file path in archive")
|
||||
}
|
||||
|
||||
func TestBackupService_UnzipWithSkip_RejectsExcessiveUncompressedSize(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
destDir := filepath.Join(tmp, "data")
|
||||
require.NoError(t, os.MkdirAll(destDir, 0o700))
|
||||
|
||||
zipPath := filepath.Join(tmp, "oversized.zip")
|
||||
zipFile := openZipInTempDir(t, tmp, zipPath)
|
||||
writer := zip.NewWriter(zipFile)
|
||||
|
||||
entry, err := writer.Create("huge.bin")
|
||||
require.NoError(t, err)
|
||||
_, err = entry.Write(bytes.Repeat([]byte("a"), 101*1024*1024))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, writer.Close())
|
||||
require.NoError(t, zipFile.Close())
|
||||
|
||||
svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"}
|
||||
err = svc.unzipWithSkip(zipPath, destDir, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeded decompression limit")
|
||||
}
|
||||
|
||||
@@ -190,6 +190,27 @@ func TestCheckMonitor_TCPFailure(t *testing.T) {
|
||||
require.NotEmpty(t, hb.Message)
|
||||
}
|
||||
|
||||
func TestCreateMonitor_AppliesDefaultIntervalAndRetries(t *testing.T) {
|
||||
db := setupUnitTestDB(t)
|
||||
svc := NewUptimeService(db, nil)
|
||||
|
||||
monitor, err := svc.CreateMonitor("defaults", "http://example.com", "http", 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 60, monitor.Interval)
|
||||
require.Equal(t, 3, monitor.MaxRetries)
|
||||
require.Equal(t, "pending", monitor.Status)
|
||||
require.True(t, monitor.Enabled)
|
||||
}
|
||||
|
||||
func TestCreateMonitor_TCPRequiresHostPort(t *testing.T) {
|
||||
db := setupUnitTestDB(t)
|
||||
svc := NewUptimeService(db, nil)
|
||||
|
||||
_, err := svc.CreateMonitor("bad-tcp", "example.com", "tcp", 60, 2)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "TCP URL must be in host:port format")
|
||||
}
|
||||
|
||||
// TestCheckMonitor_UnknownType tests unknown monitor type
|
||||
func TestCheckMonitor_UnknownType(t *testing.T) {
|
||||
db := setupUnitTestDB(t)
|
||||
|
||||
@@ -647,3 +647,91 @@ After user approval of this plan:
|
||||
2. Execute PR-2 (quality/open findings) second.
|
||||
3. Execute PR-3 (hygiene/config hardening) third.
|
||||
4. Submit final supervisor review with linked evidence and closure checklist.
|
||||
|
||||
## Patch-Coverage Uplift Addendum (CodeQL Remediation Branch)
|
||||
|
||||
### Scope
|
||||
|
||||
Input baseline (`docs/plans/codecove_patch_report.md`): 18 uncovered patch lines across 9 backend files.
|
||||
|
||||
Goal: close uncovered branches with minimal, branch-specific tests only (no broad refactors).
|
||||
|
||||
### 1) Exact test files to add/update
|
||||
|
||||
- Update `backend/internal/api/handlers/emergency_handler_test.go`
|
||||
- Update `backend/internal/api/handlers/proxy_host_handler_update_test.go`
|
||||
- Update `backend/internal/crowdsec/hub_sync_test.go`
|
||||
- Update `backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go`
|
||||
- Update `backend/internal/services/backup_service_wave3_test.go`
|
||||
- Update `backend/internal/services/uptime_service_unit_test.go`
|
||||
- Update `backend/internal/api/middleware/emergency_test.go`
|
||||
- Update `backend/internal/cerberus/cerberus_middleware_test.go`
|
||||
- Update `backend/internal/crowdsec/hub_cache_test.go`
|
||||
|
||||
### 2) Minimal branch-execution scenarios
|
||||
|
||||
#### `backend/internal/api/handlers/emergency_handler.go` (3 lines)
|
||||
- Add middleware-prevalidated reset test: set `emergency_bypass=true` in context and assert `SecurityReset` takes middleware path and returns success.
|
||||
- Add reset failure-path test: force module-disable failure (closed DB/failed upsert) and assert HTTP 500 path executes.
|
||||
|
||||
#### `backend/internal/api/handlers/proxy_host_handler.go` (3 lines)
|
||||
- Add update payload case with `security_header_profile_id` as valid string to execute string-conversion success path.
|
||||
- Add update payload case with invalid string to execute string parse failure branch.
|
||||
- Add update payload case with unsupported type (boolean/object) to execute unsupported-type branch.
|
||||
|
||||
#### `backend/internal/crowdsec/hub_sync.go` (3 lines)
|
||||
- Add apply scenario where cache metadata exists but archive read fails, forcing refresh path and post-refresh archive read.
|
||||
- Add fallback fetch scenario with first endpoint returning fallback-eligible error, second endpoint success.
|
||||
- Add fallback-stop scenario with non-fallback error to execute early break path.
|
||||
|
||||
#### `backend/internal/api/handlers/crowdsec_handler.go` (2 lines)
|
||||
- Add apply test where cached meta exists but archive/preview file stat fails to execute missing-file log branches before apply.
|
||||
- Add pull/apply branch case that exercises cache-miss diagnostics and response payload path.
|
||||
|
||||
#### `backend/internal/services/backup_service.go` (2 lines)
|
||||
- Add unzip-with-skip test with oversized decompressed entry to execute decompression-limit rejection branch.
|
||||
- Add unzip-with-skip error-path test that validates extraction abort handling for invalid archive entry flow.
|
||||
|
||||
#### `backend/internal/services/uptime_service.go` (2 lines)
|
||||
- Add `CreateMonitor` test with `interval<=0` and `max_retries<=0` to execute defaulting branches.
|
||||
- Add TCP monitor validation case with invalid `host:port` input to execute TCP validation error path.
|
||||
|
||||
#### `backend/internal/api/middleware/emergency.go` (1 line)
|
||||
- Add malformed client IP test (`RemoteAddr` unparsable) with token present to execute invalid-IP branch and confirm bypass is not set.
|
||||
|
||||
#### `backend/internal/cerberus/cerberus.go` (1 line)
|
||||
- Add middleware test with `emergency_bypass=true` in gin context and ACL enabled to execute bypass short-circuit branch.
|
||||
|
||||
#### `backend/internal/crowdsec/hub_cache.go` (1 line)
|
||||
- Add cache-load test that causes non-ENOENT metadata read failure (e.g., invalid metadata path state) to execute hard read-error branch (not `ErrCacheMiss`).
|
||||
|
||||
### 3) Verification commands (targeted + patch report)
|
||||
|
||||
Run targeted backend tests only:
|
||||
|
||||
```bash
|
||||
cd /projects/Charon
|
||||
go test ./backend/internal/api/handlers -run 'TestEmergency|TestProxyHostUpdate|TestPullThenApply|TestApplyWithoutPull|TestApplyRollbackWhenCacheMissingAndRepullFails'
|
||||
go test ./backend/internal/crowdsec -run 'TestPull|TestApply|TestFetchWith|TestHubCache'
|
||||
go test ./backend/internal/services -run 'TestBackupService_UnzipWithSkip|TestCreateMonitor|TestUpdateMonitor|TestDeleteMonitor'
|
||||
go test ./backend/internal/api/middleware -run 'TestEmergencyBypass'
|
||||
go test ./backend/internal/cerberus -run 'TestMiddleware_'
|
||||
```
|
||||
|
||||
Generate local patch coverage report artifacts:
|
||||
|
||||
```bash
|
||||
cd /projects/Charon
|
||||
bash scripts/local-patch-report.sh
|
||||
```
|
||||
|
||||
Expected artifacts:
|
||||
- `test-results/local-patch-report.md`
|
||||
- `test-results/local-patch-report.json`
|
||||
|
||||
### 4) Acceptance criteria
|
||||
|
||||
- Patch coverage increases from `79.31034%` to `>= 90%` for this remediation branch.
|
||||
- Missing patch lines decrease from `18` to `<= 6` (target `0` if all branches are feasibly testable).
|
||||
- All nine listed backend files show reduced missing-line counts in local patch report output.
|
||||
- Targeted test commands pass with zero failures.
|
||||
|
||||
Reference in New Issue
Block a user