add request dumper with filters
This commit is contained in:
parent
410e36e6a3
commit
17cfc9909e
|
@ -1,11 +1,14 @@
|
||||||
package waf
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -19,68 +22,248 @@ const (
|
||||||
APIKeyHeaderName = "X-Crowdsec-Waap-Api-Key"
|
APIKeyHeaderName = "X-Crowdsec-Waap-Api-Key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type ResponseRequest struct {
|
|
||||||
// UUID string
|
|
||||||
// Tx corazatypes.Transaction
|
|
||||||
// Interruption *corazatypes.Interruption
|
|
||||||
// Err error
|
|
||||||
// SendEvents bool
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func NewResponseRequest(Tx experimental.FullTransaction, in *corazatypes.Interruption, UUID string, err error) ResponseRequest {
|
|
||||||
// return ResponseRequest{
|
|
||||||
// UUID: UUID,
|
|
||||||
// Tx: Tx,
|
|
||||||
// Interruption: in,
|
|
||||||
// Err: err,
|
|
||||||
// SendEvents: true,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *ResponseRequest) SetRemediation(remediation string) error {
|
|
||||||
// if r.Interruption == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// r.Interruption.Action = remediation
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *ResponseRequest) SetRemediationByID(ID int, remediation string) error {
|
|
||||||
// if r.Interruption == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// if r.Interruption.RuleID == ID {
|
|
||||||
// r.Interruption.Action = remediation
|
|
||||||
// }
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (r *ResponseRequest) CancelEvent() error {
|
|
||||||
// // true by default
|
|
||||||
// r.SendEvents = false
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
type ParsedRequest struct {
|
type ParsedRequest struct {
|
||||||
RemoteAddr string
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||||
Host string
|
Host string `json:"host,omitempty"`
|
||||||
ClientIP string
|
ClientIP string `json:"client_ip,omitempty"`
|
||||||
URI string
|
URI string `json:"uri,omitempty"`
|
||||||
Args url.Values
|
Args url.Values `json:"args,omitempty"`
|
||||||
ClientHost string
|
ClientHost string `json:"client_host,omitempty"`
|
||||||
Headers http.Header
|
Headers http.Header `json:"headers,omitempty"`
|
||||||
URL *url.URL
|
URL *url.URL `json:"url,omitempty"`
|
||||||
Method string
|
Method string `json:"method,omitempty"`
|
||||||
Proto string
|
Proto string `json:"proto,omitempty"`
|
||||||
Body []byte
|
Body []byte `json:"body,omitempty"`
|
||||||
TransferEncoding []string
|
TransferEncoding []string `json:"transfer_encoding,omitempty"`
|
||||||
UUID string
|
UUID string `json:"uuid,omitempty"`
|
||||||
Tx ExtendedTransaction
|
Tx ExtendedTransaction `json:"transaction,omitempty"`
|
||||||
ResponseChannel chan WaapTempResponse
|
ResponseChannel chan WaapTempResponse `json:"-"`
|
||||||
IsInBand bool
|
IsInBand bool `json:"-"`
|
||||||
IsOutBand bool
|
IsOutBand bool `json:"-"`
|
||||||
WaapEngine string
|
WaapEngine string `json:"waap_engine,omitempty"`
|
||||||
RemoteAddrNormalized string
|
RemoteAddrNormalized string `json:"normalized_remote_addr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqDumpFilter struct {
|
||||||
|
req *ParsedRequest
|
||||||
|
HeadersContentFilters []string
|
||||||
|
HeadersNameFilters []string
|
||||||
|
HeadersDrop bool
|
||||||
|
|
||||||
|
BodyDrop bool
|
||||||
|
//BodyContentFilters []string TBD
|
||||||
|
|
||||||
|
ArgsContentFilters []string
|
||||||
|
ArgsNameFilters []string
|
||||||
|
ArgsDrop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ParsedRequest) DumpRequest(params ...any) *ReqDumpFilter {
|
||||||
|
filter := ReqDumpFilter{}
|
||||||
|
filter.BodyDrop = true
|
||||||
|
filter.HeadersNameFilters = []string{"cookie", "authorization"}
|
||||||
|
filter.req = r
|
||||||
|
return &filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear filters
|
||||||
|
func (r *ReqDumpFilter) NoFilters() *ReqDumpFilter {
|
||||||
|
r2 := ReqDumpFilter{}
|
||||||
|
r2.req = r.req
|
||||||
|
return &r2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithEmptyHeadersFilters() *ReqDumpFilter {
|
||||||
|
r.HeadersContentFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeadersContentFilters(filter string) *ReqDumpFilter {
|
||||||
|
r.HeadersContentFilters = append(r.HeadersContentFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeadersNameFilter(filter string) *ReqDumpFilter {
|
||||||
|
r.HeadersNameFilters = append(r.HeadersNameFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithNoHeaders() *ReqDumpFilter {
|
||||||
|
r.HeadersDrop = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithHeaders() *ReqDumpFilter {
|
||||||
|
r.HeadersDrop = false
|
||||||
|
r.HeadersNameFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithBody() *ReqDumpFilter {
|
||||||
|
r.BodyDrop = false
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithNoBody() *ReqDumpFilter {
|
||||||
|
r.BodyDrop = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithEmptyArgsFilters() *ReqDumpFilter {
|
||||||
|
r.ArgsContentFilters = []string{}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithArgsContentFilters(filter string) *ReqDumpFilter {
|
||||||
|
r.ArgsContentFilters = append(r.ArgsContentFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) WithArgsNameFilter(filter string) *ReqDumpFilter {
|
||||||
|
r.ArgsNameFilters = append(r.ArgsNameFilters, filter)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterBody(out *ParsedRequest) error {
|
||||||
|
if r.BodyDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out.Body = r.req.Body
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterArgs(out *ParsedRequest) error {
|
||||||
|
if r.ArgsDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(r.ArgsContentFilters) == 0 && len(r.ArgsNameFilters) == 0 {
|
||||||
|
out.Args = r.req.Args
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out.Args = make(url.Values)
|
||||||
|
for k, vals := range r.req.Args {
|
||||||
|
reject := false
|
||||||
|
//exclude by match on name
|
||||||
|
for _, filter := range r.ArgsNameFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, k)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
//exclude by content
|
||||||
|
for _, filter := range r.ArgsContentFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, v)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if it was not rejected, let's add it
|
||||||
|
if !reject {
|
||||||
|
out.Args[k] = vals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) FilterHeaders(out *ParsedRequest) error {
|
||||||
|
if r.HeadersDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.HeadersContentFilters) == 0 && len(r.HeadersNameFilters) == 0 {
|
||||||
|
out.Headers = r.req.Headers
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Headers = make(http.Header)
|
||||||
|
for k, vals := range r.req.Headers {
|
||||||
|
reject := false
|
||||||
|
//exclude by match on name
|
||||||
|
for _, filter := range r.HeadersNameFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, k)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vals {
|
||||||
|
//exclude by content
|
||||||
|
for _, filter := range r.HeadersContentFilters {
|
||||||
|
ok, err := regexp.MatchString("(?i)"+filter, v)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
reject = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if it was not rejected, let's add it
|
||||||
|
if !reject {
|
||||||
|
out.Headers[k] = vals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) GetFilteredRequest() *ParsedRequest {
|
||||||
|
//if there are no filters, we return the original request
|
||||||
|
if len(r.HeadersContentFilters) == 0 &&
|
||||||
|
len(r.HeadersNameFilters) == 0 &&
|
||||||
|
len(r.ArgsContentFilters) == 0 &&
|
||||||
|
len(r.ArgsNameFilters) == 0 &&
|
||||||
|
!r.BodyDrop && !r.HeadersDrop && !r.ArgsDrop {
|
||||||
|
log.Warningf("no filters, returning original request")
|
||||||
|
return r.req
|
||||||
|
}
|
||||||
|
|
||||||
|
r2 := ParsedRequest{}
|
||||||
|
r.FilterHeaders(&r2)
|
||||||
|
r.FilterBody(&r2)
|
||||||
|
r.FilterArgs(&r2)
|
||||||
|
return &r2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReqDumpFilter) ToJSON() error {
|
||||||
|
fd, err := os.CreateTemp("/tmp/", "crowdsec_req_dump_*.json")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while creating temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
enc := json.NewEncoder(fd)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
req := r.GetFilteredRequest()
|
||||||
|
|
||||||
|
log.Warningf("dumping : %+v", req)
|
||||||
|
|
||||||
|
if err := enc.Encode(req); err != nil {
|
||||||
|
return fmt.Errorf("while encoding request: %w", err)
|
||||||
|
}
|
||||||
|
log.Warningf("request dumped to %s", fd.Name())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a ParsedRequest from a http.Request. ParsedRequest can be consumed by the Waap Engine
|
// Generate a ParsedRequest from a http.Request. ParsedRequest can be consumed by the Waap Engine
|
||||||
|
|
181
pkg/waf/request_test.go
Normal file
181
pkg/waf/request_test.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
package waf
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBodyDumper(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *ParsedRequest
|
||||||
|
expect *ParsedRequest
|
||||||
|
filter func(r *ReqDumpFilter) *ReqDumpFilter
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default filter (cookie+authorization stripped + no body)",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit empty filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte("yo some body"),
|
||||||
|
Headers: map[string][]string{"cookie": {"toto"}, "authorization": {"tata"}, "foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.NoFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter header",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}, "test2": {"tata"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithNoBody().WithHeadersNameFilter("test2")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter header content",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}, "test2": {"tata"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"test1": {"toto"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithHeadersContentFilters("tata")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with headers",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"cookie1": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"cookie1": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithHeaders()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "drop headers",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte{},
|
||||||
|
Headers: map[string][]string{},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithNoHeaders()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with body",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Body: []byte("toto"),
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Body: []byte("toto"),
|
||||||
|
Headers: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithBody()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with empty args filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithEmptyArgsFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with args name filter",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"toto": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithArgsNameFilter("toto")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithEmptyHeadersFilters",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"cookie": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"cookie": {"lol"}, "totolol": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithEmptyHeadersFilters()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithArgsContentFilters",
|
||||||
|
req: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"test": {"lol"}, "test2": {"toto"}},
|
||||||
|
},
|
||||||
|
expect: &ParsedRequest{
|
||||||
|
Args: map[string][]string{"test": {"lol"}},
|
||||||
|
},
|
||||||
|
filter: func(r *ReqDumpFilter) *ReqDumpFilter {
|
||||||
|
return r.WithArgsContentFilters("toto")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, test := range tests {
|
||||||
|
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
orig_dr := test.req.DumpRequest()
|
||||||
|
result := test.filter(orig_dr).GetFilteredRequest()
|
||||||
|
|
||||||
|
if len(result.Body) != len(test.expect.Body) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(test.req.Body), len(test.expect.Body))
|
||||||
|
}
|
||||||
|
if len(result.Headers) != len(test.expect.Headers) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(test.req.Headers), len(test.expect.Headers))
|
||||||
|
}
|
||||||
|
for k, v := range result.Headers {
|
||||||
|
if len(v) != len(test.expect.Headers[k]) {
|
||||||
|
t.Fatalf("test %d (%s) failed, got %d, expected %d", idx, test.name, len(v), len(test.expect.Headers[k]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue