Compare commits

..

8 Commits

Author SHA1 Message Date
496bf58de2 Bump ua-parser-js from 0.7.31 to 0.7.33 in /admin
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-27 10:42:23 +00:00
f7def87d0f Add HTTP header support to string literal matching 2022-03-31 15:23:56 +02:00
aa9822854d Rename search package to filter package 2022-03-31 15:12:54 +02:00
2ce4218a30 Add filter support for HTTP headers 2022-03-31 14:53:40 +02:00
fd27955e11 Sort HTTP headers 2022-03-31 12:07:35 +02:00
426a7d5f96 Add "New request" button to Sender page 2022-03-31 11:23:17 +02:00
21b679dc91 Add new sponsorship options 2022-03-30 13:02:58 +02:00
e4f468d4d2 Publish Docker image to ghcr.io and docker.io 2022-03-30 11:50:16 +02:00
31 changed files with 664 additions and 387 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms github: dstotijn
patreon: dstotijn patreon: dstotijn
custom: "https://www.paypal.com/paypalme/dstotijn"

View File

@ -35,7 +35,7 @@ linters-settings:
godot: godot:
capital: true capital: true
ireturn: ireturn:
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/search.Expression" allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/filter.Expression"
issues: issues:
exclude-rules: exclude-rules:

View File

@ -69,6 +69,31 @@ scoop:
description: An HTTP toolkit for security research. description: An HTTP toolkit for security research.
license: MIT license: MIT
dockers:
- extra_files:
- go.mod
- go.sum
- pkg
- cmd
- admin
image_templates:
- "ghcr.io/dstotijn/hetty:{{ .Version }}"
- "ghcr.io/dstotijn/hetty:{{ .Major }}"
- "ghcr.io/dstotijn/hetty:{{ .Major }}.{{ .Minor }}"
- "ghcr.io/dstotijn/hetty:latest"
- "dstotijn/hetty:{{ .Version }}"
- "dstotijn/hetty:{{ .Major }}"
- "dstotijn/hetty:{{ .Major }}.{{ .Minor }}"
- "dstotijn/hetty:latest"
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source=https://github.com/dstotijn/hetty"
- "--build-arg=HETTY_VERSION={{.Version}}"
checksum: checksum:
name_template: "checksums.txt" name_template: "checksums.txt"

View File

@ -64,8 +64,18 @@ Alternatively, you can [download the latest release from
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
architecture, and move the binary to a directory in your `$PATH`. If your OS is architecture, and move the binary to a directory in your `$PATH`. If your OS is
not available for one of the package managers or not listed in the GitHub not available for one of the package managers or not listed in the GitHub
releases, you can compile from source _(link coming soon)_ or use a Docker image releases, you can compile from source _(link coming soon)_.
_(link coming soon)_.
#### Docker
Docker images are distributed via [GitHub's Container registry](https://github.com/dstotijn/hetty/pkgs/container/hetty)
and [Docker Hub](https://hub.docker.com/r/dstotijn/hetty). To run Hetty via with a volume for database and certificate
storage, and port 8080 forwarded:
```
docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 \
ghcr.io/dstotijn/hetty:latest
```
### Usage ### Usage
@ -136,9 +146,11 @@ Guidelines](CONTRIBUTING.md) for details.
## Sponsors ## Sponsors
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty"> <p><a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines"> <img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
</a> </a></p>
💖 Are you enjoying Hetty? You can [sponsor me](https://github.com/sponsors/dstotijn)!
## License ## License

View File

@ -7,7 +7,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair"; import { KeyValuePair } from "lib/components/KeyValuePair";
import Link from "lib/components/Link"; import Link from "lib/components/Link";
import RequestTabs from "lib/components/RequestTabs"; import RequestTabs from "lib/components/RequestTabs";
import ResponseStatus from "lib/components/ResponseStatus"; import ResponseStatus from "lib/components/ResponseStatus";
@ -112,11 +112,11 @@ function EditRequest(): JSX.Element {
newQueryParams.push({ key: "", value: "" }); newQueryParams.push({ key: "", value: "" });
setQueryParams(newQueryParams); setQueryParams(newQueryParams);
const newReqHeaders = sortKeyValuePairs(interceptedRequest.headers || []); const newReqHeaders = interceptedRequest.headers || [];
setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
setResBody(interceptedRequest.response?.body || ""); setResBody(interceptedRequest.response?.body || "");
const newResHeaders = sortKeyValuePairs(interceptedRequest.response?.headers || []); const newResHeaders = interceptedRequest.response?.headers || [];
setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
}, },
}); });

View File

@ -76,6 +76,7 @@ function Search(): JSX.Element {
<ClickAwayListener onClickAway={handleClickAway}> <ClickAwayListener onClickAway={handleClickAway}>
<Paper <Paper
component="form" component="form"
autoComplete="off"
onSubmit={handleSubmit} onSubmit={handleSubmit}
ref={filterRef} ref={filterRef}
sx={{ sx={{
@ -109,6 +110,8 @@ function Search(): JSX.Element {
value={searchExpr} value={searchExpr}
onChange={(e) => setSearchExpr(e.target.value)} onChange={(e) => setSearchExpr(e.target.value)}
onFocus={() => setFilterOpen(true)} onFocus={() => setFilterOpen(true)}
autoCorrect="false"
spellCheck="false"
/> />
<Tooltip title="Search"> <Tooltip title="Search">
<IconButton type="submit" sx={{ padding: 1.25 }}> <IconButton type="submit" sx={{ padding: 1.25 }}>

View File

@ -1,8 +1,9 @@
import { Alert, Box, Button, Typography } from "@mui/material"; import AddIcon from "@mui/icons-material/Add";
import { Alert, Box, Button, Fab, Tooltip, Typography, useTheme } from "@mui/material";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useState } from "react"; import React, { useState } from "react";
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair"; import { KeyValuePair } from "lib/components/KeyValuePair";
import RequestTabs from "lib/components/RequestTabs"; import RequestTabs from "lib/components/RequestTabs";
import Response from "lib/components/Response"; import Response from "lib/components/Response";
import SplitPane from "lib/components/SplitPane"; import SplitPane from "lib/components/SplitPane";
@ -17,15 +18,21 @@ import { queryParamsFromURL } from "lib/queryParamsFromURL";
import updateKeyPairItem from "lib/updateKeyPairItem"; import updateKeyPairItem from "lib/updateKeyPairItem";
import updateURLQueryParams from "lib/updateURLQueryParams"; import updateURLQueryParams from "lib/updateURLQueryParams";
const defaultMethod = HttpMethod.Get;
const defaultProto = HttpProto.Http20;
const emptyKeyPair = [{ key: "", value: "" }];
function EditRequest(): JSX.Element { function EditRequest(): JSX.Element {
const router = useRouter(); const router = useRouter();
const reqId = router.query.id as string | undefined; const reqId = router.query.id as string | undefined;
const [method, setMethod] = useState(HttpMethod.Get); const theme = useTheme();
const [method, setMethod] = useState(defaultMethod);
const [url, setURL] = useState(""); const [url, setURL] = useState("");
const [proto, setProto] = useState(HttpProto.Http20); const [proto, setProto] = useState(defaultProto);
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]); const [queryParams, setQueryParams] = useState<KeyValuePair[]>(emptyKeyPair);
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]); const [headers, setHeaders] = useState<KeyValuePair[]>(emptyKeyPair);
const [body, setBody] = useState(""); const [body, setBody] = useState("");
const handleQueryParamChange = (key: string, value: string, idx: number) => { const handleQueryParamChange = (key: string, value: string, idx: number) => {
@ -83,7 +90,7 @@ function EditRequest(): JSX.Element {
newQueryParams.push({ key: "", value: "" }); newQueryParams.push({ key: "", value: "" });
setQueryParams(newQueryParams); setQueryParams(newQueryParams);
const newHeaders = sortKeyValuePairs(senderRequest.headers || []); const newHeaders = senderRequest.headers || [];
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
setResponse(senderRequest.response); setResponse(senderRequest.response);
}, },
@ -131,8 +138,26 @@ function EditRequest(): JSX.Element {
createOrUpdateRequestAndSend(); createOrUpdateRequestAndSend();
}; };
const handleNewRequest = () => {
setURL("");
setMethod(defaultMethod);
setProto(defaultProto);
setQueryParams(emptyKeyPair);
setHeaders(emptyKeyPair);
setBody("");
setResponse(null);
router.push(`/sender`);
};
return ( return (
<Box display="flex" flexDirection="column" height="100%" gap={2}> <Box display="flex" flexDirection="column" height="100%" gap={2}>
<Box sx={{ position: "absolute", bottom: theme.spacing(2), right: theme.spacing(2) }}>
<Tooltip title="New request">
<Fab color="primary" onClick={handleNewRequest}>
<AddIcon />
</Fab>
</Tooltip>
</Box>
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}> <Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}> <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<UrlBar <UrlBar

View File

@ -184,20 +184,4 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
); );
} }
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
const sorted = [...items];
sorted.sort((a, b) => {
if (a.key < b.key) {
return -1;
}
if (a.key > b.key) {
return 1;
}
return 0;
});
return sorted;
}
export default KeyValuePairTable; export default KeyValuePairTable;

View File

@ -1,6 +1,5 @@
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { sortKeyValuePairs } from "./KeyValuePair";
import ResponseTabs from "./ResponseTabs"; import ResponseTabs from "./ResponseTabs";
import ResponseStatus from "lib/components/ResponseStatus"; import ResponseStatus from "lib/components/ResponseStatus";
@ -29,7 +28,7 @@ function Response({ response }: ResponseProps): JSX.Element {
</Box> </Box>
<ResponseTabs <ResponseTabs
body={response?.body} body={response?.body}
headers={sortKeyValuePairs(response?.headers || [])} headers={response?.headers || []}
hasResponse={response !== undefined && response !== null} hasResponse={response !== undefined && response !== null}
/> />
</Box> </Box>

View File

@ -5573,9 +5573,9 @@ typescript@^4.0.3:
integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
ua-parser-js@^0.7.30: ua-parser-js@^0.7.30:
version "0.7.31" version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
unbox-primitive@^1.0.1: unbox-primitive@^1.0.1:
version "1.0.1" version "1.0.1"

View File

@ -49,3 +49,17 @@ func UnmarshalURL(v interface{}) (*url.URL, error) {
return u, nil return u, nil
} }
type HTTPHeaders []HTTPHeader
func (h HTTPHeaders) Len() int {
return len(h)
}
func (h HTTPHeaders) Less(i, j int) bool {
return h[i].Key < h[j].Key
}
func (h HTTPHeaders) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}

View File

