|
@@ -4,19 +4,17 @@ import (
|
|
"context"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/json"
|
|
"fmt"
|
|
"fmt"
|
|
- "io"
|
|
|
|
"io/ioutil"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http"
|
|
|
|
+ "net/url"
|
|
"strings"
|
|
"strings"
|
|
"time"
|
|
"time"
|
|
|
|
|
|
"github.com/corazawaf/coraza/v3"
|
|
"github.com/corazawaf/coraza/v3"
|
|
- "github.com/corazawaf/coraza/v3/experimental"
|
|
|
|
corazatypes "github.com/corazawaf/coraza/v3/types"
|
|
corazatypes "github.com/corazawaf/coraza/v3/types"
|
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
"github.com/crowdsecurity/go-cs-lib/pkg/trace"
|
|
"github.com/crowdsecurity/go-cs-lib/pkg/trace"
|
|
- "github.com/davecgh/go-spew/spew"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
@@ -25,13 +23,20 @@ import (
|
|
"gopkg.in/yaml.v2"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+const (
|
|
|
|
+ InBand = "INBAND"
|
|
|
|
+ OutOfBand = "OUTOFBAND"
|
|
|
|
+)
|
|
|
|
+
|
|
type WafSource struct {
|
|
type WafSource struct {
|
|
- config WafSourceConfig
|
|
|
|
- logger *log.Entry
|
|
|
|
- mux *http.ServeMux
|
|
|
|
- server *http.Server
|
|
|
|
- addr string
|
|
|
|
- outChan chan types.Event
|
|
|
|
|
|
+ config WafSourceConfig
|
|
|
|
+ logger *log.Entry
|
|
|
|
+ mux *http.ServeMux
|
|
|
|
+ server *http.Server
|
|
|
|
+ addr string
|
|
|
|
+ outChan chan types.Event
|
|
|
|
+ OutOfBandChan chan ParsedRequest
|
|
|
|
+
|
|
inBandWaf coraza.WAF
|
|
inBandWaf coraza.WAF
|
|
outOfBandWaf coraza.WAF
|
|
outOfBandWaf coraza.WAF
|
|
}
|
|
}
|
|
@@ -146,8 +151,8 @@ func (w *WafSource) Configure(yamlConfig []byte, logger *log.Entry) error {
|
|
return errors.Wrap(err, "Cannot create WAF")
|
|
return errors.Wrap(err, "Cannot create WAF")
|
|
}
|
|
}
|
|
w.outOfBandWaf = outofbandwaf
|
|
w.outOfBandWaf = outofbandwaf
|
|
- log.Printf("OOB -> %s", spew.Sdump(w.outOfBandWaf))
|
|
|
|
- log.Printf("IB -> %s", spew.Sdump(w.inBandWaf))
|
|
|
|
|
|
+ //log.Printf("OOB -> %s", spew.Sdump(w.outOfBandWaf))
|
|
|
|
+ //log.Printf("IB -> %s", spew.Sdump(w.inBandWaf))
|
|
|
|
|
|
//We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
|
|
//We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
|
|
w.mux.HandleFunc(w.config.Path, w.wafHandler)
|
|
w.mux.HandleFunc(w.config.Path, w.wafHandler)
|
|
@@ -173,8 +178,18 @@ func (w *WafSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error
|
|
|
|
|
|
func (w *WafSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
|
|
func (w *WafSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
|
|
w.outChan = out
|
|
w.outChan = out
|
|
|
|
+ w.OutOfBandChan = make(chan ParsedRequest)
|
|
t.Go(func() error {
|
|
t.Go(func() error {
|
|
defer trace.CatchPanic("crowdsec/acquis/waf/live")
|
|
defer trace.CatchPanic("crowdsec/acquis/waf/live")
|
|
|
|
+
|
|
|
|
+ // start outOfBand GoRoutine
|
|
|
|
+ t.Go(func() error {
|
|
|
|
+ if err := w.ProcessOutBand(t); err != nil {
|
|
|
|
+ return errors.Wrap(err, "Processing Out of band routine failed: %s")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ })
|
|
|
|
+
|
|
w.logger.Infof("Starting WAF server on %s:%d%s", w.config.ListenAddr, w.config.ListenPort, w.config.Path)
|
|
w.logger.Infof("Starting WAF server on %s:%d%s", w.config.ListenAddr, w.config.ListenPort, w.config.Path)
|
|
t.Go(func() error {
|
|
t.Go(func() error {
|
|
err := w.server.ListenAndServe()
|
|
err := w.server.ListenAndServe()
|
|
@@ -203,7 +218,41 @@ func (w *WafSource) Dump() interface{} {
|
|
return w
|
|
return w
|
|
}
|
|
}
|
|
|
|
|
|
-func processReqWithEngine(waf coraza.WAF, r *http.Request, uuid string) (*corazatypes.Interruption, corazatypes.Transaction, error) {
|
|
|
|
|
|
+type ParsedRequest struct {
|
|
|
|
+ RemoteAddr string
|
|
|
|
+ Host string
|
|
|
|
+ Headers http.Header
|
|
|
|
+ URL *url.URL
|
|
|
|
+ Method string
|
|
|
|
+ Proto string
|
|
|
|
+ Body []byte
|
|
|
|
+ TransferEncoding []string
|
|
|
|
+ UUID string
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func NewParsedRequestFromRequest(r *http.Request) (ParsedRequest, error) {
|
|
|
|
+ var body []byte
|
|
|
|
+ var err error
|
|
|
|
+ if r.Body != nil {
|
|
|
|
+ body = make([]byte, 0)
|
|
|
|
+ body, err = ioutil.ReadAll(r.Body)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return ParsedRequest{}, fmt.Errorf("unable to read body: %s", err)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return ParsedRequest{
|
|
|
|
+ RemoteAddr: r.RemoteAddr,
|
|
|
|
+ Host: r.Host,
|
|
|
|
+ Headers: r.Header,
|
|
|
|
+ URL: r.URL,
|
|
|
|
+ Method: r.Method,
|
|
|
|
+ Proto: r.Proto,
|
|
|
|
+ Body: body,
|
|
|
|
+ TransferEncoding: r.TransferEncoding,
|
|
|
|
+ }, nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func processReqWithEngine(waf coraza.WAF, r ParsedRequest, uuid string, wafType string) (*corazatypes.Interruption, corazatypes.Transaction, error) {
|
|
var in *corazatypes.Interruption
|
|
var in *corazatypes.Interruption
|
|
tx := waf.NewTransactionWithID(uuid)
|
|
tx := waf.NewTransactionWithID(uuid)
|
|
|
|
|
|
@@ -219,17 +268,15 @@ func processReqWithEngine(waf coraza.WAF, r *http.Request, uuid string) (*coraza
|
|
|
|
|
|
//this method is not exported by coraza, so we have to do it ourselves.
|
|
//this method is not exported by coraza, so we have to do it ourselves.
|
|
//ideally, this would be dealt with by expr code, and we provide helpers to manipulate the transaction object?\
|
|
//ideally, this would be dealt with by expr code, and we provide helpers to manipulate the transaction object?\
|
|
- var txx experimental.FullTransaction
|
|
|
|
-
|
|
|
|
- //txx := experimental.ToFullInterface(tx)
|
|
|
|
- txx = tx.(experimental.FullTransaction)
|
|
|
|
- txx.RemoveRuleByID(1)
|
|
|
|
|
|
+ //var txx experimental.FullTransaction
|
|
|
|
+ //txx = tx.(experimental.FullTransaction)
|
|
|
|
+ //txx.RemoveRuleByID(1)
|
|
|
|
|
|
tx.ProcessConnection(r.RemoteAddr, 0, "", 0)
|
|
tx.ProcessConnection(r.RemoteAddr, 0, "", 0)
|
|
|
|
|
|
tx.ProcessURI(r.URL.String(), r.Method, r.Proto) //FIXME: get it from the headers
|
|
tx.ProcessURI(r.URL.String(), r.Method, r.Proto) //FIXME: get it from the headers
|
|
|
|
|
|
- for k, vr := range r.Header {
|
|
|
|
|
|
+ for k, vr := range r.Headers {
|
|
for _, v := range vr {
|
|
for _, v := range vr {
|
|
tx.AddRequestHeader(k, v)
|
|
tx.AddRequestHeader(k, v)
|
|
}
|
|
}
|
|
@@ -247,44 +294,49 @@ func processReqWithEngine(waf coraza.WAF, r *http.Request, uuid string) (*coraza
|
|
|
|
|
|
in = tx.ProcessRequestHeaders()
|
|
in = tx.ProcessRequestHeaders()
|
|
//if we're inband, we should stop here, but for outofband go to the end
|
|
//if we're inband, we should stop here, but for outofband go to the end
|
|
- if in != nil {
|
|
|
|
- log.Printf("headerss")
|
|
|
|
|
|
+ if in != nil && wafType == InBand {
|
|
return in, tx, nil
|
|
return in, tx, nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ ct := r.Headers.Get("content-type")
|
|
|
|
+
|
|
if tx.IsRequestBodyAccessible() {
|
|
if tx.IsRequestBodyAccessible() {
|
|
- if r.Body != nil && r.Body != http.NoBody {
|
|
|
|
- _, _, err := tx.ReadRequestBodyFrom(r.Body)
|
|
|
|
|
|
+ if r.Body != nil && len(r.Body) != 0 {
|
|
|
|
+ it, _, err := tx.WriteRequestBody(r.Body)
|
|
if err != nil {
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "Cannot read request body")
|
|
return nil, nil, errors.Wrap(err, "Cannot read request body")
|
|
}
|
|
}
|
|
- bodyReader, err := tx.RequestBodyReader()
|
|
|
|
- if err != nil {
|
|
|
|
- return nil, nil, errors.Wrap(err, "Cannot read request body")
|
|
|
|
|
|
|
|
|
|
+ if it != nil {
|
|
|
|
+ return it, nil, nil
|
|
}
|
|
}
|
|
- body := io.MultiReader(bodyReader, r.Body)
|
|
|
|
- r.Body = ioutil.NopCloser(body)
|
|
|
|
|
|
+ // from https://github.com/corazawaf/coraza/blob/main/internal/corazawaf/transaction.go#L419
|
|
|
|
+ // urlencoded cannot end with CRLF
|
|
|
|
+ if ct != "application/x-www-form-urlencoded" {
|
|
|
|
+ it, _, err := tx.WriteRequestBody([]byte{'\r', '\n'})
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, nil, fmt.Errorf("cannot write to request body to buffer: %s", err.Error())
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if it != nil {
|
|
|
|
+ return it, nil, nil
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
in, err = tx.ProcessRequestBody()
|
|
in, err = tx.ProcessRequestBody()
|
|
if err != nil {
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "Cannot process request body")
|
|
return nil, nil, errors.Wrap(err, "Cannot process request body")
|
|
|
|
|
|
}
|
|
}
|
|
- if in != nil {
|
|
|
|
- log.Printf("exception while processing body")
|
|
|
|
|
|
+ if in != nil && wafType == InBand {
|
|
return in, tx, nil
|
|
return in, tx, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- log.Printf("done -> %d", len(tx.MatchedRules()))
|
|
|
|
- // if in != nil {
|
|
|
|
- // log.Printf("exception while processing req")
|
|
|
|
- // return in, tx, nil
|
|
|
|
- // }
|
|
|
|
return nil, tx, nil
|
|
return nil, tx, nil
|
|
}
|
|
}
|
|
|
|
|
|
-func (w *WafSource) TxToEvents(tx corazatypes.Transaction, r *http.Request, kind string) ([]types.Event, error) {
|
|
|
|
|
|
+func (w *WafSource) TxToEvents(tx corazatypes.Transaction, r ParsedRequest, kind string) ([]types.Event, error) {
|
|
evts := []types.Event{}
|
|
evts := []types.Event{}
|
|
if tx == nil {
|
|
if tx == nil {
|
|
return nil, fmt.Errorf("tx is nil")
|
|
return nil, fmt.Errorf("tx is nil")
|
|
@@ -302,7 +354,7 @@ func (w *WafSource) TxToEvents(tx corazatypes.Transaction, r *http.Request, kind
|
|
}
|
|
}
|
|
|
|
|
|
// Transforms a coraza interruption to a crowdsec event
|
|
// Transforms a coraza interruption to a crowdsec event
|
|
-func (w *WafSource) RuleMatchToEvent(rule corazatypes.MatchedRule, tx corazatypes.Transaction, r *http.Request, kind string) (types.Event, error) {
|
|
|
|
|
|
+func (w *WafSource) RuleMatchToEvent(rule corazatypes.MatchedRule, tx corazatypes.Transaction, r ParsedRequest, kind string) (types.Event, error) {
|
|
evt := types.Event{}
|
|
evt := types.Event{}
|
|
//we might want to change this based on in-band vs out-of-band ?
|
|
//we might want to change this based on in-band vs out-of-band ?
|
|
evt.Type = types.LOG
|
|
evt.Type = types.LOG
|
|
@@ -353,51 +405,74 @@ func (w *WafSource) RuleMatchToEvent(rule corazatypes.MatchedRule, tx corazatype
|
|
return evt, nil
|
|
return evt, nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+func (w *WafSource) ProcessOutBand(t *tomb.Tomb) error {
|
|
|
|
+ for {
|
|
|
|
+ select {
|
|
|
|
+ case <-t.Dying():
|
|
|
|
+ log.Infof("OutOfBand function is dying")
|
|
|
|
+ return nil
|
|
|
|
+ case r := <-w.OutOfBandChan:
|
|
|
|
+ in2, tx2, err := processReqWithEngine(w.outOfBandWaf, r, r.UUID, OutOfBand)
|
|
|
|
+ if err != nil { //things went south
|
|
|
|
+ log.Errorf("Error while processing request : %s", err)
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ if tx2 != nil && len(tx2.MatchedRules()) > 0 {
|
|
|
|
+ events, err := w.TxToEvents(tx2, r, OutOfBand)
|
|
|
|
+ log.Infof("Request triggered by WAF, %d events to send", len(events))
|
|
|
|
+ for _, evt := range events {
|
|
|
|
+ w.outChan <- evt
|
|
|
|
+ }
|
|
|
|
+ if err != nil {
|
|
|
|
+ log.Errorf("Cannot convert transaction to events : %s", err)
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ log.Infof("WAF triggered : %+v", in2)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
func (w *WafSource) wafHandler(rw http.ResponseWriter, r *http.Request) {
|
|
func (w *WafSource) wafHandler(rw http.ResponseWriter, r *http.Request) {
|
|
- log.Printf("yolo here %v", r)
|
|
|
|
//let's gen a transaction id to keep consistance accross in-band and out-of-band
|
|
//let's gen a transaction id to keep consistance accross in-band and out-of-band
|
|
uuid := uuid.New().String()
|
|
uuid := uuid.New().String()
|
|
|
|
+
|
|
|
|
+ // parse the request only once
|
|
|
|
+ parsedRequest, err := NewParsedRequestFromRequest(r)
|
|
|
|
+ if err != nil {
|
|
|
|
+ log.Errorf("%s", err)
|
|
|
|
+ rw.WriteHeader(http.StatusForbidden)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
//inband first
|
|
//inband first
|
|
- in, tx, err := processReqWithEngine(w.inBandWaf, r, uuid)
|
|
|
|
|
|
+ in, tx, err := processReqWithEngine(w.inBandWaf, parsedRequest, uuid, InBand)
|
|
if err != nil { //things went south
|
|
if err != nil { //things went south
|
|
log.Errorf("Error while processing request : %s", err)
|
|
log.Errorf("Error while processing request : %s", err)
|
|
- rw.WriteHeader(http.StatusForbidden)
|
|
|
|
|
|
+ rw.WriteHeader(http.StatusForbidden) // do we want to return 403 is smth went wrong ?
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
+
|
|
if in != nil {
|
|
if in != nil {
|
|
- events, err := w.TxToEvents(tx, r, "inband")
|
|
|
|
|
|
+ rw.WriteHeader(http.StatusForbidden)
|
|
|
|
+ events, err := w.TxToEvents(tx, parsedRequest, InBand)
|
|
log.Infof("Request blocked by WAF, %d events to send", len(events))
|
|
log.Infof("Request blocked by WAF, %d events to send", len(events))
|
|
for _, evt := range events {
|
|
for _, evt := range events {
|
|
w.outChan <- evt
|
|
w.outChan <- evt
|
|
}
|
|
}
|
|
- log.Infof("done")
|
|
|
|
if err != nil {
|
|
if err != nil {
|
|
log.Errorf("Cannot convert transaction to events : %s", err)
|
|
log.Errorf("Cannot convert transaction to events : %s", err)
|
|
- rw.WriteHeader(http.StatusForbidden)
|
|
|
|
return
|
|
return
|
|
}
|
|
}
|
|
- rw.WriteHeader(http.StatusForbidden)
|
|
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // we finished the inband, we can return 200
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.WriteHeader(http.StatusOK)
|
|
- //Now we can do out of band
|
|
|
|
- in2, tx2, err := processReqWithEngine(w.outOfBandWaf, r, uuid)
|
|
|
|
- if err != nil { //things went south
|
|
|
|
- log.Errorf("Error while processing request : %s", err)
|
|
|
|
- return
|
|
|
|
- }
|
|
|
|
- if tx2 != nil && len(tx2.MatchedRules()) > 0 {
|
|
|
|
- log.Printf("got events and stuff to do")
|
|
|
|
- events, err := w.TxToEvents(tx2, r, "outofband")
|
|
|
|
- log.Infof("Request triggered by WAF, %d events to send", len(events))
|
|
|
|
- for _, evt := range events {
|
|
|
|
- w.outChan <- evt
|
|
|
|
- }
|
|
|
|
- if err != nil {
|
|
|
|
- log.Errorf("Cannot convert transaction to events : %s", err)
|
|
|
|
- }
|
|
|
|
- log.Infof("done")
|
|
|
|
- log.Infof("WAF triggered : %+v", in2)
|
|
|
|
- return
|
|
|
|
- }
|
|
|
|
|
|
+
|
|
|
|
+ // now we can process out of band asynchronously
|
|
|
|
+ go func() {
|
|
|
|
+ w.OutOfBandChan <- parsedRequest
|
|
|
|
+ }()
|
|
|
|
+
|
|
}
|
|
}
|