Path: blob/dev/pkg/operators/matchers/fuzz_harness.go
4538 views
package matchers12import (3"encoding/hex"4"strconv"5"strings"6)78const (9fuzzMaxInputSize = 16 << 1010fuzzMaxItems = 811fuzzMaxValueBytes = 25612)1314var (15fuzzMatcherTypes = []MatcherType{WordsMatcher, RegexMatcher, BinaryMatcher, StatusMatcher, SizeMatcher, DSLMatcher, XPathMatcher}16fuzzConditions = []string{"", "and", "or"}17fuzzEncodings = []string{"", "hex"}18fuzzParts = []string{"", "body", "raw", "all_headers", "header", "response"}19)2021type fuzzMatcherCandidate struct {22matcherType MatcherType23condition string24part string25encoding string26name string27negative bool28caseInsensitive bool29matchAll bool30values []string31status []int32size []int33}3435func matcherFromFuzzData(data []byte) (*Matcher, bool) {36if len(data) == 0 || len(data) > fuzzMaxInputSize {37return nil, false38}3940payload := data4142candidate := newFuzzMatcherCandidate(data)43candidate.applyLines(splitFuzzLines(payload))44candidate.addFallbackValues(payload)45candidate.addFallbackIntegers(payload)4647return candidate.build()48}4950func newFuzzMatcherCandidate(data []byte) *fuzzMatcherCandidate {51flags := fuzzByteAt(data, 1)52return &fuzzMatcherCandidate{53matcherType: fuzzMatcherTypes[int(fuzzByteAt(data, 0))%len(fuzzMatcherTypes)],54condition: fuzzConditions[int(fuzzByteAt(data, 1))%len(fuzzConditions)],55part: fuzzParts[int(fuzzByteAt(data, 2))%len(fuzzParts)],56encoding: fuzzEncodings[int(fuzzByteAt(data, 3))%len(fuzzEncodings)],57name: fuzzName(data),58negative: flags&0x01 != 0,59caseInsensitive: flags&0x02 != 0,60matchAll: flags&0x04 != 0,61}62}6364func (candidate *fuzzMatcherCandidate) applyLines(lines []string) {65for _, line := range lines {66key, rawValue, ok := cutFuzzKV(line)67if !ok {68candidate.addValue(line)69continue70}7172switch key {73case "type":74matcherType, err := toMatcherTypes(rawValue)75if err != nil {76candidate.matcherType = MatcherType(0)77} else {78candidate.matcherType = matcherType79}80case "condition":81candidate.condition = trimFuzzValue(rawValue)82case "part":83candidate.part = trimFuzzValue(rawValue)84case "encoding":85candidate.encoding = trimFuzzValue(rawValue)86case "name":87candidate.name = fuzzNameFromText(rawValue)88case "negative":89candidate.negative = parseFuzzBool(rawValue, candidate.negative)90case "case-insensitive":91candidate.caseInsensitive = parseFuzzBool(rawValue, candidate.caseInsensitive)92case "match-all":93candidate.matchAll = parseFuzzBool(rawValue, candidate.matchAll)94case "value":95candidate.addValue(rawValue)96case "word":97candidate.matcherType = WordsMatcher98candidate.addValue(rawValue)99case "regex":100candidate.matcherType = RegexMatcher101candidate.addValue(rawValue)102case "binary":103candidate.matcherType = BinaryMatcher104candidate.addValue(rawValue)105case "dsl":106candidate.matcherType = DSLMatcher107candidate.addValue(rawValue)108case "xpath":109candidate.matcherType = XPathMatcher110candidate.addValue(rawValue)111case "status":112candidate.matcherType = StatusMatcher113candidate.status = append(candidate.status, fuzzParseStatuses(rawValue)...)114case "size":115candidate.matcherType = SizeMatcher116candidate.size = append(candidate.size, fuzzParseSizes(rawValue)...)117}118}119}120121func (candidate *fuzzMatcherCandidate) addFallbackValues(payload []byte) {122if len(candidate.values) > 0 || len(candidate.values) >= fuzzMaxItems {123return124}125126for _, value := range splitFuzzFields(payload) {127candidate.addValue(value)128if len(candidate.values) >= fuzzMaxItems {129return130}131}132}133134func (candidate *fuzzMatcherCandidate) addFallbackIntegers(payload []byte) {135if len(candidate.status) == 0 {136candidate.status = append(candidate.status, fuzzStatusValue(int(fuzzByteAt(payload, 0))))137candidate.status = append(candidate.status, fuzzStatusValue(int(fuzzByteAt(payload, 1))))138}139if len(candidate.size) == 0 {140candidate.size = append(candidate.size, fuzzSizeValue(int(fuzzByteAt(payload, 2))|(int(fuzzByteAt(payload, 3))<<8)))141candidate.size = append(candidate.size, fuzzSizeValue(int(fuzzByteAt(payload, 4))|(int(fuzzByteAt(payload, 5))<<8)))142}143}144145func (candidate *fuzzMatcherCandidate) addValue(value string) {146value = trimFuzzValue(value)147if value == "" || len(candidate.values) >= fuzzMaxItems {148return149}150candidate.values = append(candidate.values, value)151}152153func (candidate *fuzzMatcherCandidate) build() (*Matcher, bool) {154matcher := &Matcher{155Type: MatcherTypeHolder{MatcherType: candidate.matcherType},156Condition: candidate.condition,157Part: candidate.part,158Negative: candidate.negative,159Name: candidate.name,160Encoding: candidate.encoding,161CaseInsensitive: candidate.caseInsensitive,162MatchAll: candidate.matchAll,163}164165switch candidate.matcherType {166case DSLMatcher:167matcher.Part = ""168matcher.Encoding = ""169matcher.CaseInsensitive = false170case StatusMatcher, SizeMatcher:171matcher.Encoding = ""172matcher.CaseInsensitive = false173case XPathMatcher:174matcher.Encoding = ""175matcher.CaseInsensitive = false176case RegexMatcher, BinaryMatcher:177matcher.CaseInsensitive = false178}179180switch candidate.matcherType {181case WordsMatcher:182matcher.Words = append([]string(nil), candidate.values...)183case RegexMatcher:184matcher.Regex = append([]string(nil), candidate.values...)185case BinaryMatcher:186matcher.Binary = append([]string(nil), candidate.values...)187case DSLMatcher:188matcher.DSL = append([]string(nil), candidate.values...)189case XPathMatcher:190matcher.XPath = append([]string(nil), candidate.values...)191case StatusMatcher:192matcher.Status = append([]int(nil), candidate.status...)193case SizeMatcher:194matcher.Size = append([]int(nil), candidate.size...)195default:196matcher.Words = append([]string(nil), candidate.values...)197}198199if matcher.GetType() == StatusMatcher || matcher.GetType() == SizeMatcher {200return matcher, len(matcher.Status) > 0 || len(matcher.Size) > 0201}202return matcher, len(candidate.values) > 0203}204205func splitFuzzLines(data []byte) []string {206fields := strings.FieldsFunc(string(data), func(r rune) bool {207return r == '\n' || r == '\r' || r == ';'208})209if len(fields) > fuzzMaxItems*4 {210fields = fields[:fuzzMaxItems*4]211}212213lines := make([]string, 0, len(fields))214for _, field := range fields {215field = trimFuzzValue(field)216if field != "" {217lines = append(lines, field)218}219}220return lines221}222223func splitFuzzFields(data []byte) []string {224fields := strings.FieldsFunc(string(data), func(r rune) bool {225return r == '\n' || r == '\r' || r == '|' || r == ','226})227if len(fields) > fuzzMaxItems {228fields = fields[:fuzzMaxItems]229}230231values := make([]string, 0, len(fields))232for _, field := range fields {233field = trimFuzzValue(field)234if field != "" {235values = append(values, field)236}237}238return values239}240241func cutFuzzKV(line string) (string, string, bool) {242key, value, ok := strings.Cut(line, "=")243if !ok {244key, value, ok = strings.Cut(line, ":")245}246if !ok {247return "", "", false248}249return strings.ToLower(strings.TrimSpace(key)), trimFuzzValue(value), true250}251252func trimFuzzValue(value string) string {253value = strings.TrimSpace(strings.ReplaceAll(value, "\x00", ""))254if len(value) > fuzzMaxValueBytes {255value = value[:fuzzMaxValueBytes]256}257return value258}259260func fuzzName(data []byte) string {261if len(data) == 0 {262return ""263}264if len(data) > 8 {265data = data[:8]266}267return "fuzz-" + hex.EncodeToString(data)268}269270func fuzzNameFromText(value string) string {271value = strings.ToLower(trimFuzzValue(value))272if value == "" {273return ""274}275var builder strings.Builder276for _, r := range value {277switch {278case r >= 'a' && r <= 'z':279builder.WriteRune(r)280case r >= '0' && r <= '9':281builder.WriteRune(r)282case r == '-':283builder.WriteRune(r)284}285if builder.Len() >= 32 {286break287}288}289if builder.Len() == 0 {290return ""291}292return builder.String()293}294295func parseFuzzBool(value string, fallback bool) bool {296switch strings.ToLower(trimFuzzValue(value)) {297case "1", "true", "yes", "on":298return true299case "0", "false", "no", "off":300return false301default:302return fallback303}304}305306func fuzzParseStatuses(value string) []int {307parsed := fuzzParseNumbers(value)308statuses := make([]int, 0, len(parsed))309for _, number := range parsed {310statuses = append(statuses, fuzzStatusValue(number))311}312return statuses313}314315func fuzzParseSizes(value string) []int {316parsed := fuzzParseNumbers(value)317sizes := make([]int, 0, len(parsed))318for _, number := range parsed {319sizes = append(sizes, fuzzSizeValue(number))320}321return sizes322}323324func fuzzParseNumbers(value string) []int {325tokens := strings.FieldsFunc(value, func(r rune) bool {326return r == ',' || r == '|' || r == ' ' || r == '\t'327})328if len(tokens) > fuzzMaxItems {329tokens = tokens[:fuzzMaxItems]330}331332parsed := make([]int, 0, len(tokens))333for _, token := range tokens {334token = trimFuzzValue(token)335if token == "" {336continue337}338number, err := strconv.Atoi(token)339if err != nil {340number = 0341for i := 0; i < len(token); i++ {342number += int(token[i])343}344}345parsed = append(parsed, number)346}347return parsed348}349350func fuzzStatusValue(number int) int {351if number >= 100 && number <= 599 {352return number353}354if number < 0 {355number = -number356}357return 100 + (number % 500)358}359360func fuzzSizeValue(number int) int {361if number > 0 && number <= 1<<20 {362return number363}364if number < 0 {365number = -number366}367return 1 + (number % (1 << 16))368}369370func fuzzByteAt(data []byte, index int) byte {371if index < 0 || index >= len(data) {372return 0373}374return data[index]375}376377378