@ -11,18 +11,19 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/proxy/intercept" "github.com/dstotijn/hetty/pkg/proxy/intercept"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
"github.com/dstotijn/hetty/pkg/sender" "github.com/dstotijn/hetty/pkg/sender"
) )
@ -124,6 +125,8 @@ func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
}) })
} }
} }
sort.Sort(HTTPHeaders(log.Headers))
} }
if reqLog.Response != nil { if reqLog.Response != nil {
@ -172,6 +175,8 @@ func parseResponseLog(resLog reqlog.ResponseLog) (HTTPResponseLog, error) {
}) })
} }
} }
sort.Sort(HTTPHeaders(httpResLog.Headers))
} }
return httpResLog, nil return httpResLog, nil
@ -634,7 +639,7 @@ func (r *mutationResolver) UpdateInterceptSettings(
} }
if input.RequestFilter != nil && *input.RequestFilter != "" { if input.RequestFilter != nil && *input.RequestFilter != "" {
expr, err := search.ParseQuery(*input.RequestFilter) expr, err := filter.ParseQuery(*input.RequestFilter)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse request filter: %w", err) return nil, fmt.Errorf("could not parse request filter: %w", err)
} }
@ -643,7 +648,7 @@ func (r *mutationResolver) UpdateInterceptSettings(
} }
if input.ResponseFilter != nil && *input.ResponseFilter != "" { if input.ResponseFilter != nil && *input.ResponseFilter != "" {
expr, err := search.ParseQuery(*input.ResponseFilter) expr, err := filter.ParseQuery(*input.ResponseFilter)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse response filter: %w", err) return nil, fmt.Errorf("could not parse response filter: %w", err)
} }
@ -710,6 +715,8 @@ func parseSenderRequest(req sender.Request) (SenderRequest, error) {
}) })
} }
} }
sort.Sort(HTTPHeaders(senderReq.Headers))
} }
if len(req.Body) > 0 { if len(req.Body) > 0 {
@ -765,6 +772,8 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
}) })
} }
} }
sort.Sort(HTTPHeaders(httpReq.Headers))
} }
if req.Body != nil { if req.Body != nil {
@ -815,6 +824,8 @@ func parseHTTPResponse(res *http.Response) (HTTPResponse, error) {
}) })
} }
} }
sort.Sort(HTTPHeaders(httpRes.Headers))
} }
if res.Body != nil { if res.Body != nil {
@ -905,43 +916,43 @@ func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
return scopeRules return scopeRules
} }
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) { func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (findFilter reqlog.FindRequestsFilter, err error) {
if input == nil { if input == nil {
return return
} }
if input.OnlyInScope != nil { if input.OnlyInScope != nil {
filter.OnlyInScope = *input.OnlyInScope findFilter.OnlyInScope = *input.OnlyInScope
} }
if input.SearchExpression != nil && *input.SearchExpression != "" { if input.SearchExpression != nil && *input.SearchExpression != "" {
expr, err := search.ParseQuery(*input.SearchExpression) expr, err := filter.ParseQuery(*input.SearchExpression)
if err != nil { if err != nil {
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err) return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
} }
filter.SearchExpr = expr findFilter.SearchExpr = expr
} }
return return
} }
func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (filter sender.FindRequestsFilter, err error) { func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (findFilter sender.FindRequestsFilter, err error) {
if input == nil { if input == nil {
return return
} }
if input.OnlyInScope != nil { if input.OnlyInScope != nil {
filter.OnlyInScope = *input.OnlyInScope findFilter.OnlyInScope = *input.OnlyInScope
} }
if input.SearchExpression != nil && *input.SearchExpression != "" { if input.SearchExpression != nil && *input.SearchExpression != "" {
expr, err := search.ParseQuery(*input.SearchExpression) expr, err := filter.ParseQuery(*input.SearchExpression)
if err != nil { if err != nil {
return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err) return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
} }
filter.SearchExpr = expr findFilter.SearchExpr = expr
} }
return return

View File

@ -15,9 +15,9 @@ import (
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
//nolint:gosec //nolint:gosec
@ -45,7 +45,7 @@ func TestUpsertProject(t *testing.T) {
database := DatabaseFromBadgerDB(badgerDB) database := DatabaseFromBadgerDB(badgerDB)
defer database.Close() defer database.Close()
searchExpr, err := search.ParseQuery("foo AND bar OR NOT baz") searchExpr, err := filter.ParseQuery("foo AND bar OR NOT baz")
if err != nil { if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err) t.Fatalf("unexpected error (expected: nil, got: %v)", err)
} }

View File

@ -1,4 +1,4 @@
package search package filter
import ( import (
"encoding/gob" "encoding/gob"
@ -78,8 +78,10 @@ func (rl *RegexpLiteral) UnmarshalBinary(data []byte) error {
} }
func init() { func init() {
gob.Register(PrefixExpression{}) // The `filter` package was previously named `search`.
gob.Register(InfixExpression{}) // We use the legacy names for backwards compatibility with existing database data.
gob.Register(StringLiteral{}) gob.RegisterName("github.com/dstotijn/hetty/pkg/search.PrefixExpression", PrefixExpression{})
gob.Register(RegexpLiteral{}) gob.RegisterName("github.com/dstotijn/hetty/pkg/search.InfixExpression", InfixExpression{})
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.StringLiteral", StringLiteral{})
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.RegexpLiteral", RegexpLiteral{})
} }

212
pkg/filter/ast_test.go Normal file
View File

@ -0,0 +1,212 @@
package filter_test
import (
"regexp"
"testing"
"github.com/dstotijn/hetty/pkg/filter"
)
func TestExpressionString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expression filter.Expression
expected string
}{
{
name: "string literal expression",
expression: filter.StringLiteral{Value: "foobar"},
expected: `"foobar"`,
},
{
name: "boolean expression with equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" = "bar")`,
},
{
name: "boolean expression with not equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpNotEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" != "bar")`,
},
{
name: "boolean expression with greater than operator",
expression: filter.InfixExpression{
Operator: filter.TokOpGt,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" > "bar")`,
},
{
name: "boolean expression with less than operator",
expression: filter.InfixExpression{
Operator: filter.TokOpLt,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" < "bar")`,
},
{
name: "boolean expression with greater than or equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpGtEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" >= "bar")`,
},
{
name: "boolean expression with less than or equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpLtEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" <= "bar")`,
},
{
name: "boolean expression with regular expression operator",
expression: filter.InfixExpression{
Operator: filter.TokOpRe,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" =~ "bar")`,
},
{
name: "boolean expression with not regular expression operator",
expression: filter.InfixExpression{
Operator: filter.TokOpNotRe,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" !~ "bar")`,
},
{
name: "boolean expression with AND, OR and NOT operators",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.StringLiteral{Value: "bar"},
Right: filter.PrefixExpression{
Operator: filter.TokOpNot,
Right: filter.StringLiteral{Value: "baz"},
},
},
},
expected: `("foo" AND ("bar" OR (NOT "baz")))`,
},
{
name: "boolean expression with nested group",
expression: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.PrefixExpression{
Operator: filter.TokOpNot,
Right: filter.StringLiteral{Value: "baz"},
},
},
expected: `(("foo" AND "bar") OR (NOT "baz"))`,
},
{
name: "implicit boolean expression with string literal operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.StringLiteral{Value: "baz"},
},
expected: `(("foo" AND "bar") AND "baz")`,
},
{
name: "implicit boolean expression nested in group",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" AND "bar")`,
},
{
name: "implicit and explicit boolean expression with string literal operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.StringLiteral{Value: "bar"},
Right: filter.StringLiteral{Value: "baz"},
},
},
Right: filter.StringLiteral{Value: "yolo"},
},
expected: `(("foo" AND ("bar" OR "baz")) AND "yolo")`,
},
{
name: "implicit boolean expression with comparison operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.InfixExpression{
Operator: filter.TokOpRe,
Left: filter.StringLiteral{Value: "baz"},
Right: filter.RegexpLiteral{regexp.MustCompile("yolo")},
},
},
expected: `(("foo" = "bar") AND ("baz" =~ "yolo"))`,
},
{
name: "eq operator takes precedence over boolean ops",
expression: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "baz"},
Right: filter.StringLiteral{Value: "yolo"},
},
},
expected: `(("foo" = "bar") OR ("baz" = "yolo"))`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.expression.String()
if tt.expected != got {
t.Errorf("expected: %v, got: %v", tt.expected, got)
}
})
}
}

