diff --git a/internal/match/extractor.go b/internal/match/extractor.go new file mode 100644 index 0000000..fc1f3ea --- /dev/null +++ b/internal/match/extractor.go @@ -0,0 +1,57 @@ +package match + +import ( + "strconv" + "strings" + + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" + "github.com/bountysecurity/gbounty/internal/response" +) + +func bytesToFindIn(g profile.Grep, req *request.Request, res *response.Response) (int, []byte) { + if len(g.Where) == 0 && res == nil || + len(g.Where) > 0 && req == nil { + return 0, []byte{} + } + + // Find grep in response + if len(g.Where) == 0 { + return resBytesToFindIn(g, res) + } + + // Find grep in request + return reqBytesToFindIn(g.Where, req) +} + +func resBytesToFindIn(g profile.Grep, res *response.Response) (int, []byte) { + headers := res.BytesOnlyHeaders() + lenHeaders := len(headers) + len("\r\n") + lenStatusLine := len(res.Proto + " " + strconv.Itoa(res.Code) + " " + res.Status + "\r\n") + + switch { + case g.Option.OnlyInHeaders(): + return lenStatusLine, res.BytesOnlyHeaders() + case g.Option.NotInHeaders(): + return lenHeaders, res.BytesWithoutHeaders() + default: + return 0, res.Bytes() + } +} + +func reqBytesToFindIn(where string, req *request.Request) (int, []byte) { + switch { + case strings.HasPrefix(where, "All"): + return reqBytesToFindInAll(where, req) + case strings.HasPrefix(where, "Url"): + return reqBytesToFindInURL(where, req) + case strings.HasPrefix(where, "Param"): + return reqBytesToFindInParam(where, req) + case strings.HasPrefix(where, "Entire"): + return reqBytesToFindInEntire(where, req) + case strings.HasPrefix(where, "HTTP"): + return reqBytesToFindInHTTP(where, req) + } + + return 0, []byte{} +} diff --git a/internal/match/extractor_req.go b/internal/match/extractor_req.go new file mode 100644 index 0000000..3de5a06 --- /dev/null +++ b/internal/match/extractor_req.go @@ -0,0 +1,162 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqBytesToFindInAll(where string, req *request.Request) (int, []byte) { + switch where { + case "All request": + return 0, req.Bytes() + case "All parameters name": + return 0, reqParamNameBytes(req) + case "All parameters value": + return 0, reqParamValueBytes(req) + case "All HTTP headers value": + return 0, req.HeaderBytes() + } + + return 0, []byte{} +} + +func reqBytesToFindInURL(where string, req *request.Request) (int, []byte) { + switch where { + case "Url path folder": + return 0, reqURLFolderBytes(req) + case "Url path filename": + return 0, reqURLFileBytes(req) + } + + return 0, []byte{} +} + +func reqBytesToFindInParam(where string, req *request.Request) (int, []byte) { + switch where { + case "Param url name": + return 0, reqQueryNameBytes(req) + case "Param url value": + return 0, reqQueryValueBytes(req) + case "Param body name": + return 0, reqBodyNameBytes(req) + case "Param body value": + return 0, reqBodyValueBytes(req) + case "Param cookie name": + return 0, reqCookieNameBytes(req) + case "Param cookie value": + return 0, reqCookieValueBytes(req) + case "Param json name": + return 0, reqJSONNameBytes(req) + case "Param json value": + return 0, reqJSONValueBytes(req) + case "Param xml name": + return 0, reqXMLNameBytes(req) + case "Param xml value": + return 0, reqXMLValueBytes(req) + case "Param xml attr name": + return 0, reqXMLAttrNameBytes(req) + case "Param xml attr value": + return 0, reqXMLAttrValueBytes(req) + case "Param multipart attr name": + return 0, reqMultipartNameBytes(req) + case "Param multipart attr value": + return 0, reqMultipartValueBytes(req) + } + + return 0, []byte{} +} + +func reqBytesToFindInEntire(where string, req *request.Request) (int, []byte) { + switch where { + case "Entire body": + return 0, req.Body + case "Entire body xml": + return 0, reqBodyXMLBytes(req) + case "Entire body json": + return 0, reqBodyJSONBytes(req) + case "Entire body multipart": + return 0, reqBodyMultipartBytes(req) + } + + return 0, []byte{} +} + +func reqBytesToFindInHTTP(where string, req *request.Request) (int, []byte) { + switch where { + case "HTTP host header": + return 0, []byte(req.Header("Host")) + case "HTTP user agent header": + return 0, []byte(req.Header("User-Agent")) + case "HTTP content type header": + return 0, []byte(req.Header("Content-Type")) + case "HTTP referer header": + return 0, []byte(req.Header("Referer")) + case "HTTP origin header": + return 0, []byte(req.Header("Origin")) + case "HTTP accept encoding header": + return 0, []byte(req.Header("Accept-Encoding")) + case "HTTP accept header": + return 0, []byte(req.Header("Accept")) + case "HTTP accept language header": + return 0, []byte(req.Header("Accept-Language")) + } + + return 0, []byte{} +} + +func reqParamNameBytes(req *request.Request) []byte { + bodyNameBytes := reqBodyNameBytes(req) + cookieNameBytes := reqCookieNameBytes(req) + jsonNameBytes := reqJSONNameBytes(req) + multipartNameBytes := reqMultipartNameBytes(req) + queryNameBytes := reqQueryNameBytes(req) + xmlNameBytes := reqXMLNameBytes(req) + xmlAttrNameBytes := reqXMLAttrNameBytes(req) + + totalLen := len(bodyNameBytes) + len(cookieNameBytes) + len(jsonNameBytes) + + len(multipartNameBytes) + len(queryNameBytes) + len(xmlNameBytes) + len(xmlAttrNameBytes) + + paramNameBytes := make([]byte, 0, totalLen) + + for _, b := range [][]byte{ + bodyNameBytes, + cookieNameBytes, + jsonNameBytes, + multipartNameBytes, + queryNameBytes, + xmlNameBytes, + xmlAttrNameBytes, + } { + paramNameBytes = append(paramNameBytes, b...) + } + + return paramNameBytes +} + +func reqParamValueBytes(req *request.Request) []byte { + bodyValueBytes := reqBodyValueBytes(req) + cookieValueBytes := reqCookieValueBytes(req) + jsonValueBytes := reqJSONValueBytes(req) + multipartValueBytes := reqMultipartValueBytes(req) + queryValueBytes := reqQueryValueBytes(req) + xmlValueBytes := reqXMLValueBytes(req) + xmlAttrValueBytes := reqXMLAttrValueBytes(req) + + totalLen := len(bodyValueBytes) + len(cookieValueBytes) + len(jsonValueBytes) + + len(multipartValueBytes) + len(queryValueBytes) + len(xmlValueBytes) + len(xmlAttrValueBytes) + + paramValueBytes := make([]byte, 0, totalLen) + + for _, b := range [][]byte{ + bodyValueBytes, + cookieValueBytes, + jsonValueBytes, + multipartValueBytes, + queryValueBytes, + xmlValueBytes, + xmlAttrValueBytes, + } { + paramValueBytes = append(paramValueBytes, b...) + } + + return paramValueBytes +} diff --git a/internal/match/extractor_req_body.go b/internal/match/extractor_req_body.go new file mode 100644 index 0000000..e9bfcac --- /dev/null +++ b/internal/match/extractor_req_body.go @@ -0,0 +1,49 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqBodyXMLBytes(req *request.Request) []byte { + if !req.HasXMLBody() { + return []byte{} + } + + return req.Body +} + +func reqBodyJSONBytes(req *request.Request) []byte { + if !req.HasJSONBody() { + return []byte{} + } + + return req.Body +} + +func reqBodyMultipartBytes(req *request.Request) []byte { + if !req.HasMultipartBody() { + return []byte{} + } + + return req.Body +} + +func reqBodyNameBytes(req *request.Request) []byte { + return reqBodyBytes(req, profile.ParamBodyName) +} + +func reqBodyValueBytes(req *request.Request) []byte { + return reqBodyBytes(req, profile.ParamBodyValue) +} + +func reqBodyBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewBodyParamFinder().Find(*req) { + if v, ok := e.(entrypoint.BodyParam); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_cookie.go b/internal/match/extractor_req_cookie.go new file mode 100644 index 0000000..b931d58 --- /dev/null +++ b/internal/match/extractor_req_cookie.go @@ -0,0 +1,25 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqCookieNameBytes(req *request.Request) []byte { + return reqCookieBytes(req, profile.CookieName) +} + +func reqCookieValueBytes(req *request.Request) []byte { + return reqCookieBytes(req, profile.CookieValue) +} + +func reqCookieBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewCookieFinder().Find(*req) { + if v, ok := e.(entrypoint.Cookie); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_json.go b/internal/match/extractor_req_json.go new file mode 100644 index 0000000..b6b071c --- /dev/null +++ b/internal/match/extractor_req_json.go @@ -0,0 +1,25 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqJSONNameBytes(req *request.Request) []byte { + return reqJSONBytes(req, profile.ParamJSONName) +} + +func reqJSONValueBytes(req *request.Request) []byte { + return reqJSONBytes(req, profile.ParamJSONValue) +} + +func reqJSONBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewJSONParamFinder().Find(*req) { + if v, ok := e.(entrypoint.JSONParam); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_multipart.go b/internal/match/extractor_req_multipart.go new file mode 100644 index 0000000..df86684 --- /dev/null +++ b/internal/match/extractor_req_multipart.go @@ -0,0 +1,25 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqMultipartNameBytes(req *request.Request) []byte { + return reqMultipartBytes(req, profile.ParamMultiAttrName) +} + +func reqMultipartValueBytes(req *request.Request) []byte { + return reqMultipartBytes(req, profile.ParamMultiAttrValue) +} + +func reqMultipartBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewMultipartFinder().Find(*req) { + if v, ok := e.(entrypoint.Multipart); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_query.go b/internal/match/extractor_req_query.go new file mode 100644 index 0000000..cfa365c --- /dev/null +++ b/internal/match/extractor_req_query.go @@ -0,0 +1,25 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqQueryNameBytes(req *request.Request) []byte { + return reqQueryBytes(req, profile.ParamURLName) +} + +func reqQueryValueBytes(req *request.Request) []byte { + return reqQueryBytes(req, profile.ParamURLValue) +} + +func reqQueryBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewQueryFinder().Find(*req) { + if v, ok := e.(entrypoint.Query); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_url.go b/internal/match/extractor_req_url.go new file mode 100644 index 0000000..00fbe4e --- /dev/null +++ b/internal/match/extractor_req_url.go @@ -0,0 +1,25 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqURLFolderBytes(req *request.Request) []byte { + return reqURLBytes(req, profile.URLPathFolder) +} + +func reqURLFileBytes(req *request.Request) []byte { + return reqURLBytes(req, profile.URLPathFile) +} + +func reqURLBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewURLFinder().Find(*req) { + if v, ok := e.(entrypoint.URL); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/extractor_req_xml.go b/internal/match/extractor_req_xml.go new file mode 100644 index 0000000..08ac15c --- /dev/null +++ b/internal/match/extractor_req_xml.go @@ -0,0 +1,33 @@ +package match + +import ( + "github.com/bountysecurity/gbounty/internal/entrypoint" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" +) + +func reqXMLNameBytes(req *request.Request) []byte { + return reqXMLBytes(req, profile.ParamXMLName) +} + +func reqXMLValueBytes(req *request.Request) []byte { + return reqXMLBytes(req, profile.ParamXMLValue) +} + +func reqXMLAttrNameBytes(req *request.Request) []byte { + return reqXMLBytes(req, profile.ParamXMLAttrName) +} + +func reqXMLAttrValueBytes(req *request.Request) []byte { + return reqXMLBytes(req, profile.ParamXMLAttrValue) +} + +func reqXMLBytes(req *request.Request, ipt profile.InsertionPointType) []byte { + var b []byte + for _, e := range entrypoint.NewXMLParamFinder().Find(*req) { + if v, ok := e.(entrypoint.XMLParam); ok && v.InsertionPointType() == ipt { + b = append(b, []byte(v.Value())...) + } + } + return b +} diff --git a/internal/match/match.go b/internal/match/match.go new file mode 100644 index 0000000..51dc650 --- /dev/null +++ b/internal/match/match.go @@ -0,0 +1,297 @@ +package match + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/bountysecurity/gbounty/internal/platform/http/client" + "github.com/bountysecurity/gbounty/internal/profile" + "github.com/bountysecurity/gbounty/internal/request" + "github.com/bountysecurity/gbounty/internal/response" + "github.com/bountysecurity/gbounty/kit/logger" + "github.com/bountysecurity/gbounty/kit/strings/occurrence" +) + +// Data is a data transfer object (DTO) used as the +// input payload for the Match function. It contains +// all the necessary data to check for match. +type Data struct { + Step profile.Step + Profile profile.Profile + Payload *string + PayloadDecode *string + Original *request.Request + Request *request.Request + Response *response.Response + CustomTokens map[string]string +} + +// Match checks whether there's a match for the given Data, +// and in such case it returns all the [occurrence.Occurrence]. +// +// Note: not all matches are accompanied by occurrences. +func Match(ctx context.Context, d Data) (bool, []occurrence.Occurrence) { + if d.Profile == nil { + return false, []occurrence.Occurrence{} + } + + var ( + ok bool + ngreps int + x interface { + GrepAt(idx int, rr map[string]string) (profile.Grep, error) + } + ) + switch d.Profile.GetType() { + case profile.TypeActive: + ngreps = len(d.Step.Greps) + x = d.Step + case profile.TypePassiveReq: + var prof *profile.Request + prof, ok = d.Profile.(*profile.Request) + if !ok { + logger.For(ctx).Errorf("Invalid profile: non-valid passive_request") + return false, []occurrence.Occurrence{} + } + + x = prof + ngreps = len(prof.Greps) + case profile.TypePassiveRes: + var prof *profile.Response + prof, ok = d.Profile.(*profile.Response) + if !ok { + logger.For(ctx).Errorf("Invalid profile: non-valid passive_response") + return false, []occurrence.Occurrence{} + } + + x = prof + ngreps = len(prof.Greps) + } + + // Safety check, we might need to revisit this. + // Its main purpose is for interactions with blind host. + if ngreps == 0 { + return false, []occurrence.Occurrence{} + } + + booleans := make([]bool, 0, ngreps) + operators := make([]profile.GrepOperator, 0, ngreps-1) + occurrences := make([]occurrence.Occurrence, 0) + + for idx := 0; idx < ngreps; idx++ { + // It must never fail here. + // Any error must be caught by the profile validation. + g, err := x.GrepAt(idx, d.CustomTokens) + if err != nil { + logger.For(ctx).Warnf( + "Grep (idx=%d) from profile (name='%s') could not be checked: %s", + idx, d.Profile.GetName(), err, + ) + continue + } + if !g.Enabled { + continue + } + + var ( + ok bool + occ []occurrence.Occurrence + ) + switch g.Type { + case profile.GrepTypeSimpleString: + ok, occ = matchSimpleString(g, d.Request, d.Response) + case profile.GrepTypeRegex: + ok, occ = matchRegex(g, d.Request, d.Response) + case profile.GrepTypeStatusCode: + ok, occ = matchStatusCode(g, d.Response) + case profile.GrepTypeTimeDelay: + ok, occ = matchTimeDelay(g, d.Response) + case profile.GrepTypeContentType: + ok, occ = matchContentType(g, d.Response) + case profile.GrepTypeContentLength: + ok, occ = matchContentLength(g, d.Response) + case profile.GrepTypeContentLengthDiff: + ok, occ = matchContentLengthDiff(ctx, g, d.Original, d.Response) + case profile.GrepTypeURLExtension: + ok, occ = matchURLExtension(g, d.Request) + case profile.GrepTypePayload: + ok, occ = matchPayload(g, d.Request, d.Response, d.Payload) + case profile.GrepTypePreEncodedPayload: + ok, occ = matchPayload(g, d.Request, d.Response, d.PayloadDecode) + } + + // We append the occurrences to the global list, + // if any. + occurrences = append(occurrences, occ...) + + // If it is the first grep, + // we ignore the operator. + if len(booleans) == 0 { + booleans = append(booleans, ok) + continue + } + + // Otherwise, we consider both, the boolean + // and the operator, like: ...AND/OR grep. + booleans = append(booleans, ok) + operators = append(operators, g.Operator) + } + + return evaluate(booleans, operators), occurrences +} + +func evaluate(booleans []bool, operators []profile.GrepOperator) bool { + // None of the greps were enabled, thus there's no match. + if len(booleans) == 0 { + return false + } + + result := booleans[0] + // Only one of the greps was enabled, thus it determines whether is match. + if len(booleans) == 1 { + return result + } + + // Otherwise, we evaluate the result of each grep + for i, op := range operators { + result = op.Match(result, booleans[i+1]) + } + + return result +} + +func matchSimpleString(g profile.Grep, req *request.Request, res *response.Response) (bool, []occurrence.Occurrence) { + offset, findIn := bytesToFindIn(g, req, res) + + var ( + occurrences []occurrence.Occurrence + grepValue = g.Value.AsString() + ) + + if g.Option.CaseSensitive() { + occurrences = occurrence.Find(string(findIn), grepValue) + } else { + occurrences = occurrence.Find(strings.ToLower(string(findIn)), strings.ToLower(grepValue)) + } + + for i := range occurrences { + occurrences[i][0] += offset + occurrences[i][1] += offset + } + + return len(occurrences) > 0, occurrences +} + +func matchRegex(g profile.Grep, req *request.Request, res *response.Response) (bool, []occurrence.Occurrence) { + offset, findIn := bytesToFindIn(g, req, res) + + value := g.Value.AsRegex() + if !g.Option.CaseSensitive() { + value = fmt.Sprintf("(?i)%s", value) + } + + occurrences := occurrence.FindRegexp(string(findIn), value) + for i := range occurrences { + occurrences[i][0] += offset + occurrences[i][1] += offset + } + + return len(occurrences) > 0, occurrences +} + +func matchStatusCode(g profile.Grep, res *response.Response) (bool, []occurrence.Occurrence) { + for _, code := range g.Value.AsStatusCodes() { + if res.Code == code { + return true, []occurrence.Occurrence{} + } + } + return false, []occurrence.Occurrence{} +} + +func matchTimeDelay(g profile.Grep, res *response.Response) (bool, []occurrence.Occurrence) { + delay := g.Value.AsTimeDelaySeconds() // as seconds + margin := 2 // +/- 2s + return delay-margin <= int(res.Time.Seconds()) && int(res.Time.Seconds()) <= delay+margin, []occurrence.Occurrence{} +} + +func matchContentType(g profile.Grep, res *response.Response) (bool, []occurrence.Occurrence) { + for _, contentType := range g.Value.AsContentTypes() { + if strings.EqualFold(contentType, res.ContentType()) { + return true, []occurrence.Occurrence{} + } + } + return false, []occurrence.Occurrence{} +} + +func matchContentLength(g profile.Grep, res *response.Response) (bool, []occurrence.Occurrence) { + // We calculate a 20% of margin + length := g.Value.AsContentLength() + margin := length * 20 / 100 + return length-margin <= res.Length() && res.Length() <= length+margin, []occurrence.Occurrence{} +} + +func matchContentLengthDiff( + ctx context.Context, + g profile.Grep, + origReq *request.Request, + res *response.Response, +) (bool, []occurrence.Occurrence) { + // First, we need to get the original response + // in order to compare the lengths. + origRes, err := originalResponse(ctx, origReq) + if err != nil { + // In case of error, we cannot check the response's length. + // So, we log the error (to notice the user), and return false. + logger.For(ctx).Errorf("Couldn't check response's length: couldn't get original response: %s", err.Error()) + return false, []occurrence.Occurrence{} + } + + // Then, we calculate the absolute difference. + diff := res.Length() - origRes.Length() + if diff < 0 { + diff *= -1 + } + + // Finally, we consider that there is a "match", if the + // difference is greater or equal than the value specified in the profile. + return diff >= g.Value.AsContentLength(), []occurrence.Occurrence{} +} + +func originalResponse(ctx context.Context, orig *request.Request) (response.Response, error) { + // First, we clone the original request to avoid side effects. + req := orig.Clone() + + // Then, we set the timeout to 10 seconds. + // If the original request has a greater timeout, then we leave it. + const timeout = 10 * time.Second + if req.Timeout < timeout { + req.Timeout = timeout + } + + // Finally, we perform the request. + httpClient := client.New() + return httpClient.Do(ctx, orig) +} + +func matchURLExtension(g profile.Grep, req *request.Request) (bool, []occurrence.Occurrence) { + requestURLExtension := filepath.Ext(req.Path) + for _, urlExtension := range g.Value.AsURLExtensions() { + if strings.EqualFold(urlExtension, requestURLExtension) { + return true, []occurrence.Occurrence{} + } + } + return false, []occurrence.Occurrence{} +} + +func matchPayload(g profile.Grep, req *request.Request, res *response.Response, payload *string) (bool, []occurrence.Occurrence) { + _, findIn := bytesToFindIn(g, req, res) + + if g.Option.CaseSensitive() { + return strings.Contains(string(findIn), *payload), []occurrence.Occurrence{} + } + + return strings.Contains(strings.ToLower(string(findIn)), strings.ToLower(*payload)), []occurrence.Occurrence{} +} diff --git a/internal/match/match_test.go b/internal/match/match_test.go new file mode 100644 index 0000000..4537b75 --- /dev/null +++ b/internal/match/match_test.go @@ -0,0 +1,57 @@ +//nolint:testpackage +package match + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bountysecurity/gbounty/internal/profile" +) + +func Test_evaluate(t *testing.T) { + t.Parallel() + + tcs := map[string]struct { + expected bool + booleans []bool + operators []profile.GrepOperator + }{ + "nil": {expected: false}, + "none": {expected: false, booleans: []bool{}}, + "true": {expected: true, booleans: []bool{true}}, + "false": {expected: false, booleans: []bool{false}}, + + "true && true": {expected: true, booleans: []bool{true, true}, operators: []profile.GrepOperator{profile.GrepOperatorAnd}}, + "true && false": {expected: false, booleans: []bool{true, false}, operators: []profile.GrepOperator{profile.GrepOperatorAnd}}, + "false && true": {expected: false, booleans: []bool{false, true}, operators: []profile.GrepOperator{profile.GrepOperatorAnd}}, + "false && false": {expected: false, booleans: []bool{false, false}, operators: []profile.GrepOperator{profile.GrepOperatorAnd}}, + + "true || true": {expected: true, booleans: []bool{true, true}, operators: []profile.GrepOperator{profile.GrepOperatorOr}}, + "true || false": {expected: true, booleans: []bool{true, false}, operators: []profile.GrepOperator{profile.GrepOperatorOr}}, + "false || true": {expected: true, booleans: []bool{false, true}, operators: []profile.GrepOperator{profile.GrepOperatorOr}}, + "false || false": {expected: false, booleans: []bool{false, false}, operators: []profile.GrepOperator{profile.GrepOperatorOr}}, + + "true && !true": {expected: false, booleans: []bool{true, true}, operators: []profile.GrepOperator{profile.GrepOperatorAndNot}}, + "true && !false": {expected: true, booleans: []bool{true, false}, operators: []profile.GrepOperator{profile.GrepOperatorAndNot}}, + "false && !true": {expected: false, booleans: []bool{false, true}, operators: []profile.GrepOperator{profile.GrepOperatorAndNot}}, + "false && !false": {expected: false, booleans: []bool{false, false}, operators: []profile.GrepOperator{profile.GrepOperatorAndNot}}, + + "true || !true": {expected: true, booleans: []bool{true, true}, operators: []profile.GrepOperator{profile.GrepOperatorOrNot}}, + "true || !false": {expected: true, booleans: []bool{true, false}, operators: []profile.GrepOperator{profile.GrepOperatorOrNot}}, + "false || !true": {expected: false, booleans: []bool{false, true}, operators: []profile.GrepOperator{profile.GrepOperatorOrNot}}, + "false || !false": {expected: true, booleans: []bool{false, false}, operators: []profile.GrepOperator{profile.GrepOperatorOrNot}}, + + "true && false || true": {expected: true, booleans: []bool{true, false, true}, operators: []profile.GrepOperator{profile.GrepOperatorAnd, profile.GrepOperatorOr}}, + "false && false || true": {expected: true, booleans: []bool{false, false, true}, operators: []profile.GrepOperator{profile.GrepOperatorAnd, profile.GrepOperatorOr}}, + "true || true && false": {expected: false, booleans: []bool{true, true, false}, operators: []profile.GrepOperator{profile.GrepOperatorOr, profile.GrepOperatorAnd}}, + } + + for name, tc := range tcs { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, evaluate(tc.booleans, tc.operators)) + }) + } +}