/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spec import ( "github.com/go-openapi/jsonreference" "github.com/google/go-cmp/cmp" fuzz "github.com/google/gofuzz" ) var SwaggerFuzzFuncs []interface{} = []interface{}{ func(v *Responses, c fuzz.Continue) { c.FuzzNoCustom(v) if v.Default != nil { // Check if we hit maxDepth and left an incomplete value if v.Default.Description == "" { v.Default = nil v.StatusCodeResponses = nil } } // conversion has no way to discern empty statusCodeResponses from // nil, since "default" is always included in the map. // So avoid empty responses list if len(v.StatusCodeResponses) == 0 { v.StatusCodeResponses = nil } }, func(v *Operation, c fuzz.Continue) { c.FuzzNoCustom(v) if v != nil { // force non-nil v.Responses = &Responses{} c.Fuzz(v.Responses) v.Schemes = nil if c.RandBool() { v.Schemes = append(v.Schemes, "http") } if c.RandBool() { v.Schemes = append(v.Schemes, "https") } if c.RandBool() { v.Schemes = append(v.Schemes, "ws") } if c.RandBool() { v.Schemes = append(v.Schemes, "wss") } // Gnostic unconditionally makes security values non-null // So do not fuzz null values into the array. for i, val := range v.Security { if val == nil { v.Security[i] = make(map[string][]string) } for k, v := range val { if v == nil { val[k] = make([]string, 0) } } } } }, func(v map[int]Response, c fuzz.Continue) { n := 0 c.Fuzz(&n) if n == 0 { // Test that fuzzer is not at maxDepth so we do not // end up with empty elements return } // Prevent negative numbers num := c.Intn(4) for i := 0; i < num+2; i++ { val := Response{} c.Fuzz(&val) val.Description = c.RandString() + "x" v[100*(i+1)+c.Intn(100)] = val } }, func(v map[string]PathItem, c fuzz.Continue) { n := 0 c.Fuzz(&n) if n == 0 { // Test that fuzzer is not at maxDepth so we do not // end up with empty elements return } num := c.Intn(5) for i := 0; i < num+2; i++ { val := PathItem{} c.Fuzz(&val) // Ref params are only allowed in certain locations, so // possibly add a few to PathItems numRefsToAdd := c.Intn(5) for i := 0; i < numRefsToAdd; i++ { theRef := Parameter{} c.Fuzz(&theRef.Refable) val.Parameters = append(val.Parameters, theRef) } v["/"+c.RandString()] = val } }, func(v *SchemaOrArray, c fuzz.Continue) { *v = SchemaOrArray{} // gnostic parser just doesn't support more // than one Schema here v.Schema = &Schema{} c.Fuzz(&v.Schema) }, func(v *SchemaOrBool, c fuzz.Continue) { *v = SchemaOrBool{} if c.RandBool() { v.Allows = c.RandBool() } else { v.Schema = &Schema{} v.Allows = true c.Fuzz(&v.Schema) } }, func(v map[string]Response, c fuzz.Continue) { n := 0 c.Fuzz(&n) if n == 0 { // Test that fuzzer is not at maxDepth so we do not // end up with empty elements return } // Response definitions are not allowed to // be refs for i := 0; i < c.Intn(5)+1; i++ { resp := &Response{} c.Fuzz(resp) resp.Ref = Ref{} resp.Description = c.RandString() + "x" // Response refs are not vendor extensible by gnostic resp.VendorExtensible.Extensions = nil v[c.RandString()+"x"] = *resp } }, func(v *Header, c fuzz.Continue) { if v != nil { c.FuzzNoCustom(v) // descendant Items of Header may not be refs cur := v.Items for cur != nil { cur.Ref = Ref{} cur = cur.Items } } }, func(v *Ref, c fuzz.Continue) { *v = Ref{} v.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString()) }, func(v *Response, c fuzz.Continue) { *v = Response{} if c.RandBool() { v.Ref = Ref{} v.Ref.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString()) } else { c.Fuzz(&v.VendorExtensible) c.Fuzz(&v.Schema) c.Fuzz(&v.ResponseProps) v.Headers = nil v.Ref = Ref{} n := 0 c.Fuzz(&n) if n != 0 { // Test that fuzzer is not at maxDepth so we do not // end up with empty elements num := c.Intn(4) for i := 0; i < num; i++ { if v.Headers == nil { v.Headers = make(map[string]Header) } hdr := Header{} c.Fuzz(&hdr) if hdr.Type == "" { // hit maxDepth, just abort trying to make haders v.Headers = nil break } v.Headers[c.RandString()+"x"] = hdr } } else { v.Headers = nil } } v.Description = c.RandString() + "x" // Gnostic parses empty as nil, so to keep avoid putting empty if len(v.Headers) == 0 { v.Headers = nil } }, func(v **Info, c fuzz.Continue) { // Info is never nil *v = &Info{} c.FuzzNoCustom(*v) (*v).Title = c.RandString() + "x" }, func(v *Extensions, c fuzz.Continue) { // gnostic parser only picks up x- vendor extensions numChildren := c.Intn(5) for i := 0; i < numChildren; i++ { if *v == nil { *v = Extensions{} } (*v)["x-"+c.RandString()] = c.RandString() } }, func(v *Swagger, c fuzz.Continue) { c.FuzzNoCustom(v) if v.Paths == nil { // Force paths non-nil since it does not have omitempty in json tag. // This means a perfect roundtrip (via json) is impossible, // since we can't tell the difference between empty/unspecified paths v.Paths = &Paths{} c.Fuzz(v.Paths) } v.Swagger = "2.0" // Gnostic support serializing ID at all // unavoidable data loss v.ID = "" v.Schemes = nil if c.RandUint64()%2 == 1 { v.Schemes = append(v.Schemes, "http") } if c.RandUint64()%2 == 1 { v.Schemes = append(v.Schemes, "https") } if c.RandUint64()%2 == 1 { v.Schemes = append(v.Schemes, "ws") } if c.RandUint64()%2 == 1 { v.Schemes = append(v.Schemes, "wss") } // Gnostic unconditionally makes security values non-null // So do not fuzz null values into the array. for i, val := range v.Security { if val == nil { v.Security[i] = make(map[string][]string) } for k, v := range val { if v == nil { val[k] = make([]string, 0) } } } }, func(v *SecurityScheme, c fuzz.Continue) { v.Description = c.RandString() + "x" c.Fuzz(&v.VendorExtensible) switch c.Intn(3) { case 0: v.Type = "basic" case 1: v.Type = "apiKey" switch c.Intn(2) { case 0: v.In = "header" case 1: v.In = "query" default: panic("unreachable") } v.Name = "x" + c.RandString() case 2: v.Type = "oauth2" switch c.Intn(4) { case 0: v.Flow = "accessCode" v.TokenURL = "https://" + c.RandString() v.AuthorizationURL = "https://" + c.RandString() case 1: v.Flow = "application" v.TokenURL = "https://" + c.RandString() case 2: v.Flow = "implicit" v.AuthorizationURL = "https://" + c.RandString() case 3: v.Flow = "password" v.TokenURL = "https://" + c.RandString() default: panic("unreachable") } c.Fuzz(&v.Scopes) default: panic("unreachable") } }, func(v *interface{}, c fuzz.Continue) { *v = c.RandString() + "x" }, func(v *string, c fuzz.Continue) { *v = c.RandString() + "x" }, func(v *ExternalDocumentation, c fuzz.Continue) { v.Description = c.RandString() + "x" v.URL = c.RandString() + "x" }, func(v *SimpleSchema, c fuzz.Continue) { c.FuzzNoCustom(v) switch c.Intn(5) { case 0: v.Type = "string" case 1: v.Type = "number" case 2: v.Type = "boolean" case 3: v.Type = "integer" case 4: v.Type = "array" default: panic("unreachable") } switch c.Intn(5) { case 0: v.CollectionFormat = "csv" case 1: v.CollectionFormat = "ssv" case 2: v.CollectionFormat = "tsv" case 3: v.CollectionFormat = "pipes" case 4: v.CollectionFormat = "" default: panic("unreachable") } // None of the types which include SimpleSchema in our definitions // actually support "example" in the official spec v.Example = nil // unsupported by openapi v.Nullable = false }, func(v *int64, c fuzz.Continue) { c.Fuzz(v) // Gnostic does not differentiate between 0 and non-specified // so avoid using 0 for fuzzer if *v == 0 { *v = 1 } }, func(v *float64, c fuzz.Continue) { c.Fuzz(v) // Gnostic does not differentiate between 0 and non-specified // so avoid using 0 for fuzzer if *v == 0.0 { *v = 1.0 } }, func(v *Parameter, c fuzz.Continue) { if v == nil { return } c.Fuzz(&v.VendorExtensible) if c.RandBool() { // body param v.Description = c.RandString() + "x" v.Name = c.RandString() + "x" v.In = "body" c.Fuzz(&v.Description) c.Fuzz(&v.Required) v.Schema = &Schema{} c.Fuzz(&v.Schema) } else { c.Fuzz(&v.SimpleSchema) c.Fuzz(&v.CommonValidations) v.AllowEmptyValue = false v.Description = c.RandString() + "x" v.Name = c.RandString() + "x" switch c.Intn(4) { case 0: // Header param v.In = "header" case 1: // Form data param v.In = "formData" v.AllowEmptyValue = c.RandBool() case 2: // Query param v.In = "query" v.AllowEmptyValue = c.RandBool() case 3: // Path param v.In = "path" v.Required = true default: panic("unreachable") } // descendant Items of Parameter may not be refs cur := v.Items for cur != nil { cur.Ref = Ref{} cur = cur.Items } } }, func(v *Schema, c fuzz.Continue) { if c.RandBool() { // file schema c.Fuzz(&v.Default) c.Fuzz(&v.Description) c.Fuzz(&v.Example) c.Fuzz(&v.ExternalDocs) c.Fuzz(&v.Format) c.Fuzz(&v.ReadOnly) c.Fuzz(&v.Required) c.Fuzz(&v.Title) v.Type = StringOrArray{"file"} } else { // normal schema c.Fuzz(&v.SchemaProps) c.Fuzz(&v.SwaggerSchemaProps) c.Fuzz(&v.VendorExtensible) // c.Fuzz(&v.ExtraProps) // ExtraProps will not roundtrip - gnostic throws out // unrecognized keys } // Not supported by official openapi v2 spec // and stripped by k8s apiserver v.ID = "" v.AnyOf = nil v.OneOf = nil v.Not = nil v.Nullable = false v.AdditionalItems = nil v.Schema = "" v.PatternProperties = nil v.Definitions = nil v.Dependencies = nil }, } var SwaggerDiffOptions = []cmp.Option{ // cmp.Diff panics on Ref since jsonreference.Ref uses unexported fields cmp.Comparer(func(a Ref, b Ref) bool { return a.String() == b.String() }), }