82
pkg/filter/http.go Normal file
View File

@ -0,0 +1,82 @@
package filter
import (
"errors"
"fmt"
"net/http"
)
func MatchHTTPHeaders(op TokenType, expr Expression, headers http.Header) (bool, error) {
if headers == nil {
return false, nil
}
switch op {
case TokOpEq:
strLiteral, ok := expr.(StringLiteral)
if !ok {
return false, errors.New("filter: expression must be a string literal")
}
// Return `true` if at least one header (<key>: <value>) is equal to the string literal.
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return true, nil
}
}
}
return false, nil
case TokOpNotEq:
strLiteral, ok := expr.(StringLiteral)
if !ok {
return false, errors.New("filter: expression must be a string literal")
}
// Return `true` if none of the headers (<key>: <value>) are equal to the string literal.
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return false, nil
}
}
}
return true, nil
case TokOpRe:
re, ok := expr.(RegexpLiteral)
if !ok {
return false, errors.New("filter: expression must be a regular expression")
}
// Return `true` if at least one header (<key>: <value>) matches the regular expression.
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return true, nil
}
}
}
return false, nil
case TokOpNotRe:
re, ok := expr.(RegexpLiteral)
if !ok {
return false, errors.New("filter: expression must be a regular expression")
}
// Return `true` if none of the headers (<key>: <value>) match the regular expression.
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return false, nil
}
}
}
return true, nil
default:
return false, fmt.Errorf("filter: unsupported operator %q", op.String())
}
}

View File

@ -1,4 +1,4 @@
package search package filter
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package search package filter
import "testing" import "testing"

View File

