diff --git a/wasmplugin/auditlogger.go b/wasmplugin/auditlogger.go new file mode 100644 index 0000000..8ae1ddf --- /dev/null +++ b/wasmplugin/auditlogger.go @@ -0,0 +1,105 @@ +package wasmplugin + +import ( + "fmt" + "sync" + + ctypes "github.com/corazawaf/coraza/v3/types" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" +) + +// Transaction context to store against a transaction ID +type TxnContext struct { + envoyRequestId string +} + +// Logger that includes context from the request in the audit logs and owns the final formatting +type ContextualAuditLogger struct { + IncludeRequestContext bool + txnContextMap map[string]*TxnContext + lock sync.Mutex +} + +// Get the global audit logger that can be used across all requests +func NewAppAuditLogger(includeRequestContext bool) *ContextualAuditLogger { + return &ContextualAuditLogger{ + txnContextMap: make(map[string]*TxnContext), + IncludeRequestContext: includeRequestContext, + } +} + +// Register a transaction with the logger +func (cal *ContextualAuditLogger) Register(txnId string, ctx *TxnContext) { + cal.lock.Lock() + defer cal.lock.Unlock() + + cal.txnContextMap[txnId] = ctx +} + +// Remove the transaction information from the context map +func (cal *ContextualAuditLogger) Unregister(txnId string) { + cal.lock.Lock() + defer cal.lock.Unlock() + + delete(cal.txnContextMap, txnId) +} + +// Emit log on the given rule and add the txn context if available +func (cal *ContextualAuditLogger) AuditLog(rule ctypes.MatchedRule) { + cal.lock.Lock() + defer cal.lock.Unlock() + + txnId := rule.TransactionID() + + logPrefix := "" + if ctx, ok := cal.txnContextMap[txnId]; ok { + if cal.IncludeRequestContext { + // If we have context, add it to the log + logPrefix = fmt.Sprintf("[request-id %q] ", ctx.envoyRequestId) + } + } + + logError(rule, logPrefix) +} + +func logError(error ctypes.MatchedRule, logPrefix string) { + msg := logPrefix + error.ErrorLog() + switch error.Rule().Severity() { + case ctypes.RuleSeverityEmergency: + proxywasm.LogCritical(msg) + case ctypes.RuleSeverityAlert: + proxywasm.LogCritical(msg) + case ctypes.RuleSeverityCritical: + proxywasm.LogCritical(msg) + case ctypes.RuleSeverityError: + proxywasm.LogError(msg) + case ctypes.RuleSeverityWarning: + proxywasm.LogWarn(msg) + case ctypes.RuleSeverityNotice: + proxywasm.LogInfo(msg) + case ctypes.RuleSeverityInfo: + proxywasm.LogInfo(msg) + case ctypes.RuleSeverityDebug: + proxywasm.LogDebug(msg) + } +} + +const ( + // This is the standard Envoy header for request IDs + envoyRequestIdHeader = "x-request-id" +) + +// A convenience method to register the request information with the audit logger if available +// on the request (else ignores). Must be called in the request context. +func registerRequestContextWithLogger(auditLogger *ContextualAuditLogger, txnId string) { + if id, err := proxywasm.GetHttpRequestHeader(envoyRequestIdHeader); err == nil { + auditLogger.Register(txnId, &TxnContext{ + envoyRequestId: id, + }) + } +} + +// Remove context for the given transaction ID +func removeRequestContextFromLogger(auditLogger *ContextualAuditLogger, txnId string) { + auditLogger.Unregister(txnId) +} diff --git a/wasmplugin/config.go b/wasmplugin/config.go index 0734524..caa3b44 100644 --- a/wasmplugin/config.go +++ b/wasmplugin/config.go @@ -12,10 +12,11 @@ import ( // pluginConfiguration is a type to represent an example configuration for this wasm plugin. type pluginConfiguration struct { - directivesMap DirectivesMap - metricLabels map[string]string - defaultDirectives string - perAuthorityDirectives map[string]string + directivesMap DirectivesMap + metricLabels map[string]string + defaultDirectives string + perAuthorityDirectives map[string]string + includeRequestIdInAuditLogs bool } type DirectivesMap map[string][]string @@ -33,6 +34,11 @@ func parsePluginConfiguration(data []byte, infoLogger func(string)) (pluginConfi } jsonData := gjson.ParseBytes(data) + includeReqId := jsonData.Get("include_request_id_in_audit_logs") + if includeReqId.Exists() && includeReqId.IsBool() && includeReqId.Bool() { + config.includeRequestIdInAuditLogs = true + } + config.directivesMap = make(DirectivesMap) jsonData.Get("directives_map").ForEach(func(key, value gjson.Result) bool { directiveName := key.String() diff --git a/wasmplugin/plugin.go b/wasmplugin/plugin.go index e2fae78..3ee44ba 100644 --- a/wasmplugin/plugin.go +++ b/wasmplugin/plugin.go @@ -30,7 +30,7 @@ func NewVMContext() types.VMContext { return &vmContext{} } -func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { +func (vc *vmContext) NewPluginContext(contextID uint32) types.PluginContext { return &corazaPlugin{} } @@ -80,6 +80,7 @@ type corazaPlugin struct { perAuthorityWAFs wafMap metricLabelsKV []string metrics *wafMetrics + auditLogger *ContextualAuditLogger } func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { @@ -94,6 +95,8 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug return types.OnPluginStartStatusFailed } + ctx.auditLogger = NewAppAuditLogger(config.includeRequestIdInAuditLogs) + // directivesAuthoritesMap is a map of directives name to the list of // authorities that reference those directives. This is used to // initialize the WAFs only for the directives that are referenced @@ -123,7 +126,7 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug // First we initialize our waf and our seclang parser conf := coraza.NewWAFConfig(). - WithErrorCallback(logError). + WithErrorCallback(ctx.auditLogger.AuditLog). WithDebugLogger(debuglog.DefaultWithPrinterFactory(logPrinterFactory)). // TODO(anuraaga): Make this configurable in plugin configuration. // WithRequestBodyLimit(1024 * 1024 * 1024). @@ -181,6 +184,7 @@ func (ctx *corazaPlugin) NewHttpContext(contextID uint32) types.HttpContext { metrics: ctx.metrics, metricLabelsKV: ctx.metricLabelsKV, perAuthorityWAFs: ctx.perAuthorityWAFs, + auditLogger: ctx.auditLogger, } } @@ -228,6 +232,7 @@ type httpContext struct { interruptedAt interruptionPhase logger debuglog.Logger metricLabelsKV []string + auditLogger *ContextualAuditLogger } func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { @@ -245,6 +250,7 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t } authority = string(propHostRaw) } + if waf, isDefault, resolveWAFErr := ctx.perAuthorityWAFs.getWAFOrDefault(authority); resolveWAFErr == nil { ctx.tx = waf.NewTransaction() @@ -268,6 +274,9 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t tx := ctx.tx + // Register context with audit logging context + registerRequestContextWithLogger(ctx.auditLogger, ctx.tx.ID()) + // This currently relies on Envoy's behavior of mapping all requests to HTTP/2 semantics // and its request properties, but they may not be true of other proxies implementing // proxy-wasm. @@ -632,6 +641,8 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types } func (ctx *httpContext) OnHttpStreamDone() { + // Cleanup transaction ID from the audit logging context + defer removeRequestContextFromLogger(ctx.auditLogger, ctx.tx.ID()) defer logTime("OnHttpStreamDone", currentTime()) tx := ctx.tx @@ -695,28 +706,6 @@ func (ctx *httpContext) handleInterruption(phase interruptionPhase, interruption return types.ActionPause } -func logError(error ctypes.MatchedRule) { - msg := error.ErrorLog() - switch error.Rule().Severity() { - case ctypes.RuleSeverityEmergency: - proxywasm.LogCritical(msg) - case ctypes.RuleSeverityAlert: - proxywasm.LogCritical(msg) - case ctypes.RuleSeverityCritical: - proxywasm.LogCritical(msg) - case ctypes.RuleSeverityError: - proxywasm.LogError(msg) - case ctypes.RuleSeverityWarning: - proxywasm.LogWarn(msg) - case ctypes.RuleSeverityNotice: - proxywasm.LogInfo(msg) - case ctypes.RuleSeverityInfo: - proxywasm.LogInfo(msg) - case ctypes.RuleSeverityDebug: - proxywasm.LogDebug(msg) - } -} - // retrieveAddressInfo retrieves address properties from the proxy // Expected targets are "source" or "destination" // Envoy ref: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes#connection-attributes