|
@@ -2,10 +2,13 @@ package wafacquisition
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
+ "encoding/json"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"io/ioutil"
|
|
|
"net/http"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
|
|
|
"github.com/corazawaf/coraza/v3"
|
|
|
corazatypes "github.com/corazawaf/coraza/v3/types"
|
|
@@ -13,6 +16,8 @@ import (
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/waf"
|
|
|
"github.com/crowdsecurity/go-cs-lib/pkg/trace"
|
|
|
+ "github.com/davecgh/go-spew/spew"
|
|
|
+ "github.com/google/uuid"
|
|
|
"github.com/pkg/errors"
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
log "github.com/sirupsen/logrus"
|
|
@@ -140,6 +145,8 @@ func (w *WafSource) Configure(yamlConfig []byte, logger *log.Entry) error {
|
|
|
return errors.Wrap(err, "Cannot create WAF")
|
|
|
}
|
|
|
w.outOfBandWaf = outofbandwaf
|
|
|
+ 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
|
|
|
w.mux.HandleFunc(w.config.Path, w.wafHandler)
|
|
@@ -195,12 +202,13 @@ func (w *WafSource) Dump() interface{} {
|
|
|
return w
|
|
|
}
|
|
|
|
|
|
-func processReqWithEngine(waf coraza.WAF, r *http.Request) (*corazatypes.Interruption, error) {
|
|
|
- tx := waf.NewTransaction()
|
|
|
+func processReqWithEngine(waf coraza.WAF, r *http.Request, uuid string) (*corazatypes.Interruption, corazatypes.Transaction, error) {
|
|
|
+ var in *corazatypes.Interruption
|
|
|
+ tx := waf.NewTransactionWithID(uuid)
|
|
|
|
|
|
if tx.IsRuleEngineOff() {
|
|
|
log.Printf("engine is off")
|
|
|
- return nil, nil
|
|
|
+ return nil, nil, nil
|
|
|
}
|
|
|
|
|
|
defer func() {
|
|
@@ -236,63 +244,158 @@ func processReqWithEngine(waf coraza.WAF, r *http.Request) (*corazatypes.Interru
|
|
|
tx.AddRequestHeader("Transfer-Encoding", r.TransferEncoding[0])
|
|
|
}
|
|
|
|
|
|
- in := tx.ProcessRequestHeaders()
|
|
|
+ in = tx.ProcessRequestHeaders()
|
|
|
+ //if we're inband, we should stop here, but for outofband go to the end
|
|
|
if in != nil {
|
|
|
log.Printf("headerss")
|
|
|
- return in, nil
|
|
|
+ return in, tx, nil
|
|
|
}
|
|
|
|
|
|
if tx.IsRequestBodyAccessible() {
|
|
|
if r.Body != nil && r.Body != http.NoBody {
|
|
|
_, _, err := tx.ReadRequestBodyFrom(r.Body)
|
|
|
if err != nil {
|
|
|
- return 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, errors.Wrap(err, "Cannot read request body")
|
|
|
+ return nil, nil, errors.Wrap(err, "Cannot read request body")
|
|
|
|
|
|
}
|
|
|
body := io.MultiReader(bodyReader, r.Body)
|
|
|
r.Body = ioutil.NopCloser(body)
|
|
|
in, err = tx.ProcessRequestBody()
|
|
|
if err != nil {
|
|
|
- return nil, errors.Wrap(err, "Cannot process request body")
|
|
|
+ return nil, nil, errors.Wrap(err, "Cannot process request body")
|
|
|
|
|
|
}
|
|
|
if in != nil {
|
|
|
- log.Printf("nothing here")
|
|
|
- return in, nil
|
|
|
+ log.Printf("exception while processing body")
|
|
|
+ return in, tx, nil
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- log.Printf("done")
|
|
|
+ log.Printf("done -> %d", len(tx.MatchedRules()))
|
|
|
+ // if in != nil {
|
|
|
+ // log.Printf("exception while processing req")
|
|
|
+ // return in, tx, nil
|
|
|
+ // }
|
|
|
+ return nil, tx, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (w *WafSource) TxToEvents(tx corazatypes.Transaction, r *http.Request, kind string) ([]types.Event, error) {
|
|
|
+ evts := []types.Event{}
|
|
|
+ if tx == nil {
|
|
|
+ return nil, fmt.Errorf("tx is nil")
|
|
|
+ }
|
|
|
+ for idx, rule := range tx.MatchedRules() {
|
|
|
+ log.Printf("rule %d", idx)
|
|
|
+ evt, err := w.RuleMatchToEvent(rule, tx, r, kind)
|
|
|
+ if err != nil {
|
|
|
+ return nil, errors.Wrap(err, "Cannot convert rule match to event")
|
|
|
+ }
|
|
|
+ evts = append(evts, evt)
|
|
|
+ }
|
|
|
+
|
|
|
+ return evts, nil
|
|
|
+}
|
|
|
+
|
|
|
+// 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) {
|
|
|
+ evt := types.Event{}
|
|
|
+ //we might want to change this based on in-band vs out-of-band ?
|
|
|
+ evt.Type = types.LOG
|
|
|
+ evt.ExpectMode = types.LIVE
|
|
|
+ //def needs fixing
|
|
|
+ evt.Stage = "s00-raw"
|
|
|
+ evt.Process = true
|
|
|
+
|
|
|
+ //we build a big-ass object that is going to be marshaled in line.raw and unmarshaled later.
|
|
|
+ //why ? because it's more consistent with the other data-sources etc. and it provides users with flexibility to alter our parsers
|
|
|
+ CorazaEvent := map[string]interface{}{
|
|
|
+ //core rule info
|
|
|
+ "rule_type": kind,
|
|
|
+ "rule_id": rule.Rule().ID(),
|
|
|
+ //"rule_action": tx.Interruption().Action,
|
|
|
+ "rule_disruptive": rule.Disruptive(),
|
|
|
+ "rule_tags": rule.Rule().Tags(),
|
|
|
+ "rule_file": rule.Rule().File(),
|
|
|
+ "rule_file_line": rule.Rule().Line(),
|
|
|
+ "rule_revision": rule.Rule().Revision(),
|
|
|
+ "rule_secmark": rule.Rule().SecMark(),
|
|
|
+ "rule_accuracy": rule.Rule().Accuracy(),
|
|
|
+
|
|
|
+ //http contextual infos
|
|
|
+ "upstream_addr": r.RemoteAddr,
|
|
|
+ "req_uuid": tx.ID(),
|
|
|
+ "source_ip": strings.Split(rule.ClientIPAddress(), ":")[0],
|
|
|
+ "uri": rule.URI(),
|
|
|
+ }
|
|
|
|
|
|
- return nil, nil
|
|
|
+ if tx.Interruption() != nil {
|
|
|
+ CorazaEvent["rule_action"] = tx.Interruption().Action
|
|
|
+ }
|
|
|
+ corazaEventB, err := json.Marshal(CorazaEvent)
|
|
|
+ if err != nil {
|
|
|
+ return evt, fmt.Errorf("Unable to marshal coraza alert: %w", err)
|
|
|
+ }
|
|
|
+ evt.Line = types.Line{
|
|
|
+ Time: time.Now(),
|
|
|
+ //should we add some info like listen addr/port/path ?
|
|
|
+ Labels: map[string]string{"type": "waf"},
|
|
|
+ Process: true,
|
|
|
+ Module: "waf",
|
|
|
+ Src: "waf",
|
|
|
+ Raw: string(corazaEventB),
|
|
|
+ }
|
|
|
+
|
|
|
+ return evt, nil
|
|
|
}
|
|
|
|
|
|
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
|
|
|
+ uuid := uuid.New().String()
|
|
|
//inband first
|
|
|
- in, err := processReqWithEngine(w.inBandWaf, r)
|
|
|
+ in, tx, err := processReqWithEngine(w.inBandWaf, r, uuid)
|
|
|
if err != nil { //things went south
|
|
|
log.Errorf("Error while processing request : %s", err)
|
|
|
rw.WriteHeader(http.StatusForbidden)
|
|
|
return
|
|
|
}
|
|
|
if in != nil {
|
|
|
- log.Infof("Request blocked by WAF : %+v", in)
|
|
|
+ events, err := w.TxToEvents(tx, r, "inband")
|
|
|
+ log.Infof("Request blocked by WAF, %d events to send", len(events))
|
|
|
+ for _, evt := range events {
|
|
|
+ w.outChan <- evt
|
|
|
+ }
|
|
|
+ log.Infof("done")
|
|
|
+ if err != nil {
|
|
|
+ log.Errorf("Cannot convert transaction to events : %s", err)
|
|
|
+ rw.WriteHeader(http.StatusForbidden)
|
|
|
+ return
|
|
|
+ }
|
|
|
rw.WriteHeader(http.StatusForbidden)
|
|
|
return
|
|
|
}
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
//Now we can do out of band
|
|
|
- in2, err := processReqWithEngine(w.outOfBandWaf, r)
|
|
|
+ in2, tx2, err := processReqWithEngine(w.outOfBandWaf, r, uuid)
|
|
|
if err != nil { //things went south
|
|
|
log.Errorf("Error while processing request : %s", err)
|
|
|
return
|
|
|
}
|
|
|
- if in2 != nil {
|
|
|
+ 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
|
|
|
}
|