@ -1,4 +1,4 @@
package search package filter
import ( import (
"fmt" "fmt"
@ -88,7 +88,7 @@ func ParseQuery(input string) (expr Expression, err error) {
p.nextToken() p.nextToken()
if p.curTokenIs(TokEOF) { if p.curTokenIs(TokEOF) {
return nil, fmt.Errorf("search: unexpected EOF") return nil, fmt.Errorf("filter: unexpected EOF")
} }
for !p.curTokenIs(TokEOF) { for !p.curTokenIs(TokEOF) {
@ -96,7 +96,7 @@ func ParseQuery(input string) (expr Expression, err error) {
switch { switch {
case err != nil: case err != nil:
return nil, fmt.Errorf("search: could not parse expression: %w", err) return nil, fmt.Errorf("filter: could not parse expression: %w", err)
case expr == nil: case expr == nil:
expr = right expr = right
default: default:

View File

@ -1,4 +1,4 @@
package search package filter
import ( import (
"errors" "errors"
@ -20,7 +20,7 @@ func TestParseQuery(t *testing.T) {
name: "empty query", name: "empty query",
input: "", input: "",
expectedExpression: nil, expectedExpression: nil,
expectedError: errors.New("search: unexpected EOF"), expectedError: errors.New("filter: unexpected EOF"),
}, },
{ {
name: "string literal expression", name: "string literal expression",

View File

@ -11,10 +11,10 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proxy/intercept" "github.com/dstotijn/hetty/pkg/proxy/intercept"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
"github.com/dstotijn/hetty/pkg/sender" "github.com/dstotijn/hetty/pkg/sender"
) )
@ -59,17 +59,17 @@ type Settings struct {
// Request log settings // Request log settings
ReqLogBypassOutOfScope bool ReqLogBypassOutOfScope bool
ReqLogOnlyFindInScope bool ReqLogOnlyFindInScope bool
ReqLogSearchExpr search.Expression ReqLogSearchExpr filter.Expression
// Intercept settings // Intercept settings
InterceptRequests bool InterceptRequests bool
InterceptResponses bool InterceptResponses bool
InterceptRequestFilter search.Expression InterceptRequestFilter filter.Expression
InterceptResponseFilter search.Expression InterceptResponseFilter filter.Expression
// Sender settings // Sender settings
SenderOnlyFindInScope bool SenderOnlyFindInScope bool
SenderSearchExpr search.Expression SenderSearchExpr filter.Expression
// Scope settings // Scope settings
ScopeRules []scope.Rule ScopeRules []scope.Rule

View File

@ -10,8 +10,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
//nolint:unparam //nolint:unparam
@ -68,22 +68,22 @@ var resFilterKeyFns = map[string]func(res *http.Response) (string, error){
} }
// MatchRequestFilter returns true if an HTTP request matches the request filter expression. // MatchRequestFilter returns true if an HTTP request matches the request filter expression.
func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) { func MatchRequestFilter(req *http.Request, expr filter.Expression) (bool, error) {
switch e := expr.(type) { switch e := expr.(type) {
case search.PrefixExpression: case filter.PrefixExpression:
return matchReqPrefixExpr(req, e) return matchReqPrefixExpr(req, e)
case search.InfixExpression: case filter.InfixExpression:
return matchReqInfixExpr(req, e) return matchReqInfixExpr(req, e)
case search.StringLiteral: case filter.StringLiteral:
return matchReqStringLiteral(req, e) return matchReqStringLiteral(req, e)
default: default:
return false, fmt.Errorf("expression type (%T) not supported", expr) return false, fmt.Errorf("expression type (%T) not supported", expr)
} }
} }
func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool, error) { func matchReqPrefixExpr(req *http.Request, expr filter.PrefixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpNot: case filter.TokOpNot:
match, err := MatchRequestFilter(req, expr.Right) match, err := MatchRequestFilter(req, expr.Right)
if err != nil { if err != nil {
return false, err return false, err
@ -95,9 +95,9 @@ func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool,
} }
} }
func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, error) { func matchReqInfixExpr(req *http.Request, expr filter.InfixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpAnd: case filter.TokOpAnd:
left, err := MatchRequestFilter(req, expr.Left) left, err := MatchRequestFilter(req, expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -109,7 +109,7 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
} }
return left && right, nil return left && right, nil
case search.TokOpOr: case filter.TokOpOr:
left, err := MatchRequestFilter(req, expr.Left) left, err := MatchRequestFilter(req, expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -123,7 +123,7 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
return left || right, nil return left || right, nil
} }
left, ok := expr.Left.(search.StringLiteral) left, ok := expr.Left.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("left operand must be a string literal") return false, errors.New("left operand must be a string literal")
} }
@ -133,21 +133,30 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
return false, fmt.Errorf("failed to get string literal from request for left operand: %w", err) return false, fmt.Errorf("failed to get string literal from request for left operand: %w", err)
} }
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { if leftVal == "headers" {
right, ok := expr.Right.(search.RegexpLiteral) match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
return match, nil
}
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
right, ok := expr.Right.(filter.RegexpLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a regular expression") return false, errors.New("right operand must be a regular expression")
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpRe: case filter.TokOpRe:
return right.MatchString(leftVal), nil return right.MatchString(leftVal), nil
case search.TokOpNotRe: case filter.TokOpNotRe:
return !right.MatchString(leftVal), nil return !right.MatchString(leftVal), nil
} }
} }
right, ok := expr.Right.(search.StringLiteral) right, ok := expr.Right.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a string literal") return false, errors.New("right operand must be a string literal")
} }
@ -158,20 +167,20 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpEq: case filter.TokOpEq:
return leftVal == rightVal, nil return leftVal == rightVal, nil
case search.TokOpNotEq: case filter.TokOpNotEq:
return leftVal != rightVal, nil return leftVal != rightVal, nil
case search.TokOpGt: case filter.TokOpGt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal > rightVal, nil return leftVal > rightVal, nil
case search.TokOpLt: case filter.TokOpLt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal < rightVal, nil return leftVal < rightVal, nil
case search.TokOpGtEq: case filter.TokOpGtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal >= rightVal, nil return leftVal >= rightVal, nil
case search.TokOpLtEq: case filter.TokOpLtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal <= rightVal, nil return leftVal <= rightVal, nil
default: default:
@ -188,7 +197,18 @@ func getMappedStringLiteralFromReq(req *http.Request, s string) (string, error)
return s, nil return s, nil
} }
func matchReqStringLiteral(req *http.Request, strLiteral search.StringLiteral) (bool, error) { func matchReqStringLiteral(req *http.Request, strLiteral filter.StringLiteral) (bool, error) {
for key, values := range req.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range reqFilterKeyFns { for _, fn := range reqFilterKeyFns {
value, err := fn(req) value, err := fn(req)
if err != nil { if err != nil {
@ -259,22 +279,22 @@ func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
} }
// MatchResponseFilter returns true if an HTTP response matches the response filter expression. // MatchResponseFilter returns true if an HTTP response matches the response filter expression.
func MatchResponseFilter(res *http.Response, expr search.Expression) (bool, error) { func MatchResponseFilter(res *http.Response, expr filter.Expression) (bool, error) {
switch e := expr.(type) { switch e := expr.(type) {
case search.PrefixExpression: case filter.PrefixExpression:
return matchResPrefixExpr(res, e) return matchResPrefixExpr(res, e)
case search.InfixExpression: case filter.InfixExpression:
return matchResInfixExpr(res, e) return matchResInfixExpr(res, e)
case search.StringLiteral: case filter.StringLiteral:
return matchResStringLiteral(res, e) return matchResStringLiteral(res, e)
default: default:
return false, fmt.Errorf("expression type (%T) not supported", expr) return false, fmt.Errorf("expression type (%T) not supported", expr)
} }
} }
func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool, error) { func matchResPrefixExpr(res *http.Response, expr filter.PrefixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpNot: case filter.TokOpNot:
match, err := MatchResponseFilter(res, expr.Right) match, err := MatchResponseFilter(res, expr.Right)
if err != nil { if err != nil {
return false, err return false, err
@ -286,9 +306,9 @@ func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool,
} }
} }
func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, error) { func matchResInfixExpr(res *http.Response, expr filter.InfixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpAnd: case filter.TokOpAnd:
left, err := MatchResponseFilter(res, expr.Left) left, err := MatchResponseFilter(res, expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -300,7 +320,7 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
} }
return left && right, nil return left && right, nil
case search.TokOpOr: case filter.TokOpOr:
left, err := MatchResponseFilter(res, expr.Left) left, err := MatchResponseFilter(res, expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -314,7 +334,7 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
return left || right, nil return left || right, nil
} }
left, ok := expr.Left.(search.StringLiteral) left, ok := expr.Left.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("left operand must be a string literal") return false, errors.New("left operand must be a string literal")
} }
@ -324,21 +344,30 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
return false, fmt.Errorf("failed to get string literal from response for left operand: %w", err) return false, fmt.Errorf("failed to get string literal from response for left operand: %w", err)
} }
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { if leftVal == "headers" {
right, ok := expr.Right.(search.RegexpLiteral) match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, res.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
return match, nil
}
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
right, ok := expr.Right.(filter.RegexpLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a regular expression") return false, errors.New("right operand must be a regular expression")
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpRe: case filter.TokOpRe:
return right.MatchString(leftVal), nil return right.MatchString(leftVal), nil
case search.TokOpNotRe: case filter.TokOpNotRe:
return !right.MatchString(leftVal), nil return !right.MatchString(leftVal), nil
} }
} }
right, ok := expr.Right.(search.StringLiteral) right, ok := expr.Right.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a string literal") return false, errors.New("right operand must be a string literal")
} }
@ -349,20 +378,20 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpEq: case filter.TokOpEq:
return leftVal == rightVal, nil return leftVal == rightVal, nil
case search.TokOpNotEq: case filter.TokOpNotEq:
return leftVal != rightVal, nil return leftVal != rightVal, nil
case search.TokOpGt: case filter.TokOpGt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal > rightVal, nil return leftVal > rightVal, nil
case search.TokOpLt: case filter.TokOpLt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal < rightVal, nil return leftVal < rightVal, nil
case search.TokOpGtEq: case filter.TokOpGtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal >= rightVal, nil return leftVal >= rightVal, nil
case search.TokOpLtEq: case filter.TokOpLtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal <= rightVal, nil return leftVal <= rightVal, nil
default: default:
@ -379,7 +408,18 @@ func getMappedStringLiteralFromRes(res *http.Response, s string) (string, error)
return s, nil return s, nil
} }
func matchResStringLiteral(res *http.Response, strLiteral search.StringLiteral) (bool, error) { func matchResStringLiteral(res *http.Response, strLiteral filter.StringLiteral) (bool, error) {
for key, values := range res.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range resFilterKeyFns { for _, fn := range resFilterKeyFns {
value, err := fn(res) value, err := fn(res)
if err != nil { if err != nil {

View File

@ -10,9 +10,9 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/log" "github.com/dstotijn/hetty/pkg/log"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/search"
) )
var ( var (
@ -56,16 +56,16 @@ type Service struct {
requestsEnabled bool requestsEnabled bool
responsesEnabled bool responsesEnabled bool
reqFilter search.Expression reqFilter filter.Expression
resFilter search.Expression resFilter filter.Expression
} }
type Config struct { type Config struct {
Logger log.Logger Logger log.Logger
RequestsEnabled bool RequestsEnabled bool
ResponsesEnabled bool ResponsesEnabled bool
RequestFilter search.Expression RequestFilter filter.Expression
ResponseFilter search.Expression ResponseFilter filter.Expression
} }
// RequestIDs implements sort.Interface. // RequestIDs implements sort.Interface.

View File

@ -1,10 +1,10 @@
package intercept package intercept
import "github.com/dstotijn/hetty/pkg/search" import "github.com/dstotijn/hetty/pkg/filter"
type Settings struct { type Settings struct {
RequestsEnabled bool RequestsEnabled bool
ResponsesEnabled bool ResponsesEnabled bool
RequestFilter search.Expression RequestFilter filter.Expression
ResponseFilter search.Expression ResponseFilter filter.Expression
} }

View File

@ -12,10 +12,10 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/log" "github.com/dstotijn/hetty/pkg/log"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
type contextKey int type contextKey int
@ -77,7 +77,7 @@ type service struct {
type FindRequestsFilter struct { type FindRequestsFilter struct {
ProjectID ulid.ULID ProjectID ulid.ULID
OnlyInScope bool OnlyInScope bool
SearchExpr search.Expression SearchExpr filter.Expression
} }
type Config struct { type Config struct {

View File

@ -8,8 +8,8 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{ var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
@ -36,22 +36,22 @@ var ResLogSearchKeyFns = map[string]func(rl ResponseLog) string{
// TODO: Request and response headers search key functions. // TODO: Request and response headers search key functions.
// Matches returns true if the supplied search expression evaluates to true. // Matches returns true if the supplied search expression evaluates to true.
func (reqLog RequestLog) Matches(expr search.Expression) (bool, error) { func (reqLog RequestLog) Matches(expr filter.Expression) (bool, error) {
switch e := expr.(type) { switch e := expr.(type) {
case search.PrefixExpression: case filter.PrefixExpression:
return reqLog.matchPrefixExpr(e) return reqLog.matchPrefixExpr(e)
case search.InfixExpression: case filter.InfixExpression:
return reqLog.matchInfixExpr(e) return reqLog.matchInfixExpr(e)
case search.StringLiteral: case filter.StringLiteral:
return reqLog.matchStringLiteral(e) return reqLog.matchStringLiteral(e)
default: default:
return false, fmt.Errorf("expression type (%T) not supported", expr) return false, fmt.Errorf("expression type (%T) not supported", expr)
} }
} }
func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, error) { func (reqLog RequestLog) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpNot: case filter.TokOpNot:
match, err := reqLog.Matches(expr.Right) match, err := reqLog.Matches(expr.Right)
if err != nil { if err != nil {
return false, err return false, err
@ -63,9 +63,9 @@ func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, er
} }
} }
func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, error) { func (reqLog RequestLog) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpAnd: case filter.TokOpAnd:
left, err := reqLog.Matches(expr.Left) left, err := reqLog.Matches(expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -77,7 +77,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
} }
return left && right, nil return left && right, nil
case search.TokOpOr: case filter.TokOpOr:
left, err := reqLog.Matches(expr.Left) left, err := reqLog.Matches(expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -91,28 +91,46 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
return left || right, nil return left || right, nil
} }
left, ok := expr.Left.(search.StringLiteral) left, ok := expr.Left.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("left operand must be a string literal") return false, errors.New("left operand must be a string literal")
} }
leftVal := reqLog.getMappedStringLiteral(left.Value) leftVal := reqLog.getMappedStringLiteral(left.Value)
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { if leftVal == "req.headers" {
right, ok := expr.Right.(search.RegexpLiteral) match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
return match, nil
}
if leftVal == "res.headers" && reqLog.Response != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Response.Header)
if err != nil {
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
}
return match, nil
}
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
right, ok := expr.Right.(filter.RegexpLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a regular expression") return false, errors.New("right operand must be a regular expression")
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpRe: case filter.TokOpRe:
return right.MatchString(leftVal), nil return right.MatchString(leftVal), nil
case search.TokOpNotRe: case filter.TokOpNotRe:
return !right.MatchString(leftVal), nil return !right.MatchString(leftVal), nil
} }
} }
right, ok := expr.Right.(search.StringLiteral) right, ok := expr.Right.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a string literal") return false, errors.New("right operand must be a string literal")
} }
@ -120,20 +138,20 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
rightVal := reqLog.getMappedStringLiteral(right.Value) rightVal := reqLog.getMappedStringLiteral(right.Value)
switch expr.Operator { switch expr.Operator {
case search.TokOpEq: case filter.TokOpEq:
return leftVal == rightVal, nil return leftVal == rightVal, nil
case search.TokOpNotEq: case filter.TokOpNotEq:
return leftVal != rightVal, nil return leftVal != rightVal, nil
case search.TokOpGt: case filter.TokOpGt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal > rightVal, nil return leftVal > rightVal, nil
case search.TokOpLt: case filter.TokOpLt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal < rightVal, nil return leftVal < rightVal, nil
case search.TokOpGtEq: case filter.TokOpGtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal >= rightVal, nil return leftVal >= rightVal, nil
case search.TokOpLtEq: case filter.TokOpLtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal <= rightVal, nil return leftVal <= rightVal, nil
default: default:
@ -162,7 +180,18 @@ func (reqLog RequestLog) getMappedStringLiteral(s string) string {
return s return s
} }
func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) { func (reqLog RequestLog) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for key, values := range reqLog.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range reqLogSearchKeyFns { for _, fn := range reqLogSearchKeyFns {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(reqLog)), strings.ToLower(fn(reqLog)),
@ -173,6 +202,17 @@ func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bo
} }
if reqLog.Response != nil { if reqLog.Response != nil {
for key, values := range reqLog.Response.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range ResLogSearchKeyFns { for _, fn := range ResLogSearchKeyFns {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(*reqLog.Response)), strings.ToLower(fn(*reqLog.Response)),

View File

@ -3,8 +3,8 @@ package reqlog_test
import ( import (
"testing" "testing"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/search"
) )
func TestRequestLogMatch(t *testing.T) { func TestRequestLogMatch(t *testing.T) {
@ -176,7 +176,7 @@ func TestRequestLogMatch(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
searchExpr, err := search.ParseQuery(tt.query) searchExpr, err := filter.ParseQuery(tt.query)
assertError(t, nil, err) assertError(t, nil, err)
got, err := tt.requestLog.Matches(searchExpr) got, err := tt.requestLog.Matches(searchExpr)

View File

@ -1,212 +0,0 @@
package search_test
import (
"regexp"
"testing"
"github.com/dstotijn/hetty/pkg/search"
)
func TestExpressionString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expression search.Expression
expected string
}{
{
name: "string literal expression",
expression: search.StringLiteral{Value: "foobar"},
expected: `"foobar"`,
},
{
name: "boolean expression with equal operator",
expression: search.InfixExpression{
Operator: search.TokOpEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" = "bar")`,
},
{
name: "boolean expression with not equal operator",
expression: search.InfixExpression{
Operator: search.TokOpNotEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" != "bar")`,
},
{
name: "boolean expression with greater than operator",
expression: search.InfixExpression{
Operator: search.TokOpGt,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" > "bar")`,
},
{
name: "boolean expression with less than operator",
expression: search.InfixExpression{
Operator: search.TokOpLt,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" < "bar")`,
},
{
name: "boolean expression with greater than or equal operator",
expression: search.InfixExpression{
Operator: search.TokOpGtEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" >= "bar")`,
},
{
name: "boolean expression with less than or equal operator",
expression: search.InfixExpression{
Operator: search.TokOpLtEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" <= "bar")`,
},
{
name: "boolean expression with regular expression operator",
expression: search.InfixExpression{
Operator: search.TokOpRe,
Left: search.StringLiteral{Value: "foo"},
Right: search.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" =~ "bar")`,
},
{
name: "boolean expression with not regular expression operator",
expression: search.InfixExpression{
Operator: search.TokOpNotRe,
Left: search.StringLiteral{Value: "foo"},
Right: search.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" !~ "bar")`,
},
{
name: "boolean expression with AND, OR and NOT operators",
expression: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.StringLiteral{Value: "foo"},
Right: search.InfixExpression{
Operator: search.TokOpOr,
Left: search.StringLiteral{Value: "bar"},
Right: search.PrefixExpression{
Operator: search.TokOpNot,
Right: search.StringLiteral{Value: "baz"},
},
},
},
expected: `("foo" AND ("bar" OR (NOT "baz")))`,
},
{
name: "boolean expression with nested group",
expression: search.InfixExpression{
Operator: search.TokOpOr,
Left: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
Right: search.PrefixExpression{
Operator: search.TokOpNot,
Right: search.StringLiteral{Value: "baz"},
},
},
expected: `(("foo" AND "bar") OR (NOT "baz"))`,
},
{
name: "implicit boolean expression with string literal operands",
expression: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
Right: search.StringLiteral{Value: "baz"},
},
expected: `(("foo" AND "bar") AND "baz")`,
},
{
name: "implicit boolean expression nested in group",
expression: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
expected: `("foo" AND "bar")`,
},
{
name: "implicit and explicit boolean expression with string literal operands",
expression: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.StringLiteral{Value: "foo"},
Right: search.InfixExpression{
Operator: search.TokOpOr,
Left: search.StringLiteral{Value: "bar"},
Right: search.StringLiteral{Value: "baz"},
},
},
Right: search.StringLiteral{Value: "yolo"},
},
expected: `(("foo" AND ("bar" OR "baz")) AND "yolo")`,
},
{
name: "implicit boolean expression with comparison operands",
expression: search.InfixExpression{
Operator: search.TokOpAnd,
Left: search.InfixExpression{
Operator: search.TokOpEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
Right: search.InfixExpression{
Operator: search.TokOpRe,
Left: search.StringLiteral{Value: "baz"},
Right: search.RegexpLiteral{regexp.MustCompile("yolo")},
},
},
expected: `(("foo" = "bar") AND ("baz" =~ "yolo"))`,
},
{
name: "eq operator takes precedence over boolean ops",
expression: search.InfixExpression{
Operator: search.TokOpOr,
Left: search.InfixExpression{
Operator: search.TokOpEq,
Left: search.StringLiteral{Value: "foo"},
Right: search.StringLiteral{Value: "bar"},
},
Right: search.InfixExpression{
Operator: search.TokOpEq,
Left: search.StringLiteral{Value: "baz"},
Right: search.StringLiteral{Value: "yolo"},
},
},
expected: `(("foo" = "bar") OR ("baz" = "yolo"))`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.expression.String()
if tt.expected != got {
t.Errorf("expected: %v, got: %v", tt.expected, got)
}
})
}
}

View File

@ -7,9 +7,9 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
var senderReqSearchKeyFns = map[string]func(req Request) string{ var senderReqSearchKeyFns = map[string]func(req Request) string{
@ -29,22 +29,22 @@ var senderReqSearchKeyFns = map[string]func(req Request) string{
// TODO: Request and response headers search key functions. // TODO: Request and response headers search key functions.
// Matches returns true if the supplied search expression evaluates to true. // Matches returns true if the supplied search expression evaluates to true.
func (req Request) Matches(expr search.Expression) (bool, error) { func (req Request) Matches(expr filter.Expression) (bool, error) {
switch e := expr.(type) { switch e := expr.(type) {
case search.PrefixExpression: case filter.PrefixExpression:
return req.matchPrefixExpr(e) return req.matchPrefixExpr(e)
case search.InfixExpression: case filter.InfixExpression:
return req.matchInfixExpr(e) return req.matchInfixExpr(e)
case search.StringLiteral: case filter.StringLiteral:
return req.matchStringLiteral(e) return req.matchStringLiteral(e)
default: default:
return false, fmt.Errorf("expression type (%T) not supported", expr) return false, fmt.Errorf("expression type (%T) not supported", expr)
} }
} }
func (req Request) matchPrefixExpr(expr search.PrefixExpression) (bool, error) { func (req Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpNot: case filter.TokOpNot:
match, err := req.Matches(expr.Right) match, err := req.Matches(expr.Right)
if err != nil { if err != nil {
return false, err return false, err
@ -56,9 +56,9 @@ func (req Request) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
} }
} }
func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) { func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
switch expr.Operator { switch expr.Operator {
case search.TokOpAnd: case filter.TokOpAnd:
left, err := req.Matches(expr.Left) left, err := req.Matches(expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -70,7 +70,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
} }
return left && right, nil return left && right, nil
case search.TokOpOr: case filter.TokOpOr:
left, err := req.Matches(expr.Left) left, err := req.Matches(expr.Left)
if err != nil { if err != nil {
return false, err return false, err
@ -84,28 +84,46 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
return left || right, nil return left || right, nil
} }
left, ok := expr.Left.(search.StringLiteral) left, ok := expr.Left.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("left operand must be a string literal") return false, errors.New("left operand must be a string literal")
} }
leftVal := req.getMappedStringLiteral(left.Value) leftVal := req.getMappedStringLiteral(left.Value)
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { if leftVal == "req.headers" {
right, ok := expr.Right.(search.RegexpLiteral) match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
return match, nil
}
if leftVal == "res.headers" && req.Response != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Response.Header)
if err != nil {
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
}
return match, nil
}
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
right, ok := expr.Right.(filter.RegexpLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a regular expression") return false, errors.New("right operand must be a regular expression")
} }
switch expr.Operator { switch expr.Operator {
case search.TokOpRe: case filter.TokOpRe:
return right.MatchString(leftVal), nil return right.MatchString(leftVal), nil
case search.TokOpNotRe: case filter.TokOpNotRe:
return !right.MatchString(leftVal), nil return !right.MatchString(leftVal), nil
} }
} }
right, ok := expr.Right.(search.StringLiteral) right, ok := expr.Right.(filter.StringLiteral)
if !ok { if !ok {
return false, errors.New("right operand must be a string literal") return false, errors.New("right operand must be a string literal")
} }
@ -113,20 +131,20 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
rightVal := req.getMappedStringLiteral(right.Value) rightVal := req.getMappedStringLiteral(right.Value)
switch expr.Operator { switch expr.Operator {
case search.TokOpEq: case filter.TokOpEq:
return leftVal == rightVal, nil return leftVal == rightVal, nil
case search.TokOpNotEq: case filter.TokOpNotEq:
return leftVal != rightVal, nil return leftVal != rightVal, nil
case search.TokOpGt: case filter.TokOpGt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal > rightVal, nil return leftVal > rightVal, nil
case search.TokOpLt: case filter.TokOpLt:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal < rightVal, nil return leftVal < rightVal, nil
case search.TokOpGtEq: case filter.TokOpGtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal >= rightVal, nil return leftVal >= rightVal, nil
case search.TokOpLtEq: case filter.TokOpLtEq:
// TODO(?) attempt to parse as int. // TODO(?) attempt to parse as int.
return leftVal <= rightVal, nil return leftVal <= rightVal, nil
default: default:
@ -155,7 +173,18 @@ func (req Request) getMappedStringLiteral(s string) string {
return s return s
} }
func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) { func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for key, values := range req.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range senderReqSearchKeyFns { for _, fn := range senderReqSearchKeyFns {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(req)), strings.ToLower(fn(req)),
@ -166,6 +195,17 @@ func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, er
} }
if req.Response != nil { if req.Response != nil {
for key, values := range req.Response.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range reqlog.ResLogSearchKeyFns { for _, fn := range reqlog.ResLogSearchKeyFns {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(*req.Response)), strings.ToLower(fn(*req.Response)),

View File

@ -3,8 +3,8 @@ package sender_test
import ( import (
"testing" "testing"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/search"
"github.com/dstotijn/hetty/pkg/sender" "github.com/dstotijn/hetty/pkg/sender"
) )
@ -177,7 +177,7 @@ func TestRequestLogMatch(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
searchExpr, err := search.ParseQuery(tt.query) searchExpr, err := filter.ParseQuery(tt.query)
assertError(t, nil, err) assertError(t, nil, err)
got, err := tt.senderReq.Matches(searchExpr) got, err := tt.senderReq.Matches(searchExpr)

View File

@ -12,9 +12,9 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
//nolint:gosec //nolint:gosec
@ -54,7 +54,7 @@ type service struct {
type FindRequestsFilter struct { type FindRequestsFilter struct {
ProjectID ulid.ULID ProjectID ulid.ULID
OnlyInScope bool OnlyInScope bool
SearchExpr search.Expression SearchExpr filter.Expression
} }
type Config struct { type Config struct {