Skip to content

Commit

Permalink
feat: move test type to its own package (#208)
Browse files Browse the repository at this point in the history
* feat: move test type to its own package
* updates ftw schema to v1.1.0

---------

Signed-off-by: Felipe Zipitria <felipe.zipitria@owasp.org>
Co-authored-by: Matteo Pace <pace.matteo96@gmail.com>
  • Loading branch information
fzipi and M4tteoP authored Nov 11, 2023
1 parent 94df95c commit a4cd40e
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 89 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module github.com/coreruleset/go-ftw

go 1.18
go 1.21

require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/coreruleset/ftw-tests-schema v1.1.0
github.com/go-logr/zerologr v1.2.3
github.com/goccy/go-yaml v1.9.2
github.com/google/uuid v1.4.0
Expand Down
11 changes: 3 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6e
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreruleset/ftw-tests-schema v1.1.0 h1:3+NYrdLE3HVmOc3nGrisRBBvY9lGjePUrV+YkT5Ay3s=
github.com/coreruleset/ftw-tests-schema v1.1.0/go.mod h1:gRd9wBxjUI85HypWRDxJzbk1JqHC4KTxl0l/Y2p9QK4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -104,14 +106,13 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tonglil/buflogr v1.0.1 h1:WXFZLKxLfqcVSmckwiMCF8jJwjIgmStJmg63YKRF1p0=
github.com/tonglil/buflogr v1.0.1/go.mod h1:yYWwvSpn/3uAaqjf6mJg/XMiAciaR0QcRJH2gJGDxNE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
Expand All @@ -120,8 +121,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -139,8 +138,6 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand All @@ -151,8 +148,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
9 changes: 5 additions & 4 deletions runner/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/google/uuid"
"github.com/rs/zerolog/log"

"github.com/coreruleset/ftw-tests-schema/types"
"github.com/coreruleset/go-ftw/check"
"github.com/coreruleset/go-ftw/config"
"github.com/coreruleset/go-ftw/ftwhttp"
Expand Down Expand Up @@ -99,7 +100,7 @@ func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) error {
if err != nil {
return err
}
if err := RunStage(runContext, ftwCheck, testCase, stage.Stage); err != nil {
if err := RunStage(runContext, ftwCheck, testCase, stage.SD); err != nil {
return err
}
}
Expand All @@ -113,11 +114,11 @@ func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) error {
// ftwCheck is the current check utility
// testCase is the test case the stage belongs to
// stage is the stage you want to run
func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase test.Test, stage test.Stage) error {
func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase types.Test, stage types.StageData) error {
stageStartTime := time.Now()
stageID := uuid.NewString()
// Apply global overrides initially
testInput := stage.Input
testInput := (test.Input)(stage.Input)
test.ApplyInputOverrides(&runContext.Config.TestOverride.Overrides, &testInput)
expectedOutput := stage.Output
expectErr := false
Expand Down Expand Up @@ -180,7 +181,7 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
}

// Set expected test output in check
ftwCheck.SetExpectTestOutput(&expectedOutput)
ftwCheck.SetExpectTestOutput((*test.Output)(&expectedOutput))

