Skip to content

feat: general-purpose compliance event reporting from filter scripts#371

Merged
lonelycode merged 7 commits intomainfrom
feat/370-audit-filter-redactions
Apr 14, 2026
Merged

feat: general-purpose compliance event reporting from filter scripts#371
lonelycode merged 7 commits intomainfrom
feat/370-audit-filter-redactions

Conversation

@lonelycode
Copy link
Copy Markdown
Member

Summary

  • Adds a general-purpose compliance event reporting mechanism to the enterprise filter scripting system, allowing script developers to flag any compliance-relevant activity (redactions, rewrites, PII detection, silent failures, third-party model calls, etc.)
  • Events flow through the full analytics pipeline: script output → async recording → database storage → gRPC transmission from edge gateways to control plane → compliance API
  • Introduces GET /compliance/events API endpoint with filtering by severity, event type, app ID, and pagination

Closes #370

How it works

Script developers set compliance_events in their Tengo output object:

tyk := import("tyk")
modified := tyk.redact_pattern(input, "\\d{3}-\\d{2}-\\d{4}", "[SSN]")

output := {
    block: false,
    payload: modified,
    compliance_events: [
        {
            event_type: "pii_redacted",
            severity: "warning",
            description: "SSN pattern found and redacted",
            metadata: { "pattern": "ssn", "count": 2 }
        }
    ]
}

Changes by layer

Scripting — New ComplianceEventOutput type on ScriptOutput, Tengo extraction with severity validation, bridge mapping in scripting_ent.go

AnalyticsRecordComplianceEvents on AnalyticsHandler interface, async channel+worker in DatabaseHandler, aistudio_compliance_events_total Prometheus metric, shared context-enrichment helper

Filter execution — Recording wired up at all 6 filter sites: proxy request/response, chat request/response, file reference, tool response

Compliance serviceGetComplianceEvents with date range, app ID, event type, severity filtering, aggregation (by_severity, by_type, by_filter), timeline, pagination

APIGET /compliance/events endpoint

gRPC edge→controlComplianceEventProto message in AnalyticsPulse, pulse plugin buffering on edge, control server processing and DB storage on receipt

Test plan

  • Enterprise scripting tests: 8 Tengo extraction tests (single/multiple events, metadata, severity validation, missing fields, block+events combo)
  • Analytics pipeline tests: handler forwarding + Prometheus metric emission, nil/empty edge cases
  • Compliance recorder tests: context enrichment, JSON metadata serialization, nil/empty output handling
  • API endpoint tests: 7 sub-tests (empty response, data insertion + aggregation verification, severity/type/app_id filtering, limit+offset pagination, metadata JSON parsing)
  • gRPC pulse tests: 6 control server tests (full compliance pulse, compliance-only pulse, zero-field events, empty list, mixed analytics+compliance, record count verification)
  • Pulse plugin tests: 7 tests (buffering, empty slice, concurrent access, proto message building with full/empty/missing fields, mixed data)
  • All existing tests pass with no regressions

🤖 Generated with Claude Code

…pts (#370)

Filter scripts can now report compliance events (redactions, rewrites, PII detection,
silent failures, etc.) by setting compliance_events in their Tengo output object.
Events flow through the analytics pipeline and are stored for compliance reporting.

Key changes:
- New ComplianceEventOutput type in ScriptOutput for script-reported events
- Tengo extraction logic with severity validation and event_type requirement
- Async analytics pipeline (RecordComplianceEvents) following existing patterns
- New aistudio_compliance_events_total Prometheus metric
- Recording at all 6 filter execution sites (proxy/chat request/response)
- ComplianceEvent model with GORM auto-migration
- Enterprise compliance service GetComplianceEvents with filtering/aggregation
- GET /compliance/events API endpoint
- gRPC edge→control flow via AnalyticsPulse proto with ComplianceEventProto
- Microgateway pulse plugin buffering and batch transmission
- Control server receives and stores edge compliance events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 13, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
0 out of 2 committers have signed the CLA.

❌ Martin Buhr
❌ lonelycode


Martin Buhr seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@probelabs
Copy link
Copy Markdown
Contributor

probelabs bot commented Apr 13, 2026

This pull request introduces a general-purpose compliance event reporting system, enabling filter scripts to flag custom, compliance-relevant activities. These events are processed through the full analytics pipeline, from script output to a new database model, transmitted via gRPC from edge gateways, and made available through a new GET /compliance/events API endpoint with filtering and pagination.

Files Changed Analysis

This is a significant feature implementation, reflected in the 30 files changed with 2041 additions and only 104 deletions. The changes span the entire application stack, indicating a full vertical slice of functionality.

  • New Core Files: The PR adds four new files that form the foundation of this feature: models/compliance_event.go (the GORM model and database schema), scripting/compliance_recorder.go (the logic for enriching and recording events), and their corresponding test files. A new test file, microgateway/internal/plugins/analytics_pulse_compliance_test.go, was also added to test the gRPC pulse mechanism.
  • System-Wide Impact: Modifications touch nearly every major component: models for the new data structure, scripting to handle output from Tengo scripts, analytics for the new processing pipeline, proto for the gRPC message definition, microgateway for edge-side buffering, and api for the new public endpoint. This demonstrates a well-integrated, system-wide feature.

Architecture & Impact Assessment

What this PR accomplishes

This PR provides a flexible and powerful framework for detailed compliance auditing. It allows developers to define and report any relevant event from within a filter script (e.g., PII redaction, content rewrites, silent failures), providing rich, contextual data that goes far beyond simple block/allow decisions. This creates a detailed audit trail for actions taken by the AI gateway's policies.

Key technical changes introduced

  1. Scripting Engine Extension: The ScriptOutput struct in scripting/types.go is extended with a ComplianceEvents field, allowing Tengo scripts to return a list of structured event objects.
  2. New Analytics Pipeline: A dedicated pipeline for compliance events is created. This includes adding RecordComplianceEvents to the AnalyticsHandler interface (analytics/interface.go) and implementing an asynchronous, batch-processing worker in analytics/database_handler.go for efficient database insertion.
  3. gRPC Integration: The feature is built for a distributed environment. The AnalyticsPulse message in proto/config_sync.proto is updated with a ComplianceEventProto message, and the microgateway's analytics_pulse_plugin.go is modified to buffer and transmit these events from edge gateways to the control plane.
  4. Data Persistence: A new compliance_events table is introduced, defined by the models.ComplianceEvent GORM model, with indexes to ensure efficient querying by application, time, and event type.
  5. API Exposure: A new endpoint, GET /api/v1/compliance/events, is created in api/compliance_handlers.go. It supports filtering by date range, app ID, event type, and severity, as well as pagination.
  6. Full Coverage: The compliance recording logic is integrated at all six filter execution points (chat_session/, proxy/), ensuring that events can be captured from any request, response, file scan, or tool call filter.

Data Flow Diagram

sequenceDiagram
    participant Script as "Tengo Script"
    participant FilterHost as "Filter Host (Proxy/Chat)"
    participant Analytics as "Analytics Service (Edge)"
    participant PulsePlugin as "Pulse Plugin (Edge)"
    participant ControlPlane as "Control Plane (gRPC Server)"
    participant DB as "Database"
    participant API as "API Server"

    FilterHost->>Script: Execute script
    Script-->>FilterHost: Return output with `compliance_events`
    FilterHost->>Analytics: RecordComplianceEvents()
    Analytics->>PulsePlugin: BufferComplianceEvents()

    loop Every X seconds
        PulsePlugin->>ControlPlane: SendAnalyticsPulse(gRPC)
    end

    ControlPlane->>Analytics: RecordComplianceEvents()
    Analytics->>DB: (Async Batch) INSERT INTO compliance_events
    
    participant User
    User->>API: GET /compliance/events
    API->>DB: SELECT * FROM compliance_events
    DB-->>API: Return events
    API-->>User: Return JSON response
Loading

Scope Discovery & Context Expansion

  • The new models.ComplianceEvent entity is a foundational piece that can be leveraged for future compliance-related features, such as dedicated dashboards, automated alerting on critical events, or more sophisticated application risk profiling.
  • The changes to microgateway/ and grpc/ confirm that this feature is architected for a distributed system, correctly handling the aggregation of data from multiple edge gateways.
  • By modifying the analytics.AnalyticsHandler interface, the PR ensures architectural consistency. All analytics handlers, including those for different deployment models (file_analytics_handler.go, http_analytics_handler.go), must now account for compliance events, even if it's a no-op implementation.
  • The addition of the aistudio_compliance_events_total Prometheus metric in metrics/metrics.go provides crucial observability, allowing operators to monitor the volume and characteristics of compliance events being generated across the platform.
Metadata
  • Review Effort: 5 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-04-14T05:24:48.403Z | Triggered by: pr_updated | Commit: a90ca1c

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs
Copy link
Copy Markdown
Contributor

probelabs bot commented Apr 13, 2026

\n\n

Architecture Issues (2)

Severity Location Issue
🟠 Error chat_session/chat_session.go:384-1496
Compliance events generated from chat sessions (request, file reference, and tool response filters) are missing the Application ID. The `appID` is hardcoded to `0` in calls to `scripting.RecordComplianceEvents` on lines 384, 697, and 1496, even though the correct `appID` is available in `cs.appID`. This prevents proper attribution of compliance events and is inconsistent with how proxy-related events are handled.
💡 SuggestionIn `chat_session/chat_session.go`, replace the hardcoded `0` with `cs.appID` in the three calls to `scripting.RecordComplianceEvents` to ensure the correct Application ID is recorded for compliance events.
🟠 Error chat_session/response_filter_utils.go:75
Compliance events from chat response filters are missing both Application ID and LLM ID. The call to `scripting.RecordComplianceEvents` hardcodes `appID` and `llmID` to `0`, breaking filtering and attribution for these events. The necessary `llmID` is already available as a function parameter, and the `appID` should be passed into this function.
💡 SuggestionModify the `ExecuteResponseFilters` function signature to accept the `appID uint`. Then, update the call to `scripting.RecordComplianceEvents` to pass the new `appID` parameter and the existing `llmID` parameter instead of hardcoded zeros. The call sites in `chat_session/chat_session.go` will also need to be updated to pass `cs.appID` to `ExecuteResponseFilters`.

Performance Issues (2)

Severity Location Issue
🟡 Warning api/compliance_handlers.go:100
The API handler for getting compliance events appears to execute multiple distinct database queries to fetch the main event list, the total count, and several aggregations (by severity, type, and filter). This can result in at least four separate database round-trips for a single API request, increasing database load and response latency.
💡 SuggestionWhile individual queries may be fast due to indexing, the number of queries can become a bottleneck. Consider optimizing the data retrieval in the `complianceService.GetComplianceEvents` implementation. One approach is to use a single, more complex SQL query with window functions or CTEs to compute the aggregations alongside fetching the paginated data, reducing the number of database round-trips. If that's not feasible with GORM, ensure that the performance of this endpoint is monitored, as it may require a caching layer or pre-aggregated summary tables if the `compliance_events` table grows very large.
🟡 Warning scripting/compliance_recorder.go:25-29
The function `RecordComplianceEvents` serializes metadata to JSON using `json.Marshal` inside a loop. This operation is performed synchronously on the request-processing path before the event is passed to the asynchronous analytics handler. If a script generates a large number of compliance events, the cumulative cost of JSON serialization within the loop could introduce noticeable latency.
💡 SuggestionFor most use cases where scripts generate only a few compliance events, the current implementation is acceptable. However, to make it more robust for high-throughput scenarios or scripts with many events, consider moving the JSON serialization into the asynchronous worker in `analytics/database_handler.go`. This would involve passing the `map[string]interface{}` to the channel and serializing it just before the batch database write, completely removing this work from the hot path.

Quality Issues (2)

Severity Location Issue
🟡 Warning api/compliance_handlers_test.go:120-126
The test `TestComplianceEnterprise_GetComplianceEvents` uses hardcoded integer literals for asserting aggregation results (e.g., `assert.Equal(t, 2, response.ByType["pii_redacted"])`). While correct for the current test data, this makes the test brittle. If the test data in the `events` slice is modified, these assertions must be manually recalculated and updated, increasing maintenance overhead.
💡 SuggestionTo improve maintainability, derive the expected aggregation counts programmatically from the `events` test data slice within the test function. This would ensure the assertions automatically adapt to any changes in the test setup, making the test more robust and easier to maintain.
🟡 Warning grpc/analytics_pulse_test.go:84
The test uses `time.Sleep(200 * time.Millisecond)` to wait for asynchronous analytics processing to complete. This can lead to flaky tests if the processing takes longer than the sleep duration under certain conditions (e.g., high system load).
💡 SuggestionTo create a more robust test, consider using synchronization primitives like channels or wait groups to signal the completion of the asynchronous operation. This would require modifying the `DatabaseHandler` to include testing hooks, but would eliminate the risk of race conditions in the test.

Powered by Visor from Probelabs

Last updated: 2026-04-14T05:24:39.175Z | Triggered by: pr_updated | Commit: a90ca1c

💡 TIP: You can chat with Visor using /visor ask <your question>

Martin Buhr and others added 3 commits April 14, 2026 16:19
Address PR review feedback:
- Validate severity query param against whitelist (info/warning/critical)
  at the API layer as defense-in-depth (GORM already uses parameterized
  queries, but reject invalid values early with a clear 400 error)
- Add tests for invalid severity and SQL injection attempt in severity param
- context.Background() in chat session paths matches the established
  pattern throughout ChatSession (30+ usages) since the struct has no
  request-scoped context (long-lived WebSocket sessions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address PR review feedback on context propagation:
- Add ctx/ctxCancel to ChatSession, cancelled on Stop()
- Use cs.ctx instead of context.Background() for compliance event
  recording in chat request, file reference, and tool response filters
- Thread ctx through ExecuteResponseFilters for chat response filters
- Add max-length validation (100 chars) on event_type query parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The enterprise GetComplianceEvents uses GORM parameterized queries
(db.Where("event_type = ?", val)) so injection attempts are treated
as literal string matches returning 0 results. Added tests proving:
- SQL injection in event_type returns empty results (not an error)
- Oversized event_type (>100 chars) returns 400

The /compliance/events endpoint is already admin-only via the v1
route group middleware chain (AuthMiddleware + AdminOnly) applied
at api/api.go:535-536.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lonelycode
Copy link
Copy Markdown
Member Author

Re: SQL injection concern on event_type — the enterprise implementation is confirmed parameterized. Here's the relevant code from enterprise/features/compliance/service.go:1249-1260:

func (s *enterpriseService) GetComplianceEvents(startDate, endDate time.Time, appID *uint, eventType *string, severity *string, limit int, offset int) (*compliance.ComplianceEventsData, error) {
	query := s.db.Model(&models.ComplianceEvent{}).Where("time_stamp BETWEEN ? AND ?", startDate, endDate)

	if appID != nil {
		query = query.Where("app_id = ?", *appID)
	}
	if eventType != nil && *eventType != "" {
		query = query.Where("event_type = ?", *eventType)
	}
	if severity != nil && *severity != "" {
		query = query.Where("severity = ?", *severity)
	}

All user inputs use GORM's ? placeholder parameterization — no string concatenation anywhere in the query construction. The test SQL injection in event_type returns no results proves the injection attempt is treated as a literal string match.

Additionally, at the API layer we validate:

  • severity against a whitelist (info/warning/critical)
  • event_type max length (100 chars)
  • app_id parsed as uint (non-numeric values ignored)
  • limit/offset parsed as int (non-numeric values use defaults)

Martin Buhr and others added 2 commits April 14, 2026 17:20
Points to enterprise feat/370-audit-filter-redactions branch which adds:
- ComplianceEvents field to ScriptOutput with Tengo extraction
- GetComplianceEvents method on enterprise compliance service

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lonelycode
Copy link
Copy Markdown
Member Author

Re: Missing Application ID in chat session compliance events — ChatSession doesn't have an appID field, and the Chat model (models/chat.go) has no AppID association either. Chat sessions are associated with an LLM + user, not an app. The app context only exists in the proxy path where requests are authenticated via API credentials tied to an app.

The 0 value for appID is intentional and consistent with how other chat session analytics are recorded (e.g. RecordContentMessage at line 1534 passes appID from the caller, which in the chat session context is also not app-scoped).

If we want to associate chat sessions with apps in the future, that would require adding an AppID field to the Chat model — which is a broader architectural change beyond the scope of this PR.

@lonelycode lonelycode merged commit 7b08976 into main Apr 14, 2026
12 of 15 checks passed
@lonelycode lonelycode deleted the feat/370-audit-filter-redactions branch April 14, 2026 05:58
lonelycode pushed a commit that referenced this pull request Apr 14, 2026
Update all pre-provided filter scripts, embedded UI templates, and
documentation to demonstrate the compliance_events output field added
in #371. Enterprise users now have working examples of governance event
reporting in PII redaction, content blocking, and response guardrails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lonelycode added a commit that referenced this pull request Apr 15, 2026
…ion (#372)

* docs: add compliance event reporting to filter scripts and documentation

Update all pre-provided filter scripts, embedded UI templates, and
documentation to demonstrate the compliance_events output field added
in #371. Enterprise users now have working examples of governance event
reporting in PII redaction, content blocking, and response guardrails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: standardise compliance event metadata keys across examples

Use "matched_pattern" consistently for detected text and
"redacted_types" (as an array) for PII redaction scripts, replacing
the inconsistent mix of "keyword", "pattern", "patterns".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Martin Buhr <martin@buhr.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: audit log filter redactions that don't block requests

2 participants