diff --git a/.github/workflows/auto-changelog.yml b/.github/workflows/auto-changelog.yml index 7d4abc81..ceeed77a 100644 --- a/.github/workflows/auto-changelog.yml +++ b/.github/workflows/auto-changelog.yml @@ -10,7 +10,7 @@ jobs: update-draft: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Draft Release uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 env: diff --git a/.github/workflows/auto-versioning.yml b/.github/workflows/auto-versioning.yml index 50db47d6..e169bbae 100644 --- a/.github/workflows/auto-versioning.yml +++ b/.github/workflows/auto-versioning.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: fetch-depth: 0 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 3a82a21d..efc5618f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -24,7 +24,7 @@ jobs: name: Performance Regression Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Set up Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 @@ -34,7 +34,7 @@ jobs: - name: Run Benchmark working-directory: backend - run: go test -bench=. -benchmem ./... | tee output.txt + run: go test -bench=. -benchmem -run='^$' ./... | tee output.txt - name: Store Benchmark Result uses: benchmark-action/github-action-benchmark@v1 diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 6906733a..57bf7b09 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: fetch-depth: 0 @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 70b6d81a..bd02dea9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: language: [ 'go', 'javascript-typescript' ] steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Initialize CodeQL uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4 diff --git a/.github/workflows/docker-lint.yml b/.github/workflows/docker-lint.yml index 2092a6ac..91fc80ff 100644 --- a/.github/workflows/docker-lint.yml +++ b/.github/workflows/docker-lint.yml @@ -14,7 +14,7 @@ jobs: hadolint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Run Hadolint uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f9e8862b..a3b9efd9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Normalize image name run: | @@ -181,7 +181,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Normalize image name run: | @@ -258,7 +258,7 @@ jobs: if: github.event_name == 'pull_request' steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Build image locally for PR run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 07254f3b..3e1366ec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: steps: # Step 1: Get the code - name: 📥 Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 # Step 2: Set up Node.js (for building any JS-based doc tools) - name: 🔧 Set up Node.js diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 27362b7f..1cfa5adb 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -11,7 +11,7 @@ jobs: name: Backend (Go) runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Set up Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 @@ -62,7 +62,7 @@ jobs: name: Frontend (React) runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Set up Node.js uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml index a4baeeca..c9068e89 100644 --- a/.github/workflows/release-goreleaser.yml +++ b/.github/workflows/release-goreleaser.yml @@ -19,7 +19,7 @@ jobs: CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }} steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: fetch-depth: 0 diff --git a/.github/workflows/waf-integration.yml b/.github/workflows/waf-integration.yml index 08e593d9..b5cd3ae3 100644 --- a/.github/workflows/waf-integration.yml +++ b/.github/workflows/waf-integration.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go index 96f293ec..c401aa23 100644 --- a/backend/internal/caddy/manager.go +++ b/backend/internal/caddy/manager.go @@ -131,7 +131,7 @@ func (m *Manager) ApplyConfig(ctx context.Context) error { engineMode := "On" // default to blocking if rs.Mode == "detection" || rs.Mode == "monitor" { engineMode = "DetectionOnly" - } else if rs.Mode == "" && secCfg.WAFMode == "monitor" { + } else if rs.Mode == "" && strings.EqualFold(secCfg.WAFMode, "monitor") { // No per-ruleset mode set, use global WAFMode engineMode = "DetectionOnly" } @@ -386,6 +386,14 @@ func (m *Manager) computeEffectiveFlags(ctx context.Context) (cerbEnabled bool, crowdsecEnabled = false } } + + // runtime override for WAF mode + var sc models.SecurityConfig + if err := m.db.Where("name = ?", "default").First(&sc).Error; err == nil { + if sc.WAFMode != "" { + wafEnabled = !strings.EqualFold(sc.WAFMode, "disabled") + } + } } // ACL, WAF, RateLimit and CrowdSec should only be considered enabled if Cerberus is enabled. diff --git a/backend/internal/caddy/manager_test.go b/backend/internal/caddy/manager_test.go index 22e5c5eb..7f765e8e 100644 --- a/backend/internal/caddy/manager_test.go +++ b/backend/internal/caddy/manager_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "time" @@ -459,3 +460,76 @@ func TestComputeEffectiveFlags_DB_ACLTrueAndFalse(t *testing.T) { _, acl, _, _, _ = manager.computeEffectiveFlags(context.Background()) require.False(t, acl) } + +func TestComputeEffectiveFlags_DB_WAFMonitor(t *testing.T) { +dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) +db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) +require.NoError(t, err) + require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) + +secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"} +manager := NewManager(nil, db, "", "", false, secCfg) + +// Set WAF mode to monitor + res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor"}) + require.NoError(t, res.Error) + +_, _, waf, _, _ := manager.computeEffectiveFlags(context.Background()) +require.True(t, waf) // Should still be true (enabled) +} + +func TestManager_ApplyConfig_WAFMonitor(t *testing.T) { + // Mock Caddy Admin API + caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/load" && r.Method == "POST" { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer caddyServer.Close() + + // Setup DB + dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{})) + + // Set WAF mode to monitor + db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor", AdminWhitelist: "127.0.0.1"}) + + // Create a ruleset + db.Create(&models.SecurityRuleSet{Name: "owasp-crs", Content: "SecRule REQUEST_URI \"@rx ^/admin\" \"id:101,phase:1,deny,status:403\""}) + + // Setup Manager + tmpDir := t.TempDir() + client := NewClient(caddyServer.URL) + manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"}) + + // Capture file writes to verify WAF mode injection + var writtenContent string + originalWriteFile := writeFileFunc + defer func() { writeFileFunc = originalWriteFile }() + writeFileFunc = func(filename string, data []byte, perm os.FileMode) error { + if strings.Contains(filename, "owasp-crs.conf") { + writtenContent = string(data) + } + return originalWriteFile(filename, data, perm) + } + + // Create a host + host := models.ProxyHost{ + DomainNames: "example.com", + ForwardHost: "127.0.0.1", + ForwardPort: 8080, + } + db.Create(&host) + + // Apply Config + err = manager.ApplyConfig(context.Background()) + assert.NoError(t, err) + + // Verify that DetectionOnly was injected into the ruleset file + assert.Contains(t, writtenContent, "SecRuleEngine DetectionOnly") + assert.Contains(t, writtenContent, "SecRequestBodyAccess On") +} diff --git a/backend/internal/services/benchmark_test.go b/backend/internal/services/benchmark_test.go new file mode 100644 index 00000000..6b26827b --- /dev/null +++ b/backend/internal/services/benchmark_test.go @@ -0,0 +1,22 @@ +package services + +import ( + "testing" + "time" +) + +func BenchmarkFormatDuration(b *testing.B) { + d := 3665 * time.Second + b.ResetTimer() + for i := 0; i < b.N; i++ { + formatDuration(d) + } +} + +func BenchmarkExtractPort(b *testing.B) { + url := "http://example.com:8080" + b.ResetTimer() + for i := 0; i < b.N; i++ { + extractPort(url) + } +}