From bec0d854fc3eae94ccf40958fafc233977f7f3a0 Mon Sep 17 00:00:00 2001 From: Calvin Lobo Date: Mon, 25 Nov 2024 12:33:24 -0500 Subject: [PATCH 1/2] Added noRequestBody function to check that HTTP GET and DELETE methods do not contain a request body --- functions/functions.go | 1 + functions/openapi/no_request_body.go | 72 +++++++++++ functions/openapi/no_request_body_test.go | 147 ++++++++++++++++++++++ model/rules.go | 1 + 4 files changed, 221 insertions(+) create mode 100644 functions/openapi/no_request_body.go create mode 100644 functions/openapi/no_request_body_test.go diff --git a/functions/functions.go b/functions/functions.go index 5fa6a326..53d44fac 100644 --- a/functions/functions.go +++ b/functions/functions.go @@ -103,6 +103,7 @@ func MapBuiltinFunctions() Functions { funcs["infoLicenseUrl"] = openapi_functions.InfoLicenseURL{} funcs["infoLicenseURLSPDX"] = openapi_functions.InfoLicenseURLSPDX{} funcs["infoContactProperties"] = openapi_functions.InfoContactProperties{} + funcs["noRequestBody"] = openapi_functions.NoRequestBody{} // add owasp functions used by the owasp rules funcs["owaspHeaderDefinition"] = owasp.HeaderDefinition{} diff --git a/functions/openapi/no_request_body.go b/functions/openapi/no_request_body.go new file mode 100644 index 00000000..ba8a7bb0 --- /dev/null +++ b/functions/openapi/no_request_body.go @@ -0,0 +1,72 @@ +// Copyright 2022 Dave Shanley / Quobix +// SPDX-License-Identifier: MIT + +package openapi + +import ( + "fmt" + "github.com/daveshanley/vacuum/model" + vacuumUtils "github.com/daveshanley/vacuum/utils" + "github.com/pb33f/doctor/model/high/base" + "gopkg.in/yaml.v3" + "strings" +) + +// NoRequestBody is a rule that checks operations are using tags and they are not empty. +type NoRequestBody struct { +} + +// GetSchema returns a model.RuleFunctionSchema defining the schema of the NoRequestBody rule. +func (r NoRequestBody) GetSchema() model.RuleFunctionSchema { + return model.RuleFunctionSchema{ + Name: "noRequestBody", + } +} + +// GetCategory returns the category of the TagDefined rule. +func (r NoRequestBody) GetCategory() string { + return model.FunctionCategoryOpenAPI +} + +// RunRule will execute the NoRequestBody rule, based on supplied context and a supplied []*yaml.Node slice. +func (r NoRequestBody) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) []model.RuleFunctionResult { + + var results []model.RuleFunctionResult + + if context.DrDocument == nil { + return results + } + + paths := context.DrDocument.V3Document.Paths + if paths != nil { + for pathItemPairs := paths.PathItems.First(); pathItemPairs != nil; pathItemPairs = pathItemPairs.Next() { + path := pathItemPairs.Key() + v := pathItemPairs.Value() + + for opPairs := v.GetOperations().First(); opPairs != nil; opPairs = opPairs.Next() { + method := opPairs.Key() + op := opPairs.Value() + + for _, checkedMethods := range []string{"GET", "DELETE"} { + if strings.EqualFold(method, checkedMethods) { + if op.RequestBody != nil { + + res := model.RuleFunctionResult{ + Message: vacuumUtils.SuppliedOrDefault(context.Rule.Message, fmt.Sprintf("`%s` operation should not have a requestBody defined", + strings.ToUpper(method))), + StartNode: op.Value.GoLow().KeyNode, + EndNode: vacuumUtils.BuildEndNode(op.Value.GoLow().KeyNode), + Path: fmt.Sprintf("$.paths['%s'].%s", path, method), + Rule: context.Rule, + } + results = append(results, res) + op.AddRuleFunctionResult(base.ConvertRuleResult(&res)) + } + } + } + } + } + } + return results + +} diff --git a/functions/openapi/no_request_body_test.go b/functions/openapi/no_request_body_test.go new file mode 100644 index 00000000..e4ff7b9c --- /dev/null +++ b/functions/openapi/no_request_body_test.go @@ -0,0 +1,147 @@ +package openapi + +import ( + "fmt" + "github.com/daveshanley/vacuum/model" + drModel "github.com/pb33f/doctor/model" + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNoRequestBody_GetSchema(t *testing.T) { + def := NoRequestBody{} + assert.Equal(t, "noRequestBody", def.GetSchema().Name) +} + +func TestNoRequestBody_RunRule(t *testing.T) { + def := NoRequestBody{} + res := def.RunRule(nil, model.RuleFunctionContext{}) + assert.Len(t, res, 0) +} + +func TestNoRequestBody_RunRule_Fail(t *testing.T) { + + yml := `openapi: 3.0.1 +paths: + /melody: + post: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string + /maddox: + get: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string + delete: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string + /ember: + get: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string +` + document, err := libopenapi.NewDocument([]byte(yml)) + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + m, _ := document.BuildV3Model() + path := "$" + + drDocument := drModel.NewDrDocument(m) + + rule := buildOpenApiTestRuleAction(path, "responses", "", nil) + ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil) + + ctx.Document = document + ctx.DrDocument = drDocument + ctx.Rule = &rule + + def := NoRequestBody{} + res := def.RunRule(nil, ctx) + assert.Len(t, res, 3) +} + +func TestNoRequestBody_RunRule_Success(t *testing.T) { + + yml := `openapi: 3.0.1 +paths: + /melody: + post: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string + /maddox: + post: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string + /ember: + patch: + requestBody: + description: "the body of the request" + content: + application/json: + schema: + properties: + id: + type: string +` + + document, err := libopenapi.NewDocument([]byte(yml)) + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + m, _ := document.BuildV3Model() + path := "$" + + drDocument := drModel.NewDrDocument(m) + + rule := buildOpenApiTestRuleAction(path, "responses", "", nil) + ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil) + + ctx.Document = document + ctx.DrDocument = drDocument + ctx.Rule = &rule + + def := NoRequestBody{} + res := def.RunRule(nil, ctx) + + assert.Len(t, res, 0) + +} diff --git a/model/rules.go b/model/rules.go index c4cf59f1..e2723e50 100644 --- a/model/rules.go +++ b/model/rules.go @@ -68,6 +68,7 @@ type RuleFunctionResult struct { ModelContext any `json:"-" yaml:"-"` } +// IgnoredItems is a map of the rule ID to an array of violation paths type IgnoredItems map[string][]string // RuleResultSet contains all the results found during a linting run, and all the methods required to From b90fe58272a15e841866da1dab21c8a0da7a2d55 Mon Sep 17 00:00:00 2001 From: Calvin Lobo Date: Mon, 25 Nov 2024 12:44:52 -0500 Subject: [PATCH 2/2] fixed test --- functions/functions_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/functions_test.go b/functions/functions_test.go index df91ceaa..1ab49fff 100644 --- a/functions/functions_test.go +++ b/functions/functions_test.go @@ -8,5 +8,5 @@ import ( func TestMapBuiltinFunctions(t *testing.T) { funcs := MapBuiltinFunctions() - assert.Len(t, funcs.GetAllFunctions(), 72) + assert.Len(t, funcs.GetAllFunctions(), 73) }