// now get the test result based on output
testResult := checkResult(ftwCheck, response, responseErr)
Expand Down
8 changes: 4 additions & 4 deletions runner/run_input_override_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (s *inputOverrideTestSuite) TestSetHostFromDestAddr() {

s.NotNil(testInput.Headers, "Header map must exist after overriding `dest_addr`")

hostHeader := testInput.Headers.Get("Host")
hostHeader := testInput.GetHeaders().Get("Host")
s.NotEqual("", hostHeader, "Host header must be set after overriding `dest_addr`")
s.Equal(overrideHost, hostHeader, "Host header must be identical to `dest_addr` after overrding `dest_addr`")
}
Expand All @@ -191,7 +191,7 @@ func (s *inputOverrideTestSuite) TestSetHostFromHostHeaderOverride() {

test.ApplyInputOverrides(&s.cfg.TestOverride.Overrides, &testInput)

hostHeader := testInput.Headers.Get("Host")
hostHeader := testInput.GetHeaders().Get("Host")
s.NotEqual("", hostHeader, "Host header must be set after overriding the `Host` header")
if hostHeader == overrideHostHeader {
s.Equal(overrideHostHeader, hostHeader, "Host header override must take precence over OverrideEmptyHostHeader")
Expand All @@ -213,7 +213,7 @@ func (s *inputOverrideTestSuite) TestSetHeaderOverridingExistingOne() {

test.ApplyInputOverrides(&s.cfg.TestOverride.Overrides, &testInput)

overriddenHeader := testInput.Headers.Get("unique_id")
overriddenHeader := testInput.GetHeaders().Get("unique_id")
s.NotEqual("", overriddenHeader, "unique_id header must be set after overriding it")
s.Equal(overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.")
}
Expand All @@ -231,7 +231,7 @@ func (s *inputOverrideTestSuite) TestApplyInputOverrides() {

test.ApplyInputOverrides(&s.cfg.TestOverride.Overrides, &testInput)

overriddenHeader := testInput.Headers.Get("unique_id")
overriddenHeader := testInput.GetHeaders().Get("unique_id")
s.NotEqual("", overriddenHeader, "unique_id header must be set after overriding it")
s.Equal(overrideHeaderValue, overriddenHeader, "Host header must be identical to overridden `Host` header.")
}
Expand Down
22 changes: 22 additions & 0 deletions test/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ package test
import (
"encoding/base64"

schema "github.com/coreruleset/ftw-tests-schema/types"
"github.com/coreruleset/go-ftw/ftwhttp"
"github.com/coreruleset/go-ftw/utils"
)

type Input schema.Input
type Output schema.Output
type FTWTest schema.FTWTest

// GetMethod returns the proper semantic when the field is empty
func (i *Input) GetMethod() string {
if i.Method == nil {
Expand Down Expand Up @@ -57,6 +63,14 @@ func (i *Input) GetPort() int {
return *i.Port
}

// GetHeaders returns the headers wrapped in a ftwhttp.Header
func (i *Input) GetHeaders() ftwhttp.Header {
if i.Headers == nil {
return ftwhttp.Header{}
}
return ftwhttp.Header(i.Headers)
}

// GetRawRequest returns the proper raw data, and error if there was none
func (i *Input) GetRawRequest() ([]byte, error) {
if utils.IsNotEmpty(i.EncodedRequest) {
Expand All @@ -68,3 +82,11 @@ func (i *Input) GetRawRequest() ([]byte, error) {
}
return nil, nil
}

// GetAutocompleteHeaders returns the autocompleteHeaders value, defaults to true
func (i *Input) GetAutocompleteHeaders() bool {
if i.AutocompleteHeaders == nil {
return true
}
return *i.AutocompleteHeaders
}
73 changes: 10 additions & 63 deletions test/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,10 @@
package test

import (
"github.com/coreruleset/ftw-tests-schema/types"
"github.com/coreruleset/go-ftw/ftwhttp"
)

// Input represents the input request in a stage
// The fields `Version`, `Method` and `URI` we want to explicitly know when they are set to ""

type Input struct {
DestAddr *string `yaml:"dest_addr,omitempty"`
Port *int `yaml:"port,omitempty"`
Protocol *string `yaml:"protocol,omitempty"`
URI *string `yaml:"uri,omitempty"`
Version *string `yaml:"version,omitempty"`
Headers ftwhttp.Header `yaml:"headers,omitempty"`
Method *string `yaml:"method,omitempty"`
Data *string `yaml:"data,omitempty"`
SaveCookie *bool `yaml:"save_cookie,omitempty"`
// Deprecated: replaced with AutocompleteHeaders
StopMagic *bool `yaml:"stop_magic"`
AutocompleteHeaders *bool `yaml:"autocomplete_headers"`
EncodedRequest string `yaml:"encoded_request,omitempty"`
RAWRequest string `yaml:"raw_request,omitempty"`
}

// Overrides represents the overridden inputs that have to be applied to tests
type Overrides struct {
DestAddr *string `yaml:"dest_addr,omitempty" koanf:"dest_addr,omitempty"`
Expand All @@ -46,42 +27,6 @@ type Overrides struct {
OverrideEmptyHostHeader *bool `yaml:"override_empty_host_header,omitempty" koanf:"override_empty_host_header,omitempty"`
}

// Output is the response expected from the test
type Output struct {
Status []int `yaml:"status,flow,omitempty"`
ResponseContains string `yaml:"response_contains,omitempty"`
LogContains string `yaml:"log_contains,omitempty"`
NoLogContains string `yaml:"no_log_contains,omitempty"`
ExpectError *bool `yaml:"expect_error,omitempty"`
}

// Stage is an individual test stage
type Stage struct {
Input Input `yaml:"input"`
Output Output `yaml:"output"`
}

// Test is an individual test
type Test struct {
TestTitle string `yaml:"test_title"`
TestDescription string `yaml:"desc,omitempty"`
Stages []struct {
Stage Stage `yaml:"stage"`
} `yaml:"stages"`
}

// FTWTest is the base type used when unmarshaling
type FTWTest struct {
FileName string
Meta struct {
Author string `yaml:"author,omitempty"`
Enabled *bool `yaml:"enabled,omitempty"`
Name string `yaml:"name,omitempty"`
Description string `yaml:"description,omitempty"`
} `yaml:"meta"`
Tests []Test `yaml:"tests"`
}

// ApplyInputOverride will check if config had global overrides and write that into the test.
func ApplyInputOverrides(overrides *Overrides, input *Input) {
applySimpleOverrides(overrides, input)
Expand All @@ -98,8 +43,10 @@ func applyDestAddrOverride(overrides *Overrides, input *Input) {
if input.Headers == nil {
input.Headers = ftwhttp.Header{}
}
if overrides.OverrideEmptyHostHeader != nil && *overrides.OverrideEmptyHostHeader && input.Headers.Get("Host") == "" {
input.Headers.Set("Host", *overrides.DestAddr)
if overrides.OverrideEmptyHostHeader != nil &&
*overrides.OverrideEmptyHostHeader &&
input.GetHeaders().Get("Host") == "" {
input.GetHeaders().Set("Host", *overrides.DestAddr)
}
}
}
Expand Down Expand Up @@ -148,7 +95,7 @@ func applyHeadersOverride(overrides *Overrides, input *Input) {
input.Headers = ftwhttp.Header{}
}
for k, v := range overrides.Headers {
input.Headers.Set(k, v)
input.GetHeaders().Set(k, v)
}
}
}
Expand All @@ -159,14 +106,14 @@ func postLoadTestFTWTest(ftwTest *FTWTest) {
}
}

func postLoadTest(test *Test) {
func postLoadTest(test *types.Test) {
for index := range test.Stages {
postLoadStage(&test.Stages[index].Stage)
postLoadStage(&test.Stages[index].SD)
}
}

func postLoadStage(stage *Stage) {
postLoadInput(&stage.Input)
func postLoadStage(stage *types.StageData) {
postLoadInput((*Input)(&stage.Input))
}

func postLoadInput(input *Input) {
Expand Down
18 changes: 9 additions & 9 deletions test/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersDefault_StopMagicDefault() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersDefaultYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[0].Stages[0].Stage.Input
input := test.Tests[0].Stages[0].SD.Input
s.True(*input.AutocompleteHeaders)
s.False(*input.StopMagic)
}
Expand All @@ -186,15 +186,15 @@ func (s *typesTestSuite) TestAutocompleteHeadersDefault_StopMagicTrue() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersDefaultYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[1].Stages[0].Stage.Input
input := test.Tests[1].Stages[0].SD.Input
s.False(*input.AutocompleteHeaders)
s.True(*input.StopMagic)
}
func (s *typesTestSuite) TestAutocompleteHeadersDefault_StopMagicFalse() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersDefaultYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[2].Stages[0].Stage.Input
input := test.Tests[2].Stages[0].SD.Input
s.True(*input.AutocompleteHeaders)
s.False(*input.StopMagic)
}
Expand All @@ -203,7 +203,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersFalse_StopMagicDefault() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersFalseYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[0].Stages[0].Stage.Input
input := test.Tests[0].Stages[0].SD.Input
s.False(*input.AutocompleteHeaders)
s.True(*input.StopMagic)
}
Expand All @@ -212,7 +212,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersFalse_StopMagicTrue() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersFalseYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[1].Stages[0].Stage.Input
input := test.Tests[1].Stages[0].SD.Input
s.False(*input.AutocompleteHeaders)
s.True(*input.StopMagic)
}
Expand All @@ -221,7 +221,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersFalse_StopMagicFalse() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersFalseYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[2].Stages[0].Stage.Input
input := test.Tests[2].Stages[0].SD.Input
s.False(*input.AutocompleteHeaders)
s.True(*input.StopMagic)
}
Expand All @@ -230,7 +230,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersTrue_StopMagicDefault() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersTrueYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[0].Stages[0].Stage.Input
input := test.Tests[0].Stages[0].SD.Input
s.True(*input.AutocompleteHeaders)
s.False(*input.StopMagic)
}
Expand All @@ -239,7 +239,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersTrue_StopMagicTrue() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersTrueYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[1].Stages[0].Stage.Input
input := test.Tests[1].Stages[0].SD.Input
s.True(*input.AutocompleteHeaders)
s.False(*input.StopMagic)
}
Expand All @@ -248,7 +248,7 @@ func (s *typesTestSuite) TestAutocompleteHeadersTrue_StopMagicFalse() {
test, err := GetTestFromYaml([]byte(autocompleteHeadersTrueYaml))
s.NoError(err, "Parsing YAML shouldn't fail")

input := test.Tests[2].Stages[0].Stage.Input
input := test.Tests[2].Stages[0].SD.Input
s.True(*input.AutocompleteHeaders)
s.False(*input.StopMagic)
}

0 comments on commit a4cd40e

Please sign in to comment.