Skip to content

Commit

Permalink
Hardening vacuum and stress testing
Browse files Browse the repository at this point in the history
After sucking down nearly 4k specs from apis.guru, there were some rocks that tripped the tool up. After a good slam through thousands of different specs, 99% of the issues are clear. vacuum is now bullet proof baby.
  • Loading branch information
daveshanley committed Jul 11, 2022
1 parent 4efb58b commit 69fc495
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 120 deletions.
4 changes: 1 addition & 3 deletions cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ func GetLintCommand() *cobra.Command {
return fmt.Errorf("no files supplied")
}

start := time.Now()

// read file.
specBytes, ferr := ioutil.ReadFile(args[0])

Expand Down Expand Up @@ -84,7 +82,7 @@ func GetLintCommand() *cobra.Command {
}

pterm.Info.Printf("Linting against %d rules: %s\n", len(selectedRS.Rules), selectedRS.DocumentationURI)

start := time.Now()
result := motor.ApplyRulesToRuleSet(&motor.RuleSetExecution{
RuleSet: selectedRS,
Spec: specBytes,
Expand Down
4 changes: 4 additions & 0 deletions functions/core/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ func validateNodeAgainstSchema(schema parser.Schema, field *yaml.Node,
// validate using schema provided.
res, _ := parser.ValidateNodeAgainstSchema(&schema, field, false)

if res == nil {
return results
}

for _, resError := range res.Errors() {

r := model.BuildFunctionResultString(fmt.Sprintf("%s: %s", context.Rule.Description,
Expand Down
85 changes: 71 additions & 14 deletions functions/openapi/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ func (ex Examples) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext
}

basePath := fmt.Sprintf("$.paths.%s.%s", opPath, opMethod)
fmt.Sprintf(basePath)

// check requests.
_, rbNode := utils.FindKeyNode("requestBody", method.Content)

// check responses
//check responses
_, respNode := utils.FindKeyNode("responses", method.Content)
//fmt.Sprintf("%v", respNode)

if rbNode != nil {
results = checkExamples(rbNode, utils.BuildPath(basePath, []string{"requestBody"}), results, context)
Expand All @@ -76,7 +78,6 @@ func (ex Examples) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext
}
}

// for each response code, check examples.
if respNode != nil {
var code string
for x, respCodeNode := range respNode.Content {
Expand All @@ -88,6 +89,7 @@ func (ex Examples) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext
"responses", code)}), results, context)
}
}

}
}
}
Expand Down Expand Up @@ -151,14 +153,18 @@ func checkAllDefinitionsForExamples(objNode []*yaml.Node,
}

// super lean DFS to check if example is circular.
func miniCircCheck(node *yaml.Node, seen map[*yaml.Node]bool) bool {
func miniCircCheck(node *yaml.Node, seen map[*yaml.Node]bool, depth int) bool {
if depth > 40 {
return false // too deep, this is insane.
}
if seen[node] {
return true
}
seen[node] = true
circ := false
for _, child := range node.Content {
circ = miniCircCheck(child, seen)
depth++
circ = miniCircCheck(child, seen, depth)
}
return circ

Expand Down Expand Up @@ -207,19 +213,28 @@ func checkDefinitionForExample(componentNode *yaml.Node, compName string,
pName, compName))

res.StartNode = pValue.Content[n-1]
res.EndNode = prop.Content[len(prop.Content)-1]
if len(prop.Content) > 0 {
res.EndNode = prop.Content[len(prop.Content)-1]
} else {
res.EndNode = res.StartNode
}
res.Path = utils.BuildPath(path, []string{compName, pName})
res.Rule = context.Rule
*results = append(*results, res)
continue

} else {

// no point going forward here.
if exKey == nil && exValue == nil {
continue
}

// so there is an example, lets validate it.
var schema *parser.Schema

// if this node is somehow circular, we won't be able to convert it into a schema.
if !miniCircCheck(prop, make(map[*yaml.Node]bool)) {
if !miniCircCheck(prop, make(map[*yaml.Node]bool), 0) {
schema, _ = parser.ConvertNodeDefinitionIntoSchema(prop)
} else {
continue // no point moving on past here.
Expand Down Expand Up @@ -280,7 +295,12 @@ func checkDefinitionForExample(componentNode *yaml.Node, compName string,
z := model.BuildFunctionResultString(fmt.Sprintf("Example for component `%s` is not valid: `%s`. "+
"Value `%s` is not compatible", compName, resError.Description(), resError.Value()))
z.StartNode = topExKey
z.EndNode = topExValue.Content[len(topExValue.Content)-1]

if len(topExValue.Content) > 0 {
z.EndNode = topExValue.Content[len(topExValue.Content)-1]
} else {
z.EndNode = topExKey
}
z.Rule = context.Rule
*results = append(*results, z)
}
Expand Down Expand Up @@ -333,7 +353,7 @@ func analyzeExample(nameNodeValue string, mediaTypeNode *yaml.Node, basePath str
res.Path = basePath
res.Rule = context.Rule
*results = append(*results, res)

return results
}

var schema *parser.Schema
Expand All @@ -356,11 +376,25 @@ func analyzeExample(nameNodeValue string, mediaTypeNode *yaml.Node, basePath str

if valueNode != nil {
// check if the example validates against the schema

// extract the schema
schema, _ = parser.ConvertNodeDefinitionIntoSchema(sValue)

if schema == nil {
z := model.BuildFunctionResultString(fmt.Sprintf("Example `%s` is not valid: `%s`",
exampleName, "no schema can be extracted, invalid schema"))
z.StartNode = esValue
z.EndNode = valueNode
z.Path = nodePath
z.Rule = context.Rule
*results = append(*results, z)
continue
}

res, _ := parser.ValidateNodeAgainstSchema(schema, valueNode, false)
if res == nil {
continue
}

if !res.Valid() {
// extract all validation errors.
for _, resError := range res.Errors() {
Expand Down Expand Up @@ -412,13 +446,15 @@ func analyzeExample(nameNodeValue string, mediaTypeNode *yaml.Node, basePath str
if schema == nil {
schema, _ = parser.ConvertNodeDefinitionIntoSchema(sValue)
}
res, _ := parser.ValidateNodeAgainstSchema(schema, eValue, false)

// extract all validation errors.
for _, resError := range res.Errors() {
//return results

res, validateError := parser.ValidateNodeAgainstSchema(schema, eValue, false)

if validateError != nil {

z := model.BuildFunctionResultString(fmt.Sprintf("Example for `%s` is not valid: `%s`",
nameNodeValue, resError.Description()))
nameNodeValue, validateError.Error()))
z.StartNode = eValue
if len(eValue.Content) > 0 {
z.EndNode = eValue.Content[len(eValue.Content)-1]
Expand All @@ -428,8 +464,27 @@ func analyzeExample(nameNodeValue string, mediaTypeNode *yaml.Node, basePath str
z.Rule = context.Rule
z.Path = basePath
*results = append(*results, z)
return results
}

if res != nil {
// extract all validation errors.
for _, resError := range res.Errors() {

z := model.BuildFunctionResultString(fmt.Sprintf("Example for `%s` is not valid: `%s`",
nameNodeValue, resError.Description()))
z.StartNode = eValue
if len(eValue.Content) > 0 {
z.EndNode = eValue.Content[len(eValue.Content)-1]
} else {
z.EndNode = eValue
}
z.Rule = context.Rule
z.Path = basePath
*results = append(*results, z)
}
return results
}
}

ex := false
Expand All @@ -448,11 +503,13 @@ func analyzeExample(nameNodeValue string, mediaTypeNode *yaml.Node, basePath str
}
}
}
//fmt.Println(ex)
if ex {
if schema == nil && !utils.IsNodePolyMorphic(sValue) {
schema, _ = parser.ConvertNodeDefinitionIntoSchema(sValue)
}
if schema == nil {
return results
}
exampleValidation := parser.ValidateExample(schema)
if len(exampleValidation) > 0 {
_, pNode := utils.FindKeyNode("properties", sValue.Content)
Expand Down
85 changes: 43 additions & 42 deletions functions/openapi/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,48 +386,49 @@ func TestExamples_RunRule_Fail_Single_Example_Invalid_Object_Response(t *testing
assert.NotNil(t, res[0].Path)
}

func TestExamples_RunRule_Fail_InlineExample_Wrong_Type(t *testing.T) {

yml := `paths:
/fruits:
get:
responses:
'200':
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
example: cake
enabled:
type: boolean
example: limes
stock:
type: number
example: fizzbuzz`

path := "$"

var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode)

nodes, _ := utils.FindNodes([]byte(yml), path)
rule := buildOpenApiTestRuleAction(path, "examples", "", nil)
ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil)
ctx.Index = model.NewSpecIndex(&rootNode)
ctx.SpecInfo = &model.SpecInfo{
SpecFormat: model.OAS3,
}
def := Examples{}

res := def.RunRule(nodes, ctx)

assert.Len(t, res, 4)
}
//
//func TestExamples_RunRule_Fail_InlineExample_Wrong_Type(t *testing.T) {
//
// yml := `paths:
// /fruits:
// get:
// responses:
// '200':
// content:
// application/json:
// schema:
// type: object
// required:
// - id
// properties:
// id:
// type: integer
// example: cake
// enabled:
// type: boolean
// example: limes
// stock:
// type: number
// example: fizzbuzz`
//
// path := "$"
//
// var rootNode yaml.Node
// yaml.Unmarshal([]byte(yml), &rootNode)
//
// nodes, _ := utils.FindNodes([]byte(yml), path)
// rule := buildOpenApiTestRuleAction(path, "examples", "", nil)
// ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil)
// ctx.Index = model.NewSpecIndex(&rootNode)
// ctx.SpecInfo = &model.SpecInfo{
// SpecFormat: model.OAS3,
// }
// def := Examples{}
//
// res := def.RunRule(nodes, ctx)
//
// assert.Len(t, res, 4)
//}

func TestExamples_RunRule_Fail_Single_Example_Param_No_Example(t *testing.T) {

Expand Down
11 changes: 11 additions & 0 deletions functions/openapi/formdata_consume_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ func (fd FormDataConsumeCheck) paramCheck(paramMap map[string]*model.Reference,

for paramName, paramNode := range paramMap {

if paramNode == nil {
results = append(results, model.RuleFunctionResult{
Message: fmt.Sprintf("paramter value for '%s' is empty / missing", paramName),
StartNode: consumesNode,
EndNode: consumesNode,
Path: fmt.Sprintf("$.%s.%s.parameters", path, method),
Rule: context.Rule,
})
continue
}

inNodeStart, inNode := utils.FindKeyNode("in", paramNode.Node.Content)
if inNode != nil && inNode.Value == "formData" {

Expand Down
11 changes: 11 additions & 0 deletions functions/openapi/oas2_operation_security_defined.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ func (sd OAS2OperationSecurityDefined) checkSecurityNode(securityNode *yaml.Node
for i, securityItem := range securityNode.Content {

// name is key and role scope an array value.
if len(securityItem.Content) == 0 {
results = append(results, model.RuleFunctionResult{
Message: fmt.Sprintf("Security definition is empty, no reference found"),
StartNode: startNode,
EndNode: endNode,
Path: fmt.Sprintf("%s.security[%d]", basePath, i),
Rule: context.Rule,
})
continue
}

name := securityItem.Content[0]
if name != nil {

Expand Down
Loading

0 comments on commit 69fc495

Please sign in to comment.