diff --git a/experimental/waf.go b/experimental/waf.go new file mode 100644 index 000000000..2ef03e549 --- /dev/null +++ b/experimental/waf.go @@ -0,0 +1,36 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package experimental + +import ( + "context" + "strings" + + "github.com/corazawaf/coraza/v3/internal/corazawaf" + "github.com/corazawaf/coraza/v3/types" +) + +type Option func(*corazawaf.Options) + +// WithID sets the transaction ID +func WithID(id string) Option { + return func(o *corazawaf.Options) { + o.ID = strings.TrimSpace(id) + } +} + +// WithContext sets the transaction context, this is useful for passing +// a context into the logger. +// The transaction lifecycle isn't tied to the context lifecycle. +func WithContext(ctx context.Context) Option { + return func(o *corazawaf.Options) { + o.Context = ctx + } +} + +// WAFWithOptions is an interface that allows to create transactions +// with options +type WAFWithOptions interface { + NewTransactionWithOptions(opts ...Option) types.Transaction +} diff --git a/http/middleware.go b/http/middleware.go index 666f56399..e4bff8915 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/coraza/v3/types" ) @@ -117,8 +118,18 @@ func WrapHandler(waf coraza.WAF, h http.Handler) http.Handler { return h } + newTX := func(*http.Request) types.Transaction { + return waf.NewTransaction() + } + + if ctxwaf, ok := waf.(experimental.WAFWithOptions); ok { + newTX = func(r *http.Request) types.Transaction { + return ctxwaf.NewTransactionWithOptions(experimental.WithContext(r.Context())) + } + } + fn := func(w http.ResponseWriter, r *http.Request) { - tx := waf.NewTransaction() + tx := newTX(r) defer func() { // We run phase 5 rules and create audit logs (if enabled) tx.ProcessLogging() diff --git a/internal/corazarules/rule_match.go b/internal/corazarules/rule_match.go index 8810bc2d6..ff5ca05f6 100644 --- a/internal/corazarules/rule_match.go +++ b/internal/corazarules/rule_match.go @@ -4,6 +4,7 @@ package corazarules import ( + "context" "fmt" "strconv" "strings" @@ -30,6 +31,8 @@ type MatchData struct { ChainLevel_ int } +var _ types.MatchData = (*MatchData)(nil) + func (m *MatchData) Variable() variables.RuleVariable { return m.Variable_ } @@ -100,8 +103,12 @@ type MatchedRule struct { MatchedDatas_ []types.MatchData Rule_ types.RuleMetadata + + Context_ context.Context } +var _ types.MatchedRule = (*MatchedRule)(nil) + func (mr *MatchedRule) Message() string { return mr.Message_ } @@ -142,6 +149,35 @@ func (mr *MatchedRule) Rule() types.RuleMetadata { return mr.Rule_ } +// Context returns the context associated with the transaction +// This is useful for logging purposes where you want to add +// additional information to the log. +// The context can be easily retrieved in the logger using +// an ancillary interface: +// ``` +// +// type Contexter interface { +// Context() context.Context +// } +// +// ``` +// and then using it like this: +// +// ``` +// +// func errorLogCb(mr types.MatchedRule) { +// ctx := context.Background() +// if ctxer, ok := mr.(Contexter); ok { +// ctx = ctxer.Context() +// } +// logger.Context(ctx).Error().Msg("...") +// } +// +// ``` +func (mr *MatchedRule) Context() context.Context { + return mr.Context_ +} + const maxSizeLogMessage = 200 func (mr MatchedRule) writeDetails(log *strings.Builder, matchData types.MatchData) { diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go index 720c78272..a63e0b668 100644 --- a/internal/corazawaf/transaction.go +++ b/internal/corazawaf/transaction.go @@ -5,6 +5,7 @@ package corazawaf import ( "bufio" + "context" "errors" "fmt" "io" @@ -42,6 +43,9 @@ type Transaction struct { // Transaction ID id string + // The context associated to the transaction. + context context.Context + // Contains the list of matched rules and associated match information matchedRules []types.MatchedRule @@ -501,6 +505,7 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) { Rule_: &r.RuleMetadata, Log_: r.Log, MatchedDatas_: mds, + Context_: tx.context, } // Populate MatchedRule disruption related fields only if the Engine is capable of performing disruptive actions if tx.RuleEngine == types.RuleEngineOn { diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go index 7ba74fed1..5d280ddc6 100644 --- a/internal/corazawaf/waf.go +++ b/internal/corazawaf/waf.go @@ -4,6 +4,7 @@ package corazawaf import ( + "context" "errors" "fmt" "io" @@ -11,7 +12,6 @@ import ( "os" "regexp" "strconv" - "strings" "time" "github.com/corazawaf/coraza/v3/debuglog" @@ -133,24 +133,38 @@ type WAF struct { ArgumentLimit int } +// Options is used to pass options to the WAF instance +type Options struct { + ID string + Context context.Context +} + // NewTransaction Creates a new initialized transaction for this WAF instance func (w *WAF) NewTransaction() *Transaction { - return w.newTransactionWithID(stringutils.RandomString(19)) + return w.newTransaction(Options{ + ID: stringutils.RandomString(19), + Context: context.Background(), + }) } -func (w *WAF) NewTransactionWithID(id string) *Transaction { - if len(strings.TrimSpace(id)) == 0 { - id = stringutils.RandomString(19) - w.Logger.Warn().Msg("Empty ID passed for new transaction") +func (w *WAF) NewTransactionWithOptions(opts Options) *Transaction { + if opts.ID == "" { + opts.ID = stringutils.RandomString(19) } - return w.newTransactionWithID(id) + + if opts.Context == nil { + opts.Context = context.Background() + } + + return w.newTransaction(opts) } // NewTransactionWithID Creates a new initialized transaction for this WAF instance // Using the specified ID -func (w *WAF) newTransactionWithID(id string) *Transaction { +func (w *WAF) newTransaction(opts Options) *Transaction { tx := w.txPool.Get().(*Transaction) - tx.id = id + tx.id = opts.ID + tx.context = opts.Context tx.matchedRules = []types.MatchedRule{} tx.interruption = nil tx.Logdata = "" // Deprecated, this variable is not used. Logdata for each matched rule is stored in the MatchData field. diff --git a/internal/corazawaf/waf_test.go b/internal/corazawaf/waf_test.go index 883c09337..771667c35 100644 --- a/internal/corazawaf/waf_test.go +++ b/internal/corazawaf/waf_test.go @@ -15,7 +15,7 @@ func TestNewTransaction(t *testing.T) { waf.ResponseBodyAccess = true waf.RequestBodyLimit = 1044 - tx := waf.NewTransactionWithID("test") + tx := waf.NewTransactionWithOptions(Options{ID: "test"}) if !tx.RequestBodyAccess { t.Error("Request body access not enabled") } @@ -28,7 +28,7 @@ func TestNewTransaction(t *testing.T) { if tx.id != "test" { t.Error("ID not set") } - tx = waf.NewTransactionWithID("") + tx = waf.NewTransactionWithOptions(Options{ID: ""}) if tx.id == "" { t.Error("ID not set") } diff --git a/types/rule_match.go b/types/rule_match.go index 8254661e8..a45e332af 100644 --- a/types/rule_match.go +++ b/types/rule_match.go @@ -45,5 +45,6 @@ type MatchedRule interface { Rule() RuleMetadata AuditLog() string + ErrorLog() string } diff --git a/waf.go b/waf.go index 30382c174..741c37b0a 100644 --- a/waf.go +++ b/waf.go @@ -4,8 +4,11 @@ package coraza import ( + "context" "fmt" + "strings" + "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/coraza/v3/internal/corazawaf" "github.com/corazawaf/coraza/v3/internal/seclang" "github.com/corazawaf/coraza/v3/types" @@ -129,5 +132,19 @@ func (w wafWrapper) NewTransaction() types.Transaction { // NewTransactionWithID implements the same method on WAF. func (w wafWrapper) NewTransactionWithID(id string) types.Transaction { - return w.waf.NewTransactionWithID(id) + id = strings.TrimSpace(id) + if len(id) == 0 { + w.waf.Logger.Warn().Msg("Empty ID passed for new transaction") + } + + return w.waf.NewTransactionWithOptions(corazawaf.Options{Context: context.Background(), ID: id}) +} + +// NewTransaction implements the same method on WAF. +func (w wafWrapper) NewTransactionWithOptions(opts ...experimental.Option) types.Transaction { + o := corazawaf.Options{} + for _, opt := range opts { + opt(&o) + } + return w.waf.NewTransactionWithOptions(o) }