From 32bd0e9a53e5a1c5df3028c34403d1c15ad63c6b Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sat, 14 Sep 2024 15:22:12 -0300 Subject: [PATCH 01/14] feat: add quantitative testing Signed-off-by: Felipe Zipitria --- .github/workflows/test.yml | 2 +- cmd/quantitative.go | 91 ++ cmd/quantitative_test.go | 61 ++ cmd/root.go | 1 + experimental/corpus/types.go | 76 ++ go.mod | 43 +- go.sum | 910 ++++++++++++++++++- internal/quantitative/leipzig/corpus.go | 223 +++++ internal/quantitative/leipzig/corpus_test.go | 52 ++ internal/quantitative/leipzig/iterator.go | 17 + internal/quantitative/local_engine.go | 197 ++++ internal/quantitative/local_engine_test.go | 62 ++ internal/quantitative/runner.go | 147 +++ internal/quantitative/stats.go | 53 ++ 14 files changed, 1921 insertions(+), 14 deletions(-) create mode 100644 cmd/quantitative.go create mode 100644 cmd/quantitative_test.go create mode 100644 experimental/corpus/types.go create mode 100644 internal/quantitative/leipzig/corpus.go create mode 100644 internal/quantitative/leipzig/corpus_test.go create mode 100644 internal/quantitative/leipzig/iterator.go create mode 100644 internal/quantitative/local_engine.go create mode 100644 internal/quantitative/local_engine_test.go create mode 100644 internal/quantitative/runner.go create mode 100644 internal/quantitative/stats.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6c4808..ed3116d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: ^1.19 + go-version: ^1.21 cache: true - name: Run Go Tests diff --git a/cmd/quantitative.go b/cmd/quantitative.go new file mode 100644 index 0000000..9092f02 --- /dev/null +++ b/cmd/quantitative.go @@ -0,0 +1,91 @@ +// Copyright 2023 OWASP ModSecurity Core Rule Set Project +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/coreruleset/go-ftw/internal/quantitative" + "github.com/coreruleset/go-ftw/output" + "github.com/spf13/cobra" + "os" +) + +// NewQuantitativeCmd +// Returns a new cobra command for running quantitative tests +func NewQuantitativeCmd() *cobra.Command { + runCmd := &cobra.Command{ + Use: "quantitative", + Short: "Run Quantitative Tests", + Long: `Run all quantitative tests`, + RunE: runQuantitativeE, + } + + runCmd.Flags().BoolP("markdown", "m", false, "Markdown table output mode") + runCmd.Flags().IntP("fast", "x", 0, "Process 1 in every X lines of input ('fast run' mode)") + runCmd.Flags().IntP("lines", "l", 0, "Number of lines of input to process before stopping") + runCmd.Flags().IntP("paranoia-level", "P", 1, "Paranoia level used to run the quantitative tests") + runCmd.Flags().IntP("number", "n", 0, "Number is the payload line from the corpus to exclusively send") + runCmd.Flags().StringP("payload", "p", "", "Payload is a string you want to test using quantitative tests. Will not use the corpus.") + runCmd.Flags().IntP("rule", "r", 0, "Rule ID of interest: only show false positives for specified rule ID") + runCmd.Flags().StringP("corpus", "c", "leipzig", "Corpus to use for the quantitative tests") + runCmd.Flags().StringP("corpus-lang", "L", "eng", "Corpus language to use for the quantitative tests.") + runCmd.Flags().StringP("corpus-size", "s", "100K", "Corpus size to use for the quantitative tests. Most corpus will have a size like \"100K\", \"1M\", etc.") + runCmd.Flags().StringP("corpus-year", "y", "2023", "Corpus year to use for the quantitative tests. Most corpus will have a year like \"2023\", \"2022\", etc.") + runCmd.Flags().StringP("corpus-source", "S", "news", "Corpus source to use for the quantitative tests. Most corpus will have a source like \"news\", \"web\", \"wikipedia\", etc.") + runCmd.Flags().StringP("directory", "d", ".", "Directory where the CRS rules are stored") + runCmd.Flags().StringP("file", "f", "", "output file path for quantitative tests. Prints to standard output by default.") + runCmd.Flags().StringP("output", "o", "normal", "output type for quantitative tests. \"normal\" is the default.") + + return runCmd +} + +func runQuantitativeE(cmd *cobra.Command, _ []string) error { + cmd.SilenceUsage = true + + corpus, _ := cmd.Flags().GetString("corpus") + corpusSize, _ := cmd.Flags().GetString("corpus-size") + corpusLang, _ := cmd.Flags().GetString("corpus-lang") + corpusYear, _ := cmd.Flags().GetString("corpus-year") + corpusSource, _ := cmd.Flags().GetString("corpus-source") + directory, _ := cmd.Flags().GetString("directory") + fast, _ := cmd.Flags().GetInt("fast") + lines, _ := cmd.Flags().GetInt("lines") + markdown, _ := cmd.Flags().GetBool("markdown") + outputFilename, _ := cmd.Flags().GetString("file") + paranoiaLevel, _ := cmd.Flags().GetInt("paranoia-level") + payload, _ := cmd.Flags().GetString("payload") + number, _ := cmd.Flags().GetInt("number") + rule, _ := cmd.Flags().GetInt("rule") + wantedOutput, _ := cmd.Flags().GetString("output") + + // use outputFile to write to file + var outputFile *os.File + var err error + if outputFilename == "" { + outputFile = os.Stdout + } else { + outputFile, err = os.Open(outputFilename) + if err != nil { + return err + } + } + out := output.NewOutput(wantedOutput, outputFile) + + params := quantitative.QuantitativeParams{ + Corpus: corpus, + CorpusSize: corpusSize, + CorpusYear: corpusYear, + CorpusLang: corpusLang, + CorpusSource: corpusSource, + Directory: directory, + Fast: fast, + Lines: lines, + Markdown: markdown, + ParanoiaLevel: paranoiaLevel, + Number: number, + Payload: payload, + Rule: rule, + } + + return quantitative.RunQuantitativeTests(params, out) +} diff --git a/cmd/quantitative_test.go b/cmd/quantitative_test.go new file mode 100644 index 0000000..9aecfb6 --- /dev/null +++ b/cmd/quantitative_test.go @@ -0,0 +1,61 @@ +// Copyright 2023 OWASP ModSecurity Core Rule Set Project +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "github.com/spf13/cobra" + "github.com/stretchr/testify/suite" + "io/fs" + "os" + "path" + "testing" +) + +var crsSetupFileContents = `# CRS Setup Configuration File` +var emptyRulesFile = `# Empty Rules File` + +type quantitativeCmdTestSuite struct { + suite.Suite + tempDir string + rootCmd *cobra.Command +} + +func TestQuantitativeTestSuite(t *testing.T) { + suite.Run(t, new(quantitativeCmdTestSuite)) +} + +func (s *quantitativeCmdTestSuite) SetupTest() { + s.rootCmd = NewRootCommand() + s.tempDir = s.T().TempDir() + + err := os.MkdirAll(path.Join(s.tempDir, "rules"), fs.ModePerm) + s.Require().NoError(err) + fakeCRSSetupConf, err := os.Create(path.Join(s.tempDir, "crs-setup.conf.example")) + s.Require().NoError(err) + n, err := fakeCRSSetupConf.WriteString(crsSetupFileContents) + s.Require().NoError(err) + s.Equal(len(crsSetupFileContents), n) + err = fakeCRSSetupConf.Close() + s.Require().NoError(err) + fakeRulesFile, err := os.Create(path.Join(s.tempDir, "rules", "Rules1.conf")) + s.Require().NoError(err) + n, err = fakeRulesFile.WriteString(emptyRulesFile) + s.Require().NoError(err) + s.Equal(len(emptyRulesFile), n) + s.rootCmd.AddCommand(NewQuantitativeCmd()) +} + +func (s *quantitativeCmdTestSuite) TearDownTest() { + err := os.RemoveAll(s.tempDir) + s.Require().NoError(err) +} + +func (s *quantitativeCmdTestSuite) TestQuantitativeCommand() { + s.rootCmd.SetArgs([]string{"quantitative", "-d", s.tempDir}) + cmd, err := s.rootCmd.ExecuteContextC(context.Background()) + s.Require().NoError(err, "quantitative command should not return an error") + s.Equal("quantitative", cmd.Name(), "quantitative command should have the name 'quantitative'") + s.Require().NoError(err) +} diff --git a/cmd/root.go b/cmd/root.go index 6836be1..399e1cc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,7 @@ func Execute(version string) error { rootCmd := NewRootCommand() rootCmd.AddCommand(NewCheckCommand()) rootCmd.AddCommand(NewRunCommand()) + rootCmd.AddCommand(NewQuantitativeCmd()) rootCmd.Version = version return rootCmd.ExecuteContext(context.Background()) diff --git a/experimental/corpus/types.go b/experimental/corpus/types.go new file mode 100644 index 0000000..df38b3d --- /dev/null +++ b/experimental/corpus/types.go @@ -0,0 +1,76 @@ +// Package corpus provides functionality for creating and managing corpora. +// +// A corpus is a collection of text documents that are used for training and testing machine learning models. +// The documents in a corpus are typically sentences or paragraphs of text. +// +// The corpus package provides an interface for working with corpora, as well as a set of built-in corpora +// that can be used for detecting which text will generate false positives in WAF rules. +// +// This interface includes methods for getting the URL of the corpus, getting the file from the remote URL, +// getting an iterator for the corpus, getting the payload given a line from the corpus iterator. Each corpus +// will have a size, year, source, and language. +// The iterator interface includes methods for getting the next sentence from the corpus and checking if there +// is another sentence in the corpus. +// Each corpus will need its own implementation of the Corpus interface. As this is an experimental package, this +// interface is subject to change. +package corpus + +// CorpusFile contains the cache directory and file name +type CorpusFile struct { + // CacheDir is the directory where files are cached + CacheDir string + // FilePath is the path to the cached file + FilePath string +} + +// Corpus is the interface that needs to be implemented for getting the payload from a corpus +type Corpus interface { + // URL returns the URL of the corpus + URL() string + + // WithURL sets the URL of the corpus + WithURL(url string) Corpus + + // GetCorpusFile gets the file from the remote url. + // It returns the local file path were the corpus is stored. + GetCorpusFile() CorpusFile + + // GetIterator returns an iterator for the corpus + GetIterator(c CorpusFile) Iterator + + // GetPayload returns the payload given a line from the Corpus Iterator + GetPayload(line string) string + + // Size returns the size of the corpus + Size() string + // WithSize sets the size of the corpus + // Most corpus will have a size like "100K", "1M", etc., related to the amount of sentences in the corpus + WithSize(size string) Corpus + + // Year returns the year of the corpus + Year() string + // WithYear sets the year of the corpus + // Most corpus will have a year like "2023", "2022", etc. + WithYear(year string) Corpus + + // Source returns the source of the corpus + Source() string + // WithSource sets the source of the corpus + // Most corpus will have a source like "news", "web", "wikipedia", etc. + WithSource(source string) Corpus + + // Lang returns the language of the corpus + Lang() string + // WithLanguage sets the language of the corpus + // Most corpus will have a language like "eng", "de", etc. + WithLanguage(lang string) Corpus +} + +// Iterator is an interface for iterating over a corpus +type Iterator interface { + // Next returns the next sentence from the corpus + Next() string + // HasNext returns true if there is another sentence in the corpus + // false otherwise + HasNext() bool +} diff --git a/go.mod b/go.mod index cf4987b..25ab54a 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.21 require ( github.com/Masterminds/sprig v2.22.0+incompatible + github.com/corazawaf/coraza/v3 v3.2.1 github.com/coreruleset/ftw-tests-schema/v2 v2.1.0 github.com/go-logr/zerologr v1.2.3 github.com/goccy/go-yaml v1.9.2 github.com/google/uuid v1.6.0 + github.com/hashicorp/go-getter v1.7.6 github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 @@ -26,33 +28,70 @@ require ( ) require ( + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + cloud.google.com/go/storage v1.38.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/antchfx/htmlquery v1.3.2 // indirect github.com/antchfx/xpath v1.3.1 // indirect + github.com/aws/aws-sdk-go v1.44.122 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/corazawaf/libinjection-go v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.2 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.27.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/api v0.169.0 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/binaryregexp v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 209f359..d4691df 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,227 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc= -github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antchfx/htmlquery v1.3.2 h1:85YdttVkR1rAY+Oiv/nKI4FCimID+NXhDn82kz3mEvs= github.com/antchfx/htmlquery v1.3.2/go.mod h1:1mbkcEgEarAokJiWhTfr4hR06w/q2ZZjnYLrDt6CTUk= -github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc= -github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.1 h1:PNbFuUqHwWl0xRjvUPjJ95Agbmdj2uzzIwmQKgu4oCk= github.com/antchfx/xpath v1.3.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/corazawaf/coraza/v3 v3.2.1 h1:zBIji4ut9FtFe8lXdqFwXMAkUoDJZ7HsOlEUYWERLI8= +github.com/corazawaf/coraza/v3 v3.2.1/go.mod h1:fVndCGdUHJWl9c26VZPcORQRzUYwMPnRkC6TyTkhbUg= +github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y= +github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreruleset/ftw-tests-schema/v2 v2.1.0 h1:2ilKzKRG5UzzxBcrJLXFtPalStdQ9jzzaYFuFk0OEk0= github.com/coreruleset/ftw-tests-schema/v2 v2.1.0/go.mod h1:ZHVFX5ses4+5IxUP0ufCNg/VqRWxziH6ZuUca092Hxo= @@ -19,13 +229,35 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -40,12 +272,120 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w= github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.6 h1:5jHuM+aH373XNtXl9TNTUH5Qd69Trve11tHIrB+6yj4= +github.com/hashicorp/go-getter v1.7.6/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5 h1:FcxwOojw6pUiPpsf7Q6Fw/pI+7cR6FlapLBEGV/902A= github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5/go.mod h1:GYeBD1CF7AqnKZK+UCytLcY3G+UKo0ByXX/3xfdNyqQ= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= @@ -54,6 +394,16 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -66,78 +416,314 @@ github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tonglil/buflogr v1.1.1 h1:CKAjOHBSMmgbRFxpn/RhQHPj5oANc7ekhlsoUDvcZIg= github.com/tonglil/buflogr v1.1.1/go.mod h1:WLLtPRLqcFYWQLbA+ytXy5WrFTYnfA+beg1MpvJCxm4= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -145,30 +731,332 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -wait4x.dev/v2 v2.14.1 h1:GbFJcholvjtmeStrswPnY3N6Gk60L6m/kr2FCQ1Mnkg= -wait4x.dev/v2 v2.14.1/go.mod h1:MLpjcyq8dgiZErpTn4w+EM9glgym14d5iq84uS9B5WQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= wait4x.dev/v2 v2.14.2 h1:FtH2y4rSuL6RcACDl43XSABy7YKRyYVc6Nfexu+D5P8= wait4x.dev/v2 v2.14.2/go.mod h1:OpyyokMcvJM4En4r/V+SWTNdcpo87Zj1e5vjF9cU7m0= diff --git a/internal/quantitative/leipzig/corpus.go b/internal/quantitative/leipzig/corpus.go new file mode 100644 index 0000000..11aa490 --- /dev/null +++ b/internal/quantitative/leipzig/corpus.go @@ -0,0 +1,223 @@ +package leipzig + +import ( + "bufio" + "fmt" + "github.com/coreruleset/go-ftw/experimental/corpus" + "github.com/hashicorp/go-getter" + "github.com/rs/zerolog/log" + "os" + "path" + "path/filepath" + "strings" +) + +// LeipzigCorpus represents a corpus of text data +// Original files are available at https://wortschatz.uni-leipzig.de/en/download +const ( + defaultCorpusSite = "https://downloads.wortschatz-leipzig.de/corpora" + defaultCorpusLanguage = "eng" + defaultCorpusSize = "100K" + defaultCorpusYear = "2023" + defaultCorpusSource = "news" + defaultCorpusExt = "tar.gz" + defaultCorpusType = "sentences.txt" +) + +// LeipzigCorpus is a corpus of text data +type LeipzigCorpus struct { + // url_ is the URL of the corpus + url_ string + // lang is the language of the corpus + lang string + // corpusFile is the original file name that contains the corpus file + corpusFile string + // File is the file name of the corpus + File string + // size is the size of the corpus + size string + // source is the source of the corpus + source string + // year is the year of the corpus + year string +} + +func (c *LeipzigCorpus) regenerateFileNames() { + c.corpusFile = fmt.Sprintf("%s_%s_%s_%s.%s", + c.lang, c.source, c.year, c.size, + defaultCorpusExt) + c.File = fmt.Sprintf("%s_%s_%s_%s-%s", + c.lang, c.source, c.year, c.size, + defaultCorpusType) +} + +// NewLeipzigCorpus returns a new Leipzig corpus +func NewLeipzigCorpus() corpus.Corpus { + leipzig := &LeipzigCorpus{ + url_: defaultCorpusSite, + corpusFile: "", + File: "", + lang: defaultCorpusLanguage, + source: defaultCorpusSource, + year: defaultCorpusYear, + size: defaultCorpusSize, + } + + leipzig.regenerateFileNames() + + return leipzig +} + +// size returns the size of the corpus +func (c *LeipzigCorpus) Size() string { + return c.size +} + +func (c *LeipzigCorpus) WithSize(size string) corpus.Corpus { + c.size = size + c.regenerateFileNames() + return c +} + +// year returns the year of the corpus +func (c *LeipzigCorpus) Year() string { + return c.year +} + +func (c *LeipzigCorpus) WithYear(year string) corpus.Corpus { + c.year = year + c.regenerateFileNames() + return c +} + +// URL returns the URL of the corpus +func (c *LeipzigCorpus) URL() string { + return c.url_ +} + +// WithURL sets the URL of the corpus +// The URL corresponds to the base URI where the corpus is stored. Then the corpusFile will be added. +func (c *LeipzigCorpus) WithURL(url string) corpus.Corpus { + c.url_ = url + return c +} + +// Source returns the source of the corpus +func (c *LeipzigCorpus) Source() string { + return c.source +} + +func (c *LeipzigCorpus) WithSource(source string) corpus.Corpus { + c.source = source + c.regenerateFileNames() + return c +} + +// Lang returns the language of the corpus +func (c *LeipzigCorpus) Lang() string { + return c.lang +} + +func (c *LeipzigCorpus) WithLanguage(lang string) corpus.Corpus { + c.lang = lang + c.regenerateFileNames() + return c +} + +// GetIterator returns an iterator for the corpus +func (c *LeipzigCorpus) GetIterator(cache corpus.CorpusFile) corpus.Iterator { + // open cache file + if cache.FilePath == "" { + log.Fatal().Msg("Cache file path is empty") + } + file, err := os.Open(cache.FilePath) + if err != nil { + log.Fatal().Err(err).Msgf("Could not open the file %s", cache.FilePath) + } + scanner := bufio.NewScanner(file) + it := &LeipzigIterator{ + scanner: scanner, + } + return it +} + +// GetPayload returns the payload from the line +// We assume that the first word is the line number, +// and we want the rest +func (c *LeipzigCorpus) GetPayload(line string) string { + return strings.Join(strings.Split(line, "\t")[1:], " ") +} + +// GetCorpusFile gets the file from the remote url. +// We assume that the file is compressed somehow, and we want to get a file inside it. +func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { + home, err := os.UserHomeDir() + if err != nil { + log.Fatal().Err(err).Msg("Could not get home directory") + } + + url := fmt.Sprintf("%s/%s", c.url_, c.corpusFile) + + cacheDir := path.Join(home, ".ftw") + + log.Debug().Msgf("Downloading corpus file from %s", url) + dest := path.Join(cacheDir, "extracted") + if err := os.MkdirAll(dest, os.ModePerm); err != nil { + log.Fatal().Err(err).Msg("Could not create destination directory") + } + + cache := corpus.CorpusFile{ + CacheDir: cacheDir, + FilePath: "", + } + + if info, err := os.Stat(path.Join(home, ".ftw", c.File)); err == nil { + log.Debug().Msgf("File %s already exists", info.Name()) + cache.FilePath = path.Join(home, ".ftw", c.File) + return cache + } + + client := &getter.Client{ + Mode: getter.ClientModeAny, + Src: url, + Dst: dest, + } + + log.Info().Msgf("Downloading corpus file from %s", url) + err = client.Get() + if err != nil { + log.Fatal().Msgf("download failed: %v", err) + } + + err = filepath.Walk(cacheDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println("Error walking:", err) + return err + } + + if info.IsDir() { + return nil // Skip directories + } + + log.Trace().Msgf("Checking file %s", info.Name()) + + if info.Name() == c.File { + newPath := filepath.Join(cacheDir, info.Name()) + err = os.Rename(path, newPath) + if err != nil { + fmt.Println("Error renaming:", err) + return err + } + fmt.Println("Moved", path, "to", newPath) + cache.FilePath = newPath + } + + return nil + }) + + if err != nil { + log.Fatal().Err(err).Msg("Could not walk the path") + } + + return cache +} diff --git a/internal/quantitative/leipzig/corpus_test.go b/internal/quantitative/leipzig/corpus_test.go new file mode 100644 index 0000000..cd55f98 --- /dev/null +++ b/internal/quantitative/leipzig/corpus_test.go @@ -0,0 +1,52 @@ +package leipzig + +import ( + "github.com/coreruleset/go-ftw/experimental/corpus" + "github.com/stretchr/testify/suite" + "testing" +) + +type leipzigCorpusTestSuite struct { + suite.Suite + corpus corpus.Corpus + cache corpus.CorpusFile + iter corpus.Iterator +} + +func TestLeipzigCorpusTestSuite(t *testing.T) { + suite.Run(t, new(leipzigCorpusTestSuite)) +} + +func (s *leipzigCorpusTestSuite) SetupTest() { + s.corpus = NewLeipzigCorpus() + s.Require().Equal("https://downloads.wortschatz-leipzig.de/corpora", s.corpus.URL()) + s.Require().Equal("eng", s.corpus.Lang()) + s.Require().Equal("100K", s.corpus.Size()) + s.Require().Equal("news", s.corpus.Source()) + s.Require().Equal("2023", s.corpus.Year()) +} + +func (s *leipzigCorpusTestSuite) TestWithSize() { + s.corpus.WithSize("300K") + s.Require().Equal("300K", s.corpus.Size()) +} + +func (s *leipzigCorpusTestSuite) TestGetIterator() { + s.corpus.WithSize("10K") + s.cache = s.corpus.GetCorpusFile() + s.iter = s.corpus.GetIterator(s.cache) +} + +func (s *leipzigCorpusTestSuite) TestNextSentenceFromCorpus() { + s.cache = s.corpus.GetCorpusFile() + s.iter = s.corpus.GetIterator(s.cache) + s.Require().True(s.iter.HasNext()) + s.Require().Equal("1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", s.iter.Next()) +} + +func (s *leipzigCorpusTestSuite) TestGetPayloadFromString() { + s.cache = s.corpus.GetCorpusFile() + s.iter = s.corpus.GetIterator(s.cache) + s.Require().True(s.iter.HasNext()) + s.Require().Equal("1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", s.iter.Next()) +} diff --git a/internal/quantitative/leipzig/iterator.go b/internal/quantitative/leipzig/iterator.go new file mode 100644 index 0000000..4af1f44 --- /dev/null +++ b/internal/quantitative/leipzig/iterator.go @@ -0,0 +1,17 @@ +package leipzig + +import "bufio" + +type LeipzigIterator struct { + scanner *bufio.Scanner +} + +// HasNext returns true if there is another sentence in the corpus +func (c *LeipzigIterator) HasNext() bool { + return c.scanner.Scan() +} + +// Next returns the next sentence from the corpus +func (c *LeipzigIterator) Next() string { + return c.scanner.Text() +} diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go new file mode 100644 index 0000000..b4a7bf7 --- /dev/null +++ b/internal/quantitative/local_engine.go @@ -0,0 +1,197 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package quantitative + +import ( + "bytes" + "fmt" + "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/types" + "github.com/rs/zerolog/log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "text/template" +) + +const ( + defaultPrefix = "." + testingConfigTmpl = `SecRuleEngine On +SecRequestBodyAccess On +SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" +SecRule REQUEST_HEADERS:Content-Type "^application/json" \ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" +SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \ + "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" +SecRequestBodyLimit 13107200 +SecRequestBodyInMemoryLimit 131072 +SecRequestBodyLimitAction Reject +SecRule REQBODY_ERROR "!@eq 0" \ + "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" +SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ + "id:'200003',phase:2,t:none,log,deny,status:400, \ + msg:'Multipart request body failed strict validation." +SecResponseBodyAccess On +SecResponseBodyMimeType text/plain text/html text/xml +SecResponseBodyLimit 524288 +SecResponseBodyLimitAction ProcessPartial +SecDataDir /tmp/ +SecAuditEngine RelevantOnly +SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$" +SecAuditLogParts ABIJDEFHZ +SecAuditLogType Serial +SecAction \ + "id:900000,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + tag:'OWASP_CRS',\ + ver:'OWASP_CRS/4.7.0-dev',\ + setvar:tx.blocking_paranoia_level={{ .ParanoiaLevel }}" +` +) + +type LocalEngine struct { + waf coraza.WAF +} + +// NewEngine creates a new engine to test payloads +func NewEngine(prefix string, paranoia int) *LocalEngine { + eng := &LocalEngine{ + waf: crsWAF(prefix, paranoia), + } + return eng +} + +// CRSCall benchmarks the CRS WAF with a GET request +// payload: the string to be passed in the request body +// returns the status of the transaction and a map of the matched rules with their IDs and the data that matched. +func (e *LocalEngine) CRSCall(payload string) (int, map[int]string) { + var status = http.StatusOK + var matchedRules = make(map[int]string) + + tx := e.waf.NewTransaction() + tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080) + tx.ProcessURI("/post", "POST", "HTTP/1.1") + tx.AddRequestHeader("Host", "localhost") + tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75. 0.3770.100 Safari/537.36") + tx.AddRequestHeader("Accept", "application/json") + tx.AddRequestHeader("Content-Type", "application/x-www-form-urlencoded") + body := []byte("payload=" + url.QueryEscape(payload)) + + tx.AddRequestHeader("Content-Length", strconv.Itoa(len(body))) + tx.ProcessRequestHeaders() + if _, _, err := tx.WriteRequestBody(body); err != nil { + log.Error().Err(err).Msg("failed to write request body") + } + it, err := tx.ProcessRequestBody() + if err != nil { + log.Error().Err(err).Msg("failed to process request body") + } + if it != nil { // execution was interrupted + status = obtainStatusCodeFromInterruptionOrDefault(it, http.StatusOK) + matchedRules = getMatchedRules(tx) + } + // We don't care about the response body for now, nor logging. + if err := tx.Close(); err != nil { + log.Error().Err(err).Msg("failed to close transaction") + } + + return status, matchedRules +} + +// crsWAF creates a WAF with the CRS rules +// prefix: the path to the CRS rules +// paranoiaLevel: 1 - 4 should be added as a template to the crs-setup.conf file +// If you want to run your own waf rules instead of crs, create a similar function to crsWAF +func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { + if prefix == "" { + prefix = defaultPrefix + } + // test if the prefix is a valid path + if _, err := os.Stat(fmt.Sprintf("%s/crs-setup.conf.example", prefix)); err != nil { + if _, err = os.Stat(fmt.Sprintf("%s/rules", prefix)); err != nil { + log.Fatal().Err(err).Msg("failed to find the CRS rules") + } + } + // inject variables into config template + vars := map[string]interface{}{ + "ParanoiaLevel": paranoiaLevel, + } + log.Debug().Msgf("Using paranoia level: %d\n", paranoiaLevel) + // set up configuration from template + configTmpl, err := template.New("crs-config").Parse(testingConfigTmpl) + if err != nil { + log.Fatal().Err(err).Msg("failed to parse config template") + } + crsConfig := &bytes.Buffer{} + err = configTmpl.Execute(crsConfig, vars) + if err != nil { + log.Fatal().Err(err).Msg("failed to inject variables into config template") + } + + conf := coraza.NewWAFConfig(). + WithDirectives(crsConfig.String()). + WithDirectives(fmt.Sprintf("Include %s/crs-setup.conf.example", prefix)). + WithDirectives(fmt.Sprintf("Include %s/rules/R*.conf", prefix)) + + waf, err := coraza.NewWAF(conf) + if err != nil { + log.Fatal().Err(err).Msg("failed to create WAF") + } + return waf +} + +func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int { + if it.Action == "deny" { + statusCode := it.Status + if statusCode == 0 { + statusCode = 403 + } + + return statusCode + } + + return defaultStatusCode +} + +// getMatchedRules returns the IDs of the rules that matched +func getMatchedRules(tx types.Transaction) map[int]string { + var matchedRules = make(map[int]string) + + for _, rule := range tx.MatchedRules() { + id := rule.Rule().ID() + if needToDiscardAdminRule(id) { + continue + } + matchedRules[id] = rule.Data() + } + + return matchedRules +} + +// needToDiscardAdminRule checks if the rule is an admin rule +// Administrative rules are used to separate logically between +// different paranoia levels, for example. +func needToDiscardAdminRule(id int) bool { + strId := strconv.Itoa(id) + if id < 902000 || /* configuration rules */ + id > 949000 || /* reporting ruls */ + id == 941010 || /* special rule to remove REQUEST_FILENAME from the target list of all the 941xxx rules */ + strings.HasSuffix(strId, "11") || /* detection paranoia level < 1, phase:1 rule */ + strings.HasSuffix(strId, "12") || /* detection paranoia level < 1, phase:2 rule */ + strings.HasSuffix(strId, "13") || /* detection paranoia level < 2, phase:1 rule */ + strings.HasSuffix(strId, "14") || /* detection paranoia level < 2, phase:2 rule */ + strings.HasSuffix(strId, "15") || /* detection paranoia level < 3, phase:1 rule */ + strings.HasSuffix(strId, "16") || /* detection paranoia level < 3, phase:2 rule */ + strings.HasSuffix(strId, "17") || /* detection paranoia level < 4, phase:1 rule */ + strings.HasSuffix(strId, "18") { /* detection paranoia level < 4, phase:2 rule */ + return true + } + return false +} diff --git a/internal/quantitative/local_engine_test.go b/internal/quantitative/local_engine_test.go new file mode 100644 index 0000000..4894d69 --- /dev/null +++ b/internal/quantitative/local_engine_test.go @@ -0,0 +1,62 @@ +package quantitative + +import ( + "github.com/hashicorp/go-getter" + "github.com/stretchr/testify/suite" + "net/http" + "os" + "path" + "testing" +) + +const crsURL = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" + +type localEngineTestSuite struct { + suite.Suite + dir string + engine *LocalEngine +} + +func TestLocalEngineTestSuite(t *testing.T) { + suite.Run(t, new(localEngineTestSuite)) +} + +func (s *localEngineTestSuite) SetupTest() { + s.dir = path.Join(os.TempDir()) + s.Require().NoError(os.MkdirAll(s.dir, 0755)) + client := &getter.Client{ + Mode: getter.ClientModeAny, + Src: crsURL, + Dst: s.dir, + } + + err := client.Get() + s.Require().NoError(err) + s.engine = NewEngine(path.Join(s.dir, "coreruleset-4.6.0"), 1) + s.Require().NotNil(s.engine) +} + +func (s *localEngineTestSuite) TeardownTest() { + err := os.RemoveAll(s.dir) + s.Require().NoError(err) +} + +// TestCRSCall For this test you will need to have the Core Rule Set repository cloned in the parent directory as the project. +func (s *localEngineTestSuite) TestCRSCall() { + // simple payload, no matches + status, matchedRules := s.engine.CRSCall("this is a test") + s.Require().Equal(http.StatusOK, status) + s.Require().Empty(matchedRules) + + // this payload will match a few rules + status, matchedRules = s.engine.CRSCall("' OR 1 = 1") + s.Require().Equal(http.StatusForbidden, status) + s.Require().NotEmpty(matchedRules) + + expected := []int{942100 /* libinjection match */} + var keys []int + for k := range matchedRules { + keys = append(keys, k) + } + s.Require().Equal(expected, keys) +} diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go new file mode 100644 index 0000000..0b31229 --- /dev/null +++ b/internal/quantitative/runner.go @@ -0,0 +1,147 @@ +package quantitative + +import ( + "github.com/coreruleset/go-ftw/experimental/corpus" + "github.com/coreruleset/go-ftw/internal/quantitative/leipzig" + "github.com/coreruleset/go-ftw/output" + "github.com/rs/zerolog/log" + "net/http" + "time" +) + +// QuantitativeParams is the parameters for the quantitative tests +type QuantitativeParams struct { + // Lines is the number of lines of input to process before stopping + Lines int + // Fast is the process 1 in every X lines of input ('fast run' mode) + Fast int + // Rule is the rule ID of interest: only show false positives for specified rule ID + Rule int + // Payload is just a string to use instead of reading from the corpus + Payload string + // Number is the payload number (the line in the corpus) to exclusively send + Number int + // Directory is the directory where the CRS rules are stored + Directory string + // Markdown is the Markdown table output mode + Markdown bool + // ParanoiaLevel is the paranoia level in where to run the quantitative tests + ParanoiaLevel int + // CorpusSize is the corpus size to use for the quantitative tests + CorpusSize string + // Corpus is the corpus to use for the quantitative tests + Corpus string + // CorpusLang is the language of the corpus + CorpusLang string + // CorpusYear is the year of the corpus + CorpusYear string + // CorpusSource is the source of the corpus: e.g. most corpus will have a source like "news", "web", "wikipedia", etc. + CorpusSource string +} + +// NewCorpus creates a new corpus +func NewCorpus(name string) corpus.Corpus { + switch name { + case "leipzig": + return leipzig.NewLeipzigCorpus() + default: + log.Fatal().Msgf("Unknown corpus %s", name) + return nil + } +} + +// RunQuantitativeTests runs all quantitative tests +func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { + log.Info().Msg("Running quantitative tests") + + log.Trace().Msgf("Lines: %d", params.Lines) + log.Trace().Msgf("Fast: %d", params.Fast) + log.Trace().Msgf("Rule: %d", params.Rule) + log.Trace().Msgf("Payload: %s", params.Payload) + log.Trace().Msgf("Read Corpus Line: %d", params.Number) + log.Trace().Msgf("Directory: %s", params.Directory) + log.Trace().Msgf("Markdown: %t", params.Markdown) + log.Trace().Msgf("Paranoia level: %d", params.ParanoiaLevel) + log.Trace().Msgf("Corpus size: %s", params.CorpusSize) + log.Trace().Msgf("Corpus lang: %s", params.CorpusLang) + log.Trace().Msgf("Corpus: %s", params.Corpus) + + startTime := time.Now() + // create a new corpusRunner + corpusRunner := NewCorpus(params.Corpus). + WithSize(params.CorpusSize). + WithYear(params.CorpusYear). + WithSource(params.CorpusSource). + WithLanguage(params.CorpusLang) + + // download the corpusRunner file + lc := corpusRunner.GetCorpusFile() + // create the results + stats := NewQuantitativeStats() + + runner := NewEngine(params.Directory, params.ParanoiaLevel) + + // Are we using the corpus at all? + if params.Payload != "" { + // CRSCall with payload + doEngineCall(runner, params.Payload, params.Rule, stats) + } else { // iterate over the corpus + for iter := corpusRunner.GetIterator(lc); iter.HasNext(); { + line := iter.Next() + stats.Run++ + log.Trace().Msgf("Line: %s", line) + // check if we look for a specific payload line # + if needSpecificPayload(params.Number, stats.Run) { + continue + } + // ask the corpus to get the payload + payload := corpusRunner.GetPayload(line) + + log.Trace().Msgf("Payload: %s", payload) + + // check if we only want to process a specific number of lines + if params.Lines > 0 && stats.Run >= params.Lines { + break + } + } + } + + stats.TotalTime = time.Since(startTime) + stats.printSummary(out) + return nil +} + +// needSpecificPayload returns true when the line we have is the one we want +func needSpecificPayload(want int, have int) bool { + return want == have +} + +// wantSpecificRuleResults returns true +func wantSpecificRuleResults(specific int, rule int) bool { + skip := false + if specific > 0 && specific != rule { + skip = true + } + return skip +} + +// doEngineCall +func doEngineCall(engine *LocalEngine, payload string, specificRule int, stats *QuantitativeRunStats) { + status, matchedRules := engine.CRSCall(payload) + log.Trace().Msgf("Status: %d", status) + log.Trace().Msgf("Rules: %v", matchedRules) + if status == http.StatusForbidden { + // append the line to the false positives + log.Debug().Msgf("False positive with string: %s", payload) + log.Trace().Msgf("=> rules matched: %+v", matchedRules) + for rule, data := range matchedRules { + // check if we only want to show false positives for a specific rule + if wantSpecificRuleResults(rule, specificRule) { + log.Debug().Msgf("rule %d does not match the specific rule we wanted %d", rule, specificRule) + continue + } + stats.addFalsePositive(rule) + log.Debug().Msgf("==> rule %d matched with data: %s", rule, data) + } + } +} diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go new file mode 100644 index 0000000..8a2e6d4 --- /dev/null +++ b/internal/quantitative/stats.go @@ -0,0 +1,53 @@ +package quantitative + +import ( + "encoding/json" + "github.com/coreruleset/go-ftw/output" + "github.com/rs/zerolog/log" + "time" +) + +// RunStats accumulates test statistics. +type QuantitativeRunStats struct { + // Run is the amount of tests executed in this run. + Run int `json:"run"` + // TotalTime is the duration over all runs, the sum of all individual run times. + TotalTime time.Duration + // FalsePositives is the total false positives detected + FalsePositives int `json:"falsePositives"` + // FalsePositivesPerRule is the aggregated false positives per rule + FalsePositivesPerRule map[int]int `json:"falsePositivesPerRule"` +} + +// NewQuantitativeStats returns a new empty stats +func NewQuantitativeStats() *QuantitativeRunStats { + return &QuantitativeRunStats{ + Run: 0, + FalsePositives: 0, + FalsePositivesPerRule: make(map[int]int), + TotalTime: 0, + } +} + +// print final statistics +func (s *QuantitativeRunStats) printSummary(out *output.Output) { + log.Debug().Msg("Printing Stats summary") + if s.FalsePositives > 0 { + if out.IsJson() { + b, _ := json.Marshal(s) + out.RawPrint(string(b)) + } else { + ratio := float64(s.FalsePositives) / float64(s.Run) + out.Println("Run %d payloads in %s", s.Run, s.TotalTime) + out.Println("Total False positive ratio: %d/%d = %.4f", s.FalsePositives, s.Run, ratio) + out.Println("False positives per rule: %+v", s.FalsePositivesPerRule) + // echo "| Freq. | ID # | Paranoia Level |" + // echo "| ------ | ------ | -------------- |" + } + } +} + +func (s *QuantitativeRunStats) addFalsePositive(rule int) { + s.FalsePositives++ + s.FalsePositivesPerRule[rule]++ +} From d2fba4c5a55c58a6afc76e0e0ae328122dc1da44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Zipitr=C3=ADa?= <3012076+fzipi@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:31:34 -0300 Subject: [PATCH 02/14] Apply suggestions from code review Co-authored-by: Max Leske <250711+theseion@users.noreply.github.com> --- cmd/quantitative.go | 14 ++++----- cmd/quantitative_test.go | 2 +- experimental/corpus/types.go | 33 ++++++++++++---------- internal/quantitative/leipzig/corpus.go | 11 ++++---- internal/quantitative/leipzig/iterator.go | 2 +- internal/quantitative/local_engine.go | 8 +++--- internal/quantitative/local_engine_test.go | 4 +-- internal/quantitative/runner.go | 6 ++-- 8 files changed, 42 insertions(+), 38 deletions(-) diff --git a/cmd/quantitative.go b/cmd/quantitative.go index 9092f02..65980fb 100644 --- a/cmd/quantitative.go +++ b/cmd/quantitative.go @@ -15,26 +15,26 @@ import ( func NewQuantitativeCmd() *cobra.Command { runCmd := &cobra.Command{ Use: "quantitative", - Short: "Run Quantitative Tests", + Short: "Run quantitative tests", Long: `Run all quantitative tests`, RunE: runQuantitativeE, } runCmd.Flags().BoolP("markdown", "m", false, "Markdown table output mode") - runCmd.Flags().IntP("fast", "x", 0, "Process 1 in every X lines of input ('fast run' mode)") + runCmd.Flags().IntP("sample", "s", 0, "Process every s-th line of input (s % of lines)") runCmd.Flags().IntP("lines", "l", 0, "Number of lines of input to process before stopping") runCmd.Flags().IntP("paranoia-level", "P", 1, "Paranoia level used to run the quantitative tests") - runCmd.Flags().IntP("number", "n", 0, "Number is the payload line from the corpus to exclusively send") + runCmd.Flags().IntP("corpus-line", "n", 0, "Number is the payload line from the corpus to exclusively send") runCmd.Flags().StringP("payload", "p", "", "Payload is a string you want to test using quantitative tests. Will not use the corpus.") runCmd.Flags().IntP("rule", "r", 0, "Rule ID of interest: only show false positives for specified rule ID") runCmd.Flags().StringP("corpus", "c", "leipzig", "Corpus to use for the quantitative tests") - runCmd.Flags().StringP("corpus-lang", "L", "eng", "Corpus language to use for the quantitative tests.") - runCmd.Flags().StringP("corpus-size", "s", "100K", "Corpus size to use for the quantitative tests. Most corpus will have a size like \"100K\", \"1M\", etc.") + runCmd.Flags().StringP("corpus-lang", "L", "eng", "Corpus language to use for the quantitative tests") + runCmd.Flags().StringP("corpus-size", "s", "100K", "Corpus size to use for the quantitative tests. Most corpora will have sizes like \"100K\", \"1M\", etc.") runCmd.Flags().StringP("corpus-year", "y", "2023", "Corpus year to use for the quantitative tests. Most corpus will have a year like \"2023\", \"2022\", etc.") runCmd.Flags().StringP("corpus-source", "S", "news", "Corpus source to use for the quantitative tests. Most corpus will have a source like \"news\", \"web\", \"wikipedia\", etc.") runCmd.Flags().StringP("directory", "d", ".", "Directory where the CRS rules are stored") - runCmd.Flags().StringP("file", "f", "", "output file path for quantitative tests. Prints to standard output by default.") - runCmd.Flags().StringP("output", "o", "normal", "output type for quantitative tests. \"normal\" is the default.") + runCmd.Flags().StringP("file", "f", "", "Output file path for quantitative tests. Prints to standard output by default.") + runCmd.Flags().StringP("output", "o", "normal", "Output type for quantitative tests. \"normal\" is the default.") return runCmd } diff --git a/cmd/quantitative_test.go b/cmd/quantitative_test.go index 9aecfb6..0a6dd7d 100644 --- a/cmd/quantitative_test.go +++ b/cmd/quantitative_test.go @@ -55,7 +55,7 @@ func (s *quantitativeCmdTestSuite) TearDownTest() { func (s *quantitativeCmdTestSuite) TestQuantitativeCommand() { s.rootCmd.SetArgs([]string{"quantitative", "-d", s.tempDir}) cmd, err := s.rootCmd.ExecuteContextC(context.Background()) - s.Require().NoError(err, "quantitative command should not return an error") + s.Require().NoError(err, "quantitative command should not return error") s.Equal("quantitative", cmd.Name(), "quantitative command should have the name 'quantitative'") s.Require().NoError(err) } diff --git a/experimental/corpus/types.go b/experimental/corpus/types.go index df38b3d..bfa4866 100644 --- a/experimental/corpus/types.go +++ b/experimental/corpus/types.go @@ -6,12 +6,12 @@ // The corpus package provides an interface for working with corpora, as well as a set of built-in corpora // that can be used for detecting which text will generate false positives in WAF rules. // -// This interface includes methods for getting the URL of the corpus, getting the file from the remote URL, -// getting an iterator for the corpus, getting the payload given a line from the corpus iterator. Each corpus +// This interface includes methods for retrieving the URL of a corpus, fetching the file from the remote URL, +// creating an iterator for the corpus, and retrieving the payload of a given a line from the corpus iterator. Each corpus // will have a size, year, source, and language. -// The iterator interface includes methods for getting the next sentence from the corpus and checking if there +// The iterator interface includes methods for fetching the next sentence from the corpus and checking whether there // is another sentence in the corpus. -// Each corpus will need its own implementation of the Corpus interface. As this is an experimental package, this +// Each corpus must implement the corpus interface. As this is an experimental package, this // interface is subject to change. package corpus @@ -23,7 +23,7 @@ type CorpusFile struct { FilePath string } -// Corpus is the interface that needs to be implemented for getting the payload from a corpus +// Corpus is the interface that must be implemented to make a corpus available to clients type Corpus interface { // URL returns the URL of the corpus URL() string @@ -31,9 +31,8 @@ type Corpus interface { // WithURL sets the URL of the corpus WithURL(url string) Corpus - // GetCorpusFile gets the file from the remote url. - // It returns the local file path were the corpus is stored. - GetCorpusFile() CorpusFile + // FetchCorpusFile fetches the corpus file from the remote URL and returns a CorpusFile for interaction with the file. + FetchCorpusFile() CorpusFile // GetIterator returns an iterator for the corpus GetIterator(c CorpusFile) Iterator @@ -43,26 +42,30 @@ type Corpus interface { // Size returns the size of the corpus Size() string + // WithSize sets the size of the corpus - // Most corpus will have a size like "100K", "1M", etc., related to the amount of sentences in the corpus + // Most corpora will have a sizes like "100K", "1M", etc., related to the amount of sentences in the corpus WithSize(size string) Corpus // Year returns the year of the corpus Year() string + // WithYear sets the year of the corpus - // Most corpus will have a year like "2023", "2022", etc. + // Most corpora will have a year like "2023", "2022", etc. WithYear(year string) Corpus // Source returns the source of the corpus Source() string + // WithSource sets the source of the corpus - // Most corpus will have a source like "news", "web", "wikipedia", etc. + // Most corpora will have a source like "news", "web", "wikipedia", etc. WithSource(source string) Corpus - // Lang returns the language of the corpus - Lang() string + // Language returns the language of the corpus + Language() string + // WithLanguage sets the language of the corpus - // Most corpus will have a language like "eng", "de", etc. + // Most corpora will have a language like "eng", "de", etc. WithLanguage(lang string) Corpus } @@ -70,7 +73,7 @@ type Corpus interface { type Iterator interface { // Next returns the next sentence from the corpus Next() string - // HasNext returns true if there is another sentence in the corpus + // HasNext returns true unless the end of the corpus has been reached // false otherwise HasNext() bool } diff --git a/internal/quantitative/leipzig/corpus.go b/internal/quantitative/leipzig/corpus.go index 11aa490..62cb496 100644 --- a/internal/quantitative/leipzig/corpus.go +++ b/internal/quantitative/leipzig/corpus.go @@ -24,7 +24,8 @@ const ( defaultCorpusType = "sentences.txt" ) -// LeipzigCorpus is a corpus of text data +// LeipzigCorpus is a corpus of text data. +// Implements the Corpus interface. type LeipzigCorpus struct { // url_ is the URL of the corpus url_ string @@ -33,7 +34,7 @@ type LeipzigCorpus struct { // corpusFile is the original file name that contains the corpus file corpusFile string // File is the file name of the corpus - File string + Filename string // size is the size of the corpus size string // source is the source of the corpus @@ -149,7 +150,7 @@ func (c *LeipzigCorpus) GetPayload(line string) string { } // GetCorpusFile gets the file from the remote url. -// We assume that the file is compressed somehow, and we want to get a file inside it. +// We assume that the file is compressed somehow, and we want to get a file from the container. func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { home, err := os.UserHomeDir() if err != nil { @@ -160,7 +161,7 @@ func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { cacheDir := path.Join(home, ".ftw") - log.Debug().Msgf("Downloading corpus file from %s", url) + log.Debug().Msgf("Preparing download of corpus file from %s", url) dest := path.Join(cacheDir, "extracted") if err := os.MkdirAll(dest, os.ModePerm); err != nil { log.Fatal().Err(err).Msg("Could not create destination directory") @@ -205,7 +206,7 @@ func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { newPath := filepath.Join(cacheDir, info.Name()) err = os.Rename(path, newPath) if err != nil { - fmt.Println("Error renaming:", err) + fmt.Println("Error moving:", err) return err } fmt.Println("Moved", path, "to", newPath) diff --git a/internal/quantitative/leipzig/iterator.go b/internal/quantitative/leipzig/iterator.go index 4af1f44..4ac1d77 100644 --- a/internal/quantitative/leipzig/iterator.go +++ b/internal/quantitative/leipzig/iterator.go @@ -1,7 +1,7 @@ package leipzig import "bufio" - +// Implements the Iterator interface. type LeipzigIterator struct { scanner *bufio.Scanner } diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index b4a7bf7..15a17b8 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -68,9 +68,9 @@ func NewEngine(prefix string, paranoia int) *LocalEngine { return eng } -// CRSCall benchmarks the CRS WAF with a GET request +// CrsCall benchmarks the CRS WAF with a GET request // payload: the string to be passed in the request body -// returns the status of the transaction and a map of the matched rules with their IDs and the data that matched. +// returns the status of the HTTP response and a map of the matched rules with their IDs and the data that matched. func (e *LocalEngine) CRSCall(payload string) (int, map[int]string) { var status = http.StatusOK var matchedRules = make(map[int]string) @@ -105,10 +105,10 @@ func (e *LocalEngine) CRSCall(payload string) (int, map[int]string) { return status, matchedRules } -// crsWAF creates a WAF with the CRS rules +// newCrsWaf creates a WAF with the CRS rules // prefix: the path to the CRS rules // paranoiaLevel: 1 - 4 should be added as a template to the crs-setup.conf file -// If you want to run your own waf rules instead of crs, create a similar function to crsWAF +// If you want to run your own WAF rules instead of CRS, create a similar function to newCrsWaf func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { if prefix == "" { prefix = defaultPrefix diff --git a/internal/quantitative/local_engine_test.go b/internal/quantitative/local_engine_test.go index 4894d69..68238bf 100644 --- a/internal/quantitative/local_engine_test.go +++ b/internal/quantitative/local_engine_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -const crsURL = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" +const crsUrl = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" type localEngineTestSuite struct { suite.Suite @@ -42,7 +42,7 @@ func (s *localEngineTestSuite) TeardownTest() { } // TestCRSCall For this test you will need to have the Core Rule Set repository cloned in the parent directory as the project. -func (s *localEngineTestSuite) TestCRSCall() { +func (s *localEngineTestSuite) TestCrsCall() { // simple payload, no matches status, matchedRules := s.engine.CRSCall("this is a test") s.Require().Equal(http.StatusOK, status) diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 0b31229..2a71566 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -9,7 +9,7 @@ import ( "time" ) -// QuantitativeParams is the parameters for the quantitative tests +// QuantitativeParams holds the parameters for the quantitative tests type QuantitativeParams struct { // Lines is the number of lines of input to process before stopping Lines int @@ -83,14 +83,14 @@ func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { // Are we using the corpus at all? if params.Payload != "" { - // CRSCall with payload + // CrsCall with payload doEngineCall(runner, params.Payload, params.Rule, stats) } else { // iterate over the corpus for iter := corpusRunner.GetIterator(lc); iter.HasNext(); { line := iter.Next() stats.Run++ log.Trace().Msgf("Line: %s", line) - // check if we look for a specific payload line # + // check if we are looking for a specific payload line # if needSpecificPayload(params.Number, stats.Run) { continue } From ceb3e465aec6a7fc1ee24929a6b730fb96b70dc0 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sat, 21 Sep 2024 10:52:24 -0300 Subject: [PATCH 03/14] feat: move to interfaces Signed-off-by: Felipe Zipitria --- cmd/quantitative.go | 15 +- cmd/quantitative_test.go | 9 +- experimental/corpus/types.go | 46 ++++-- internal/quantitative/leipzig/corpus.go | 57 ++++---- internal/quantitative/leipzig/corpus_test.go | 28 ++-- internal/quantitative/leipzig/file.go | 39 +++++ internal/quantitative/leipzig/iterator.go | 17 ++- internal/quantitative/leipzig/payload.go | 41 ++++++ internal/quantitative/leipzig/payload_test.go | 138 ++++++++++++++++++ internal/quantitative/local_engine.go | 7 +- internal/quantitative/local_engine_test.go | 10 +- internal/quantitative/runner.go | 47 +++--- internal/quantitative/stats.go | 79 +++++++--- 13 files changed, 414 insertions(+), 119 deletions(-) create mode 100644 internal/quantitative/leipzig/file.go create mode 100644 internal/quantitative/leipzig/payload.go create mode 100644 internal/quantitative/leipzig/payload_test.go diff --git a/cmd/quantitative.go b/cmd/quantitative.go index 65980fb..396551b 100644 --- a/cmd/quantitative.go +++ b/cmd/quantitative.go @@ -4,10 +4,13 @@ package cmd import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/coreruleset/go-ftw/internal/quantitative" "github.com/coreruleset/go-ftw/output" - "github.com/spf13/cobra" - "os" ) // NewQuantitativeCmd @@ -20,8 +23,6 @@ func NewQuantitativeCmd() *cobra.Command { RunE: runQuantitativeE, } - runCmd.Flags().BoolP("markdown", "m", false, "Markdown table output mode") - runCmd.Flags().IntP("sample", "s", 0, "Process every s-th line of input (s % of lines)") runCmd.Flags().IntP("lines", "l", 0, "Number of lines of input to process before stopping") runCmd.Flags().IntP("paranoia-level", "P", 1, "Paranoia level used to run the quantitative tests") runCmd.Flags().IntP("corpus-line", "n", 0, "Number is the payload line from the corpus to exclusively send") @@ -50,7 +51,6 @@ func runQuantitativeE(cmd *cobra.Command, _ []string) error { directory, _ := cmd.Flags().GetString("directory") fast, _ := cmd.Flags().GetInt("fast") lines, _ := cmd.Flags().GetInt("lines") - markdown, _ := cmd.Flags().GetBool("markdown") outputFilename, _ := cmd.Flags().GetString("file") paranoiaLevel, _ := cmd.Flags().GetInt("paranoia-level") payload, _ := cmd.Flags().GetString("payload") @@ -58,6 +58,10 @@ func runQuantitativeE(cmd *cobra.Command, _ []string) error { rule, _ := cmd.Flags().GetInt("rule") wantedOutput, _ := cmd.Flags().GetString("output") + if paranoiaLevel > 1 && rule > 0 { + return fmt.Errorf("paranoia level and rule ID cannot be used together") + } + // use outputFile to write to file var outputFile *os.File var err error @@ -80,7 +84,6 @@ func runQuantitativeE(cmd *cobra.Command, _ []string) error { Directory: directory, Fast: fast, Lines: lines, - Markdown: markdown, ParanoiaLevel: paranoiaLevel, Number: number, Payload: payload, diff --git a/cmd/quantitative_test.go b/cmd/quantitative_test.go index 0a6dd7d..84a0058 100644 --- a/cmd/quantitative_test.go +++ b/cmd/quantitative_test.go @@ -5,16 +5,17 @@ package cmd import ( "context" - "github.com/spf13/cobra" - "github.com/stretchr/testify/suite" "io/fs" "os" "path" "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/suite" ) -var crsSetupFileContents = `# CRS Setup Configuration File` -var emptyRulesFile = `# Empty Rules File` +var crsSetupFileContents = `# CRS Setup Configuration filename` +var emptyRulesFile = `# Empty Rules filename` type quantitativeCmdTestSuite struct { suite.Suite diff --git a/experimental/corpus/types.go b/experimental/corpus/types.go index bfa4866..e0ba21e 100644 --- a/experimental/corpus/types.go +++ b/experimental/corpus/types.go @@ -1,3 +1,6 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + // Package corpus provides functionality for creating and managing corpora. // // A corpus is a collection of text documents that are used for training and testing machine learning models. @@ -15,12 +18,27 @@ // interface is subject to change. package corpus -// CorpusFile contains the cache directory and file name -type CorpusFile struct { +// Define an enum for CorpusType +type Type string + +const ( + Leipzig Type = "leipzig" +) + +// File interface is used to interact with Corpus files. +// It provides methods for setting the cache directory and file path. +type File interface { // CacheDir is the directory where files are cached - CacheDir string + CacheDir() string + // FilePath is the path to the cached file - FilePath string + FilePath() string + + // WithCacheDir sets the cache directory + WithCacheDir(cacheDir string) File + + // WithFilePath sets the file path + WithFilePath(filePath string) File } // Corpus is the interface that must be implemented to make a corpus available to clients @@ -32,24 +50,21 @@ type Corpus interface { WithURL(url string) Corpus // FetchCorpusFile fetches the corpus file from the remote URL and returns a CorpusFile for interaction with the file. - FetchCorpusFile() CorpusFile + FetchCorpusFile() File // GetIterator returns an iterator for the corpus - GetIterator(c CorpusFile) Iterator - - // GetPayload returns the payload given a line from the Corpus Iterator - GetPayload(line string) string + GetIterator(c File) Iterator // Size returns the size of the corpus Size() string - + // WithSize sets the size of the corpus // Most corpora will have a sizes like "100K", "1M", etc., related to the amount of sentences in the corpus WithSize(size string) Corpus // Year returns the year of the corpus Year() string - + // WithYear sets the year of the corpus // Most corpora will have a year like "2023", "2022", etc. WithYear(year string) Corpus @@ -72,8 +87,15 @@ type Corpus interface { // Iterator is an interface for iterating over a corpus type Iterator interface { // Next returns the next sentence from the corpus - Next() string + Next() Payload // HasNext returns true unless the end of the corpus has been reached // false otherwise HasNext() bool } + +type Payload interface { + // LineNumber returns the payload given a line from the Corpus Iterator + LineNumber() int + // Content returns the payload given a line from the Corpus Iterator + Content() string +} diff --git a/internal/quantitative/leipzig/corpus.go b/internal/quantitative/leipzig/corpus.go index 62cb496..a0e2c23 100644 --- a/internal/quantitative/leipzig/corpus.go +++ b/internal/quantitative/leipzig/corpus.go @@ -1,15 +1,19 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package leipzig import ( "bufio" "fmt" - "github.com/coreruleset/go-ftw/experimental/corpus" - "github.com/hashicorp/go-getter" - "github.com/rs/zerolog/log" "os" "path" "path/filepath" - "strings" + + "github.com/hashicorp/go-getter" + "github.com/rs/zerolog/log" + + "github.com/coreruleset/go-ftw/experimental/corpus" ) // LeipzigCorpus represents a corpus of text data @@ -33,8 +37,8 @@ type LeipzigCorpus struct { lang string // corpusFile is the original file name that contains the corpus file corpusFile string - // File is the file name of the corpus - Filename string + // filename is the file name of the corpus + filename string // size is the size of the corpus size string // source is the source of the corpus @@ -47,7 +51,7 @@ func (c *LeipzigCorpus) regenerateFileNames() { c.corpusFile = fmt.Sprintf("%s_%s_%s_%s.%s", c.lang, c.source, c.year, c.size, defaultCorpusExt) - c.File = fmt.Sprintf("%s_%s_%s_%s-%s", + c.filename = fmt.Sprintf("%s_%s_%s_%s-%s", c.lang, c.source, c.year, c.size, defaultCorpusType) } @@ -57,7 +61,7 @@ func NewLeipzigCorpus() corpus.Corpus { leipzig := &LeipzigCorpus{ url_: defaultCorpusSite, corpusFile: "", - File: "", + filename: "", lang: defaultCorpusLanguage, source: defaultCorpusSource, year: defaultCorpusYear, @@ -115,7 +119,7 @@ func (c *LeipzigCorpus) WithSource(source string) corpus.Corpus { } // Lang returns the language of the corpus -func (c *LeipzigCorpus) Lang() string { +func (c *LeipzigCorpus) Language() string { return c.lang } @@ -126,14 +130,15 @@ func (c *LeipzigCorpus) WithLanguage(lang string) corpus.Corpus { } // GetIterator returns an iterator for the corpus -func (c *LeipzigCorpus) GetIterator(cache corpus.CorpusFile) corpus.Iterator { +func (c *LeipzigCorpus) GetIterator(cache corpus.File) corpus.Iterator { // open cache file - if cache.FilePath == "" { + cached := cache.FilePath() + if cached == "" { log.Fatal().Msg("Cache file path is empty") } - file, err := os.Open(cache.FilePath) + file, err := os.Open(cached) if err != nil { - log.Fatal().Err(err).Msgf("Could not open the file %s", cache.FilePath) + log.Fatal().Err(err).Msgf("Could not open the file %s", cached) } scanner := bufio.NewScanner(file) it := &LeipzigIterator{ @@ -142,16 +147,9 @@ func (c *LeipzigCorpus) GetIterator(cache corpus.CorpusFile) corpus.Iterator { return it } -// GetPayload returns the payload from the line -// We assume that the first word is the line number, -// and we want the rest -func (c *LeipzigCorpus) GetPayload(line string) string { - return strings.Join(strings.Split(line, "\t")[1:], " ") -} - -// GetCorpusFile gets the file from the remote url. +// FetchCorpusFile gets the file from the remote url. // We assume that the file is compressed somehow, and we want to get a file from the container. -func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { +func (c *LeipzigCorpus) FetchCorpusFile() corpus.File { home, err := os.UserHomeDir() if err != nil { log.Fatal().Err(err).Msg("Could not get home directory") @@ -167,14 +165,11 @@ func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { log.Fatal().Err(err).Msg("Could not create destination directory") } - cache := corpus.CorpusFile{ - CacheDir: cacheDir, - FilePath: "", - } + cache := NewFile().WithCacheDir(cacheDir) - if info, err := os.Stat(path.Join(home, ".ftw", c.File)); err == nil { - log.Debug().Msgf("File %s already exists", info.Name()) - cache.FilePath = path.Join(home, ".ftw", c.File) + if info, err := os.Stat(path.Join(home, ".ftw", cache.FilePath())); err == nil { + log.Debug().Msgf("filename %s already exists", info.Name()) + cache = cache.WithFilePath(path.Join(home, ".ftw", c.filename)) return cache } @@ -202,7 +197,7 @@ func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { log.Trace().Msgf("Checking file %s", info.Name()) - if info.Name() == c.File { + if info.Name() == c.filename { newPath := filepath.Join(cacheDir, info.Name()) err = os.Rename(path, newPath) if err != nil { @@ -210,7 +205,7 @@ func (c *LeipzigCorpus) GetCorpusFile() corpus.CorpusFile { return err } fmt.Println("Moved", path, "to", newPath) - cache.FilePath = newPath + cache = cache.WithFilePath(newPath) } return nil diff --git a/internal/quantitative/leipzig/corpus_test.go b/internal/quantitative/leipzig/corpus_test.go index cd55f98..a80f1f6 100644 --- a/internal/quantitative/leipzig/corpus_test.go +++ b/internal/quantitative/leipzig/corpus_test.go @@ -1,15 +1,20 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package leipzig import ( - "github.com/coreruleset/go-ftw/experimental/corpus" - "github.com/stretchr/testify/suite" "testing" + + "github.com/stretchr/testify/suite" + + "github.com/coreruleset/go-ftw/experimental/corpus" ) type leipzigCorpusTestSuite struct { suite.Suite corpus corpus.Corpus - cache corpus.CorpusFile + cache corpus.File iter corpus.Iterator } @@ -20,7 +25,7 @@ func TestLeipzigCorpusTestSuite(t *testing.T) { func (s *leipzigCorpusTestSuite) SetupTest() { s.corpus = NewLeipzigCorpus() s.Require().Equal("https://downloads.wortschatz-leipzig.de/corpora", s.corpus.URL()) - s.Require().Equal("eng", s.corpus.Lang()) + s.Require().Equal("eng", s.corpus.Language()) s.Require().Equal("100K", s.corpus.Size()) s.Require().Equal("news", s.corpus.Source()) s.Require().Equal("2023", s.corpus.Year()) @@ -33,20 +38,15 @@ func (s *leipzigCorpusTestSuite) TestWithSize() { func (s *leipzigCorpusTestSuite) TestGetIterator() { s.corpus.WithSize("10K") - s.cache = s.corpus.GetCorpusFile() + s.cache = s.corpus.FetchCorpusFile() s.iter = s.corpus.GetIterator(s.cache) } func (s *leipzigCorpusTestSuite) TestNextSentenceFromCorpus() { - s.cache = s.corpus.GetCorpusFile() - s.iter = s.corpus.GetIterator(s.cache) - s.Require().True(s.iter.HasNext()) - s.Require().Equal("1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", s.iter.Next()) -} - -func (s *leipzigCorpusTestSuite) TestGetPayloadFromString() { - s.cache = s.corpus.GetCorpusFile() + s.cache = s.corpus.FetchCorpusFile() s.iter = s.corpus.GetIterator(s.cache) s.Require().True(s.iter.HasNext()) - s.Require().Equal("1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", s.iter.Next()) + payload := s.iter.Next() + s.Require().Equal(1, payload.LineNumber()) + s.Require().Equal("$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", payload.Content()) } diff --git a/internal/quantitative/leipzig/file.go b/internal/quantitative/leipzig/file.go new file mode 100644 index 0000000..3d67737 --- /dev/null +++ b/internal/quantitative/leipzig/file.go @@ -0,0 +1,39 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package leipzig + +import "github.com/coreruleset/go-ftw/experimental/corpus" + +// File implements the corpus.File interface. +type File struct { + cacheDir string + filePath string +} + +// NewFile returns a new File +func NewFile() corpus.File { + return File{} +} + +// CacheDir is the directory where files are cached +func (f File) CacheDir() string { + return f.cacheDir +} + +// FilePath is the path to the cached file +func (f File) FilePath() string { + return f.filePath +} + +// WithCacheDir sets the cache directory +func (f File) WithCacheDir(cacheDir string) corpus.File { + f.cacheDir = cacheDir + return f +} + +// WithFilePath sets the file path +func (f File) WithFilePath(filePath string) corpus.File { + f.filePath = filePath + return f +} diff --git a/internal/quantitative/leipzig/iterator.go b/internal/quantitative/leipzig/iterator.go index 4ac1d77..4ec71fc 100644 --- a/internal/quantitative/leipzig/iterator.go +++ b/internal/quantitative/leipzig/iterator.go @@ -1,9 +1,18 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package leipzig -import "bufio" +import ( + "bufio" + + "github.com/coreruleset/go-ftw/experimental/corpus" +) + // Implements the Iterator interface. type LeipzigIterator struct { scanner *bufio.Scanner + line int } // HasNext returns true if there is another sentence in the corpus @@ -12,6 +21,8 @@ func (c *LeipzigIterator) HasNext() bool { } // Next returns the next sentence from the corpus -func (c *LeipzigIterator) Next() string { - return c.scanner.Text() +func (c *LeipzigIterator) Next() corpus.Payload { + p := c.scanner.Text() + c.line++ + return NewPayload(p) } diff --git a/internal/quantitative/leipzig/payload.go b/internal/quantitative/leipzig/payload.go new file mode 100644 index 0000000..9d60e2f --- /dev/null +++ b/internal/quantitative/leipzig/payload.go @@ -0,0 +1,41 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package leipzig + +import ( + "strconv" + "strings" +) + +// Payload implements the corpus.Payload interface. +type Payload struct { + line int + payload string +} + +// NewPayload returns a new Payload +func NewPayload(line string) *Payload { + split := strings.Split(line, "\t") + // convert to int + num, err := strconv.Atoi(split[0]) + if err != nil { + num = -1 + } + p := strings.Join(split[1:], " ") + return &Payload{ + line: num, + payload: p, + } +} + +// LineNumber returns the payload given a line from the Corpus Iterator +// If the line number is not a number, it will return -1 +func (p *Payload) LineNumber() int { + return p.line +} + +// Content returns the payload given a line from the Corpus Iterator +func (p *Payload) Content() string { + return p.payload +} diff --git a/internal/quantitative/leipzig/payload_test.go b/internal/quantitative/leipzig/payload_test.go new file mode 100644 index 0000000..52df43f --- /dev/null +++ b/internal/quantitative/leipzig/payload_test.go @@ -0,0 +1,138 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package leipzig + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/suite" +) + +type payloadTestSuite struct { + suite.Suite +} + +func TestPayloadTestSuite(t *testing.T) { + suite.Run(t, new(payloadTestSuite)) +} + +func (s *payloadTestSuite) TestNewPayload() { + type args struct { + line string + } + tests := []struct { + name string + args args + want *Payload + }{ + { + name: "TestNewPayload", + args: args{ + line: "1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", + }, + want: &Payload{ + line: 1, + payload: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", + }, + }, + { + name: "TestAdditional", + args: args{ + line: "2000\tThis is an additional payload", + }, + want: &Payload{ + line: 2000, + payload: "This is an additional payload", + }, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + if got := NewPayload(tt.args.line); !reflect.DeepEqual(got, tt.want) { + s.Require().Equal(got, tt.want) + } + }) + } +} + +func (s *payloadTestSuite) TestPayload_Content() { + type fields struct { + line int + payload string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "TestContent", + fields: fields{ + line: 1, + payload: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", + }, + want: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", + }, + { + name: "TestContent2", + fields: fields{ + line: 2000, + payload: "This is another test payload", + }, + want: "This is another test payload", + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + p := &Payload{ + line: tt.fields.line, + payload: tt.fields.payload, + } + if got := p.Content(); got != tt.want { + s.Require().Equal(got, tt.want) + } + }) + } +} + +func (s *payloadTestSuite) TestPayload_LineNumber() { + type fields struct { + line int + payload string + } + tests := []struct { + name string + fields fields + want int + }{ + { + name: "TestLineNumber", + fields: fields{ + line: 1, + payload: "This is a test payload", + }, + want: 1, + }, + { + name: "TestLineNumber2", + fields: fields{ + line: 2000, + payload: "This is another test payload", + }, + want: 2000, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + p := &Payload{ + line: tt.fields.line, + payload: tt.fields.payload, + } + if got := p.LineNumber(); got != tt.want { + s.Require().Equal(got, tt.want) + } + }) + } +} diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index 15a17b8..51724ce 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -6,15 +6,16 @@ package quantitative import ( "bytes" "fmt" - "github.com/corazawaf/coraza/v3" - "github.com/corazawaf/coraza/v3/types" - "github.com/rs/zerolog/log" "net/http" "net/url" "os" "strconv" "strings" "text/template" + + "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/types" + "github.com/rs/zerolog/log" ) const ( diff --git a/internal/quantitative/local_engine_test.go b/internal/quantitative/local_engine_test.go index 68238bf..15af9e6 100644 --- a/internal/quantitative/local_engine_test.go +++ b/internal/quantitative/local_engine_test.go @@ -1,12 +1,16 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package quantitative import ( - "github.com/hashicorp/go-getter" - "github.com/stretchr/testify/suite" "net/http" "os" "path" "testing" + + "github.com/hashicorp/go-getter" + "github.com/stretchr/testify/suite" ) const crsUrl = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" @@ -26,7 +30,7 @@ func (s *localEngineTestSuite) SetupTest() { s.Require().NoError(os.MkdirAll(s.dir, 0755)) client := &getter.Client{ Mode: getter.ClientModeAny, - Src: crsURL, + Src: crsUrl, Dst: s.dir, } diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 2a71566..345be6c 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -1,12 +1,17 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package quantitative import ( + "net/http" + "time" + + "github.com/rs/zerolog/log" + "github.com/coreruleset/go-ftw/experimental/corpus" "github.com/coreruleset/go-ftw/internal/quantitative/leipzig" "github.com/coreruleset/go-ftw/output" - "github.com/rs/zerolog/log" - "net/http" - "time" ) // QuantitativeParams holds the parameters for the quantitative tests @@ -23,8 +28,6 @@ type QuantitativeParams struct { Number int // Directory is the directory where the CRS rules are stored Directory string - // Markdown is the Markdown table output mode - Markdown bool // ParanoiaLevel is the paranoia level in where to run the quantitative tests ParanoiaLevel int // CorpusSize is the corpus size to use for the quantitative tests @@ -40,19 +43,19 @@ type QuantitativeParams struct { } // NewCorpus creates a new corpus -func NewCorpus(name string) corpus.Corpus { - switch name { - case "leipzig": +func NewCorpus(corpusType corpus.Type) corpus.Corpus { + switch corpusType { + case corpus.Leipzig: return leipzig.NewLeipzigCorpus() default: - log.Fatal().Msgf("Unknown corpus %s", name) + log.Fatal().Msgf("Unknown corpus implementation: %s", corpusType) return nil } } // RunQuantitativeTests runs all quantitative tests func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { - log.Info().Msg("Running quantitative tests") + out.Println("Running quantitative tests") log.Trace().Msgf("Lines: %d", params.Lines) log.Trace().Msgf("Fast: %d", params.Fast) @@ -60,7 +63,6 @@ func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { log.Trace().Msgf("Payload: %s", params.Payload) log.Trace().Msgf("Read Corpus Line: %d", params.Number) log.Trace().Msgf("Directory: %s", params.Directory) - log.Trace().Msgf("Markdown: %t", params.Markdown) log.Trace().Msgf("Paranoia level: %d", params.ParanoiaLevel) log.Trace().Msgf("Corpus size: %s", params.CorpusSize) log.Trace().Msgf("Corpus lang: %s", params.CorpusLang) @@ -68,14 +70,14 @@ func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { startTime := time.Now() // create a new corpusRunner - corpusRunner := NewCorpus(params.Corpus). + corpusRunner := NewCorpus(corpus.Leipzig). WithSize(params.CorpusSize). WithYear(params.CorpusYear). WithSource(params.CorpusSource). WithLanguage(params.CorpusLang) // download the corpusRunner file - lc := corpusRunner.GetCorpusFile() + lc := corpusRunner.FetchCorpusFile() // create the results stats := NewQuantitativeStats() @@ -83,30 +85,31 @@ func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { // Are we using the corpus at all? if params.Payload != "" { + log.Trace().Msgf("Payload received from cmdline: %s", params.Payload) // CrsCall with payload doEngineCall(runner, params.Payload, params.Rule, stats) } else { // iterate over the corpus + log.Trace().Msgf("Iterating over corpus") for iter := corpusRunner.GetIterator(lc); iter.HasNext(); { - line := iter.Next() - stats.Run++ - log.Trace().Msgf("Line: %s", line) + p := iter.Next() + stats.incrementRun() + payload := p.Content() + log.Trace().Msgf("Line: %s", payload) // check if we are looking for a specific payload line # - if needSpecificPayload(params.Number, stats.Run) { + if needSpecificPayload(params.Number, stats.Count()) { continue } - // ask the corpus to get the payload - payload := corpusRunner.GetPayload(line) - log.Trace().Msgf("Payload: %s", payload) // check if we only want to process a specific number of lines - if params.Lines > 0 && stats.Run >= params.Lines { + if params.Lines > 0 && stats.Count() >= params.Lines { break } + doEngineCall(runner, payload, params.Rule, stats) } } - stats.TotalTime = time.Since(startTime) + stats.SetTotalTime(time.Since(startTime)) stats.printSummary(out) return nil } diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go index 8a2e6d4..6969598 100644 --- a/internal/quantitative/stats.go +++ b/internal/quantitative/stats.go @@ -1,53 +1,90 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + package quantitative import ( "encoding/json" - "github.com/coreruleset/go-ftw/output" - "github.com/rs/zerolog/log" "time" + + "github.com/rs/zerolog/log" + + "github.com/coreruleset/go-ftw/output" ) // RunStats accumulates test statistics. type QuantitativeRunStats struct { - // Run is the amount of tests executed in this run. - Run int `json:"run"` - // TotalTime is the duration over all runs, the sum of all individual run times. - TotalTime time.Duration - // FalsePositives is the total false positives detected - FalsePositives int `json:"falsePositives"` - // FalsePositivesPerRule is the aggregated false positives per rule - FalsePositivesPerRule map[int]int `json:"falsePositivesPerRule"` + // count_ is the amount of tests executed in this run. + count_ int + // totalTime is the duration over all runs, the sum of all individual run times. + totalTime time.Duration + // falsePositives is the total false positives detected + falsePositives int + // falsePositivesPerRule is the aggregated false positives per rule + falsePositivesPerRule map[int]int } // NewQuantitativeStats returns a new empty stats func NewQuantitativeStats() *QuantitativeRunStats { return &QuantitativeRunStats{ - Run: 0, - FalsePositives: 0, - FalsePositivesPerRule: make(map[int]int), - TotalTime: 0, + count_: 0, + falsePositives: 0, + falsePositivesPerRule: make(map[int]int), + totalTime: 0, } } // print final statistics func (s *QuantitativeRunStats) printSummary(out *output.Output) { log.Debug().Msg("Printing Stats summary") - if s.FalsePositives > 0 { + if s.falsePositives > 0 { if out.IsJson() { b, _ := json.Marshal(s) out.RawPrint(string(b)) } else { - ratio := float64(s.FalsePositives) / float64(s.Run) - out.Println("Run %d payloads in %s", s.Run, s.TotalTime) - out.Println("Total False positive ratio: %d/%d = %.4f", s.FalsePositives, s.Run, ratio) - out.Println("False positives per rule: %+v", s.FalsePositivesPerRule) + ratio := float64(s.falsePositives) / float64(s.count_) + out.Println("Run %d payloads in %s", s.count_, s.totalTime) + out.Println("Total False positive ratio: %d/%d = %.4f", s.falsePositives, s.count_, ratio) + out.Println("False positives per rule: %+v", s.falsePositivesPerRule) // echo "| Freq. | ID # | Paranoia Level |" // echo "| ------ | ------ | -------------- |" } } } +// addFalsePositive increments the false positive count and the false positive count for the rule. func (s *QuantitativeRunStats) addFalsePositive(rule int) { - s.FalsePositives++ - s.FalsePositivesPerRule[rule]++ + s.falsePositives++ + s.falsePositivesPerRule[rule]++ +} + +// incrementRun increments the amount of tests executed in this run. +func (s *QuantitativeRunStats) incrementRun() { + s.count_++ +} + +// Count returns the amount of tests executed in this run. +func (s *QuantitativeRunStats) Count() int { + return s.count_ +} + +// TotalTime returns the duration over all runs, the sum of all individual run times. +func (s *QuantitativeRunStats) TotalTime() time.Duration { + return s.totalTime +} + +// SetTotalTime sets the duration over all runs, the sum of all individual run times. +func (s *QuantitativeRunStats) SetTotalTime(totalTime time.Duration) { + s.totalTime = totalTime +} + +// MarshalJSON marshals the stats to JSON. +func (s *QuantitativeRunStats) MarshalJSON() ([]byte, error) { + // Custom marshaling logic here + return json.Marshal(map[string]interface{}{ + "count": s.count_, + "totalTime": s.totalTime, + "falsePositives": s.falsePositives, + "falsePositivesPerRule": s.falsePositivesPerRule, + }) } From 56d047db07e477a179eef978ac90ece92947d434 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sat, 21 Sep 2024 11:22:19 -0300 Subject: [PATCH 04/14] test: add more coverage Signed-off-by: Felipe Zipitria --- cmd/quantitative.go | 15 +- experimental/corpus/types.go | 18 +- internal/quantitative/leipzig/corpus.go | 16 +- internal/quantitative/leipzig/corpus_test.go | 22 ++- internal/quantitative/leipzig/file_test.go | 186 +++++++++++++++++++ internal/quantitative/local_engine_test.go | 8 +- internal/quantitative/runner.go | 8 +- internal/quantitative/runner_test.go | 74 ++++++++ internal/quantitative/stats.go | 5 + internal/quantitative/stats_test.go | 131 +++++++++++++ 10 files changed, 468 insertions(+), 15 deletions(-) create mode 100644 internal/quantitative/leipzig/file_test.go create mode 100644 internal/quantitative/runner_test.go create mode 100644 internal/quantitative/stats_test.go diff --git a/cmd/quantitative.go b/cmd/quantitative.go index 396551b..ea1d9f2 100644 --- a/cmd/quantitative.go +++ b/cmd/quantitative.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + "github.com/coreruleset/go-ftw/experimental/corpus" "github.com/coreruleset/go-ftw/internal/quantitative" "github.com/coreruleset/go-ftw/output" ) @@ -43,7 +44,7 @@ func NewQuantitativeCmd() *cobra.Command { func runQuantitativeE(cmd *cobra.Command, _ []string) error { cmd.SilenceUsage = true - corpus, _ := cmd.Flags().GetString("corpus") + corpusTypeAsString, _ := cmd.Flags().GetString("corpus") corpusSize, _ := cmd.Flags().GetString("corpus-size") corpusLang, _ := cmd.Flags().GetString("corpus-lang") corpusYear, _ := cmd.Flags().GetString("corpus-year") @@ -75,8 +76,16 @@ func runQuantitativeE(cmd *cobra.Command, _ []string) error { } out := output.NewOutput(wantedOutput, outputFile) - params := quantitative.QuantitativeParams{ - Corpus: corpus, + var corpusType corpus.Type + if corpusTypeAsString != "" { + err = corpusType.Set(corpusTypeAsString) + if err != nil { + return err + } + } + + params := quantitative.Params{ + Corpus: corpusType, CorpusSize: corpusSize, CorpusYear: corpusYear, CorpusLang: corpusLang, diff --git a/experimental/corpus/types.go b/experimental/corpus/types.go index e0ba21e..bc3e194 100644 --- a/experimental/corpus/types.go +++ b/experimental/corpus/types.go @@ -18,13 +18,29 @@ // interface is subject to change. package corpus -// Define an enum for CorpusType +import "fmt" + +// Type is the type of the corpus type Type string const ( Leipzig Type = "leipzig" ) +func (t *Type) String() string { + return string(*t) +} + +func (t *Type) Set(value string) error { + switch value { + case "leipzig": + *t = Leipzig + return nil + default: + return fmt.Errorf("invalid option for Type: '%s'", value) + } +} + // File interface is used to interact with Corpus files. // It provides methods for setting the cache directory and file path. type File interface { diff --git a/internal/quantitative/leipzig/corpus.go b/internal/quantitative/leipzig/corpus.go index a0e2c23..de57e5d 100644 --- a/internal/quantitative/leipzig/corpus.go +++ b/internal/quantitative/leipzig/corpus.go @@ -73,22 +73,24 @@ func NewLeipzigCorpus() corpus.Corpus { return leipzig } -// size returns the size of the corpus +// Size returns the size of the corpus func (c *LeipzigCorpus) Size() string { return c.size } +// WithSize sets the size of the corpus func (c *LeipzigCorpus) WithSize(size string) corpus.Corpus { c.size = size c.regenerateFileNames() return c } -// year returns the year of the corpus +// Year returns the year of the corpus func (c *LeipzigCorpus) Year() string { return c.year } +// WithYear sets the year of the corpus func (c *LeipzigCorpus) WithYear(year string) corpus.Corpus { c.year = year c.regenerateFileNames() @@ -112,17 +114,19 @@ func (c *LeipzigCorpus) Source() string { return c.source } +// WithSource sets the source of the corpus func (c *LeipzigCorpus) WithSource(source string) corpus.Corpus { c.source = source c.regenerateFileNames() return c } -// Lang returns the language of the corpus +// Language returns the language of the corpus func (c *LeipzigCorpus) Language() string { return c.lang } +// WithLanguage sets the language of the corpus func (c *LeipzigCorpus) WithLanguage(lang string) corpus.Corpus { c.lang = lang c.regenerateFileNames() @@ -165,7 +169,11 @@ func (c *LeipzigCorpus) FetchCorpusFile() corpus.File { log.Fatal().Err(err).Msg("Could not create destination directory") } - cache := NewFile().WithCacheDir(cacheDir) + cache := NewFile().WithCacheDir(cacheDir).WithFilePath(c.filename) + + if cache.FilePath() == "" { + log.Fatal().Msg("Cache file path is empty") + } if info, err := os.Stat(path.Join(home, ".ftw", cache.FilePath())); err == nil { log.Debug().Msgf("filename %s already exists", info.Name()) diff --git a/internal/quantitative/leipzig/corpus_test.go b/internal/quantitative/leipzig/corpus_test.go index a80f1f6..314d626 100644 --- a/internal/quantitative/leipzig/corpus_test.go +++ b/internal/quantitative/leipzig/corpus_test.go @@ -36,8 +36,28 @@ func (s *leipzigCorpusTestSuite) TestWithSize() { s.Require().Equal("300K", s.corpus.Size()) } +func (s *leipzigCorpusTestSuite) TestWithYear() { + s.corpus.WithYear("2024") + s.Require().Equal("2024", s.corpus.Year()) +} + +func (s *leipzigCorpusTestSuite) TestWithSource() { + s.corpus.WithSource("news") + s.Require().Equal("news", s.corpus.Source()) +} + +func (s *leipzigCorpusTestSuite) TestWithLanguage() { + s.corpus.WithLanguage("eng") + s.Require().Equal("eng", s.corpus.Language()) +} + +func (s *leipzigCorpusTestSuite) TestWithURL() { + s.corpus.WithURL("https://downloads.wortschatz-leipzig.de/corpora") + s.Require().Equal("https://downloads.wortschatz-leipzig.de/corpora", s.corpus.URL()) +} + func (s *leipzigCorpusTestSuite) TestGetIterator() { - s.corpus.WithSize("10K") + s.corpus = s.corpus.WithSize("10K") s.cache = s.corpus.FetchCorpusFile() s.iter = s.corpus.GetIterator(s.cache) } diff --git a/internal/quantitative/leipzig/file_test.go b/internal/quantitative/leipzig/file_test.go new file mode 100644 index 0000000..1fcb878 --- /dev/null +++ b/internal/quantitative/leipzig/file_test.go @@ -0,0 +1,186 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package leipzig + +import ( + "reflect" + "testing" + + "github.com/coreruleset/go-ftw/experimental/corpus" +) + +func TestFile_CacheDir(t *testing.T) { + type fields struct { + cacheDir string + filePath string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1", + fields: fields{ + cacheDir: "cacheDir", + filePath: "filePath", + }, + want: "cacheDir", + }, + { + name: "Test 2", + fields: fields{ + cacheDir: "cacheDir2", + filePath: "filePath2", + }, + want: "cacheDir2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File{ + cacheDir: tt.fields.cacheDir, + filePath: tt.fields.filePath, + } + if got := f.CacheDir(); got != tt.want { + t.Errorf("CacheDir() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFile_FilePath(t *testing.T) { + type fields struct { + cacheDir string + filePath string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1", + fields: fields{ + cacheDir: "cacheDir", + filePath: "filePath", + }, + want: "filePath", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File{ + cacheDir: tt.fields.cacheDir, + filePath: tt.fields.filePath, + } + if got := f.FilePath(); got != tt.want { + t.Errorf("FilePath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFile_WithCacheDir(t *testing.T) { + type fields struct { + cacheDir string + filePath string + } + type args struct { + cacheDir string + } + tests := []struct { + name string + fields fields + args args + want corpus.File + }{ + { + name: "Test 1", + fields: fields{ + cacheDir: "cacheDir1", + filePath: "filePath", + }, + args: args{ + cacheDir: "cacheDir10", + }, + want: File{ + cacheDir: "cacheDir10", + filePath: "filePath", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File{ + cacheDir: tt.fields.cacheDir, + filePath: tt.fields.filePath, + } + if got := f.WithCacheDir(tt.args.cacheDir); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithCacheDir() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFile_WithFilePath(t *testing.T) { + type fields struct { + cacheDir string + filePath string + } + type args struct { + filePath string + } + tests := []struct { + name string + fields fields + args args + want corpus.File + }{ + { + name: "Test 1", + fields: fields{ + cacheDir: "cacheDir", + filePath: "filePath1", + }, + args: args{ + filePath: "filePath2", + }, + want: File{ + cacheDir: "cacheDir", + filePath: "filePath2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File{ + cacheDir: tt.fields.cacheDir, + filePath: tt.fields.filePath, + } + if got := f.WithFilePath(tt.args.filePath); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithFilePath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewFile(t *testing.T) { + tests := []struct { + name string + want corpus.File + }{ + { + name: "Test 1", + want: File{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewFile(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewFile() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/quantitative/local_engine_test.go b/internal/quantitative/local_engine_test.go index 15af9e6..a347db9 100644 --- a/internal/quantitative/local_engine_test.go +++ b/internal/quantitative/local_engine_test.go @@ -4,6 +4,7 @@ package quantitative import ( + "fmt" "net/http" "os" "path" @@ -13,7 +14,10 @@ import ( "github.com/stretchr/testify/suite" ) -const crsUrl = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" +const ( + crsUrl = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" + crsTestVersion = "4.6.0" +) type localEngineTestSuite struct { suite.Suite @@ -36,7 +40,7 @@ func (s *localEngineTestSuite) SetupTest() { err := client.Get() s.Require().NoError(err) - s.engine = NewEngine(path.Join(s.dir, "coreruleset-4.6.0"), 1) + s.engine = NewEngine(path.Join(s.dir, fmt.Sprintf("coreruleset-%s", crsTestVersion)), 1) s.Require().NotNil(s.engine) } diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 345be6c..adc97eb 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -14,8 +14,8 @@ import ( "github.com/coreruleset/go-ftw/output" ) -// QuantitativeParams holds the parameters for the quantitative tests -type QuantitativeParams struct { +// Params holds the parameters for the quantitative tests +type Params struct { // Lines is the number of lines of input to process before stopping Lines int // Fast is the process 1 in every X lines of input ('fast run' mode) @@ -33,7 +33,7 @@ type QuantitativeParams struct { // CorpusSize is the corpus size to use for the quantitative tests CorpusSize string // Corpus is the corpus to use for the quantitative tests - Corpus string + Corpus corpus.Type // CorpusLang is the language of the corpus CorpusLang string // CorpusYear is the year of the corpus @@ -54,7 +54,7 @@ func NewCorpus(corpusType corpus.Type) corpus.Corpus { } // RunQuantitativeTests runs all quantitative tests -func RunQuantitativeTests(params QuantitativeParams, out *output.Output) error { +func RunQuantitativeTests(params Params, out *output.Output) error { out.Println("Running quantitative tests") log.Trace().Msgf("Lines: %d", params.Lines) diff --git a/internal/quantitative/runner_test.go b/internal/quantitative/runner_test.go new file mode 100644 index 0000000..275f7cb --- /dev/null +++ b/internal/quantitative/runner_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package quantitative + +import ( + "bytes" + "fmt" + "os" + "path" + "testing" + + "github.com/hashicorp/go-getter" + "github.com/stretchr/testify/suite" + + "github.com/coreruleset/go-ftw/experimental/corpus" + "github.com/coreruleset/go-ftw/output" +) + +type runnerTestSuite struct { + suite.Suite + params Params + c corpus.Corpus + dir string +} + +func TestRunnerTestSuite(t *testing.T) { + suite.Run(t, new(runnerTestSuite)) +} + +func (s *runnerTestSuite) SetupTest() { + s.params = Params{ + Lines: 1000, + Fast: 10, + Rule: 1000, + Payload: "test", + Number: 1000, + Directory: path.Join(s.dir, fmt.Sprintf("coreruleset-%s", crsTestVersion)), + ParanoiaLevel: 1, + CorpusSize: "10K", + Corpus: "leipzig", + CorpusLang: "eng", + CorpusYear: "2023", + CorpusSource: "news", + } + s.dir = path.Join(os.TempDir()) + s.Require().NoError(os.MkdirAll(s.dir, 0755)) + client := &getter.Client{ + Mode: getter.ClientModeAny, + Src: crsUrl, + Dst: s.dir, + } + + err := client.Get() + s.Require().NoError(err) +} + +func (s *runnerTestSuite) TeardownTest() { + err := os.RemoveAll(s.dir) + s.Require().NoError(err) +} + +func (s *runnerTestSuite) TestNewCorpus() { + s.c = NewCorpus(corpus.Leipzig) + s.Require().NotNil(s.c) + s.Require().Equal(s.c.URL(), "https://downloads.wortschatz-leipzig.de/corpora") +} + +func (s *runnerTestSuite) TestRunQuantitativeTests() { + var b bytes.Buffer + out := output.NewOutput("plain", &b) + err := RunQuantitativeTests(s.params, out) + s.Require().NoError(err) +} diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go index 6969598..7fa63a3 100644 --- a/internal/quantitative/stats.go +++ b/internal/quantitative/stats.go @@ -58,6 +58,11 @@ func (s *QuantitativeRunStats) addFalsePositive(rule int) { s.falsePositivesPerRule[rule]++ } +// FalsePositives returns the total false positives detected +func (s *QuantitativeRunStats) FalsePositives() int { + return s.falsePositives +} + // incrementRun increments the amount of tests executed in this run. func (s *QuantitativeRunStats) incrementRun() { s.count_++ diff --git a/internal/quantitative/stats_test.go b/internal/quantitative/stats_test.go new file mode 100644 index 0000000..b615ebb --- /dev/null +++ b/internal/quantitative/stats_test.go @@ -0,0 +1,131 @@ +// Copyright 2024 OWASP CRS Project +// SPDX-License-Identifier: Apache-2.0 + +package quantitative + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "github.com/coreruleset/go-ftw/output" +) + +type statsTestSuite struct { + suite.Suite +} + +func TestStatsTestSuite(t *testing.T) { + suite.Run(t, new(statsTestSuite)) +} + +func (s *statsTestSuite) TestNewQuantitativeStats() { + tests := []struct { + name string + want *QuantitativeRunStats + }{ + { + name: "Test 1", + want: &QuantitativeRunStats{ + count_: 0, + falsePositives: 0, + falsePositivesPerRule: make(map[int]int), + totalTime: 0, + }, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + got := NewQuantitativeStats() + s.Require().Equal(got, tt.want) + }) + } +} + +func (s *statsTestSuite) TestQuantitativeRunStats_MarshalJSON() { + type fields struct { + count_ int + totalTime time.Duration + falsePositives int + falsePositivesPerRule map[int]int + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Test 1", + fields: fields{ + count_: 1, + totalTime: 1, + falsePositives: 1, + falsePositivesPerRule: map[int]int{920010: 1}, + }, + want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), + wantErr: false, + }, + { + name: "Test 2", + fields: fields{ + count_: 2, + totalTime: 2, + falsePositives: 2, + falsePositivesPerRule: map[int]int{933100: 2}, + }, + want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), + wantErr: false, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + q := &QuantitativeRunStats{ + count_: tt.fields.count_, + totalTime: tt.fields.totalTime, + falsePositives: tt.fields.falsePositives, + falsePositivesPerRule: tt.fields.falsePositivesPerRule, + } + got, err := q.MarshalJSON() + s.Require().NoError(err) + s.Require().Equal(got, tt.want) + }) + } +} + +func (s *statsTestSuite) TestQuantitativeRunStats_functions() { + q := NewQuantitativeStats() + + q.incrementRun() + s.Require().Equal(q.Count(), 1) + + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) + + q.incrementRun() + s.Require().Equal(q.Count(), 2) + + q.addFalsePositive(920200) + s.Require().Equal(q.FalsePositives(), 2) + + duration := time.Duration(5000) + q.SetTotalTime(duration) + s.Require().Equal(q.TotalTime(), duration) +} + +func (s *statsTestSuite) TestQuantitativeRunStats_printSummary() { + var b bytes.Buffer + out := output.NewOutput("plain", &b) + q := NewQuantitativeStats() + + q.incrementRun() + s.Require().Equal(q.Count(), 1) + + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) + + q.printSummary(out) + s.Require().Equal(b.String(), "Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule: map[920100:1]\n") +} From 9e30aa094a3938f99032c3db045c6516cfb4216f Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 11:26:48 -0300 Subject: [PATCH 05/14] fix: apply code review suggestions Signed-off-by: Felipe Zipitria --- cmd/quantitative_test.go | 6 ++--- internal/quantitative/local_engine.go | 28 +++++++++++++++------- internal/quantitative/local_engine_test.go | 17 +++++++++---- internal/quantitative/runner.go | 7 +++--- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/cmd/quantitative_test.go b/cmd/quantitative_test.go index 84a0058..6cdf7dd 100644 --- a/cmd/quantitative_test.go +++ b/cmd/quantitative_test.go @@ -33,12 +33,12 @@ func (s *quantitativeCmdTestSuite) SetupTest() { err := os.MkdirAll(path.Join(s.tempDir, "rules"), fs.ModePerm) s.Require().NoError(err) - fakeCRSSetupConf, err := os.Create(path.Join(s.tempDir, "crs-setup.conf.example")) + fakeCrsSetupConf, err := os.Create(path.Join(s.tempDir, "crs-setup.conf.example")) s.Require().NoError(err) - n, err := fakeCRSSetupConf.WriteString(crsSetupFileContents) + n, err := fakeCrsSetupConf.WriteString(crsSetupFileContents) s.Require().NoError(err) s.Equal(len(crsSetupFileContents), n) - err = fakeCRSSetupConf.Close() + err = fakeCrsSetupConf.Close() s.Require().NoError(err) fakeRulesFile, err := os.Create(path.Join(s.tempDir, "rules", "Rules1.conf")) s.Require().NoError(err) diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index 51724ce..d1f60ee 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -1,4 +1,4 @@ -// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// Copyright 2024 OWASP CRS Project // SPDX-License-Identifier: Apache-2.0 package quantitative @@ -57,25 +57,37 @@ SecAction \ ` ) -type LocalEngine struct { +// LocalEngine is the interface for the local engine +type LocalEngine interface { + // Create creates a new engine to test payloads + Create(prefix string, paranoia int) LocalEngine + // CrsCall benchmarks the CRS WAF using a POST request with the payload + CrsCall(payload string) (int, map[int]string) +} + +// localEngine is the engine to test payloads +type localEngine struct { waf coraza.WAF } -// NewEngine creates a new engine to test payloads -func NewEngine(prefix string, paranoia int) *LocalEngine { - eng := &LocalEngine{ +// Create creates a new engine to test payloads +func (e *localEngine) Create(prefix string, paranoia int) LocalEngine { + eng := localEngine{ waf: crsWAF(prefix, paranoia), } - return eng + return &eng } // CrsCall benchmarks the CRS WAF with a GET request // payload: the string to be passed in the request body // returns the status of the HTTP response and a map of the matched rules with their IDs and the data that matched. -func (e *LocalEngine) CRSCall(payload string) (int, map[int]string) { +func (e *localEngine) CrsCall(payload string) (int, map[int]string) { var status = http.StatusOK var matchedRules = make(map[int]string) + if e.waf == nil { + log.Fatal().Msg("local engine not initialized") + } tx := e.waf.NewTransaction() tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080) tx.ProcessURI("/post", "POST", "HTTP/1.1") @@ -106,7 +118,7 @@ func (e *LocalEngine) CRSCall(payload string) (int, map[int]string) { return status, matchedRules } -// newCrsWaf creates a WAF with the CRS rules +// crsWAF creates a WAF with the CRS rules // prefix: the path to the CRS rules // paranoiaLevel: 1 - 4 should be added as a template to the crs-setup.conf file // If you want to run your own WAF rules instead of CRS, create a similar function to newCrsWaf diff --git a/internal/quantitative/local_engine_test.go b/internal/quantitative/local_engine_test.go index a347db9..0aaf8bc 100644 --- a/internal/quantitative/local_engine_test.go +++ b/internal/quantitative/local_engine_test.go @@ -15,14 +15,18 @@ import ( ) const ( - crsUrl = "https://github.com/coreruleset/coreruleset/releases/download/v4.6.0/coreruleset-4.6.0-minimal.tar.gz" crsTestVersion = "4.6.0" ) +var crsUrl = fmt.Sprintf( + "https://github.com/coreruleset/coreruleset/releases/download/v%s/coreruleset-%s-minimal.tar.gz", + crsTestVersion, + crsTestVersion) + type localEngineTestSuite struct { suite.Suite dir string - engine *LocalEngine + engine LocalEngine } func TestLocalEngineTestSuite(t *testing.T) { @@ -40,7 +44,8 @@ func (s *localEngineTestSuite) SetupTest() { err := client.Get() s.Require().NoError(err) - s.engine = NewEngine(path.Join(s.dir, fmt.Sprintf("coreruleset-%s", crsTestVersion)), 1) + s.engine = &localEngine{} + s.engine = s.engine.Create(path.Join(s.dir, fmt.Sprintf("coreruleset-%s", crsTestVersion)), 1) s.Require().NotNil(s.engine) } @@ -51,13 +56,15 @@ func (s *localEngineTestSuite) TeardownTest() { // TestCRSCall For this test you will need to have the Core Rule Set repository cloned in the parent directory as the project. func (s *localEngineTestSuite) TestCrsCall() { + s.Require().NotNil(s.engine) + // simple payload, no matches - status, matchedRules := s.engine.CRSCall("this is a test") + status, matchedRules := s.engine.CrsCall("this is a test") s.Require().Equal(http.StatusOK, status) s.Require().Empty(matchedRules) // this payload will match a few rules - status, matchedRules = s.engine.CRSCall("' OR 1 = 1") + status, matchedRules = s.engine.CrsCall("' OR 1 = 1") s.Require().Equal(http.StatusForbidden, status) s.Require().NotEmpty(matchedRules) diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index adc97eb..1d725d4 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -81,7 +81,8 @@ func RunQuantitativeTests(params Params, out *output.Output) error { // create the results stats := NewQuantitativeStats() - runner := NewEngine(params.Directory, params.ParanoiaLevel) + var engine LocalEngine = &localEngine{} + runner := engine.Create(params.Directory, params.ParanoiaLevel) // Are we using the corpus at all? if params.Payload != "" { @@ -129,8 +130,8 @@ func wantSpecificRuleResults(specific int, rule int) bool { } // doEngineCall -func doEngineCall(engine *LocalEngine, payload string, specificRule int, stats *QuantitativeRunStats) { - status, matchedRules := engine.CRSCall(payload) +func doEngineCall(engine LocalEngine, payload string, specificRule int, stats *QuantitativeRunStats) { + status, matchedRules := engine.CrsCall(payload) log.Trace().Msgf("Status: %d", status) log.Trace().Msgf("Rules: %v", matchedRules) if status == http.StatusForbidden { From fa547f87e5246ccd384fbcba11b5946a3350f98d Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 11:56:00 -0300 Subject: [PATCH 06/14] docs: add basic documentation Signed-off-by: Felipe Zipitria --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/README.md b/README.md index 5f97051..629900b 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,94 @@ Now you can do that by passing the `--wait-for-host` flag. The value of this opt - `--wait-for-no-redirect` Do not follow HTTP 3xx redirects. - `--wait-for-timeout` Sets the timeout for all wait operations, 0 is unlimited. (default 10s) +## (EXPERIMENTAL) Quantitative testing + +In the latest version of `go-ftw`, we have added a new feature that allows you to run quantitative tests. +This feature is still experimental and may change in the future. + +### What is the idea behind quantitative tests? + +Quantitative tests allow you to run tests using payloads to quantify the amount of false positives you might get when running in production. +We use a well-known corpora of text to generate payloads that are sent to the WAF. The WAF should not block these payloads, as they are not malicious. + +Anyone can create their own corpora of text and use it to test their WAF. The corpora of text is a list of strings that are sent to the WAF to check if it blocks them. + +The result of this test is a percentage of false positives. The lower the percentage, the better the WAF is at not blocking benign payloads. + +### What is a corpus? Why do I need one? + +A corpus is a collection of text that is used to generate payloads. +The text can be anything, from news articles to books. The idea is to have a large collection of text that can be used to generate payloads. + +The default corpus is the [Leipzig Corpora Collection](https://wortschatz.uni-leipzig.de/en/download/), which is a collection of text from the web. + +### How to create a corpus? + +You can create your own corpus by collecting text from the web or using text from books, articles, etc. +Or even use it with your own website! What you will need to do is to implement the interface `corpus.Corpus`, the `corpus.File`, +and for iterating over the corpus, the `corpus.Iterator` and `corpus.Payload` interfaces. + +You can see an example of how to implement the `corpus.Corpus` interface in the `corpus/leipzig` package. + +### How to run quantitative tests? + +To run quantitative tests, you just need to pass the `quantitative` flag to `ftw`. + +```bash +❯ ./go-ftw quantitative -h +Run all quantitative tests + +Usage: + ftw quantitative [flags] + +Flags: + -c, --corpus string Corpus to use for the quantitative tests (default "leipzig") + -L, --corpus-lang string Corpus language to use for the quantitative tests (default "eng") + -n, --corpus-line int Number is the payload line from the corpus to exclusively send + -s, --corpus-size string Corpus size to use for the quantitative tests. Most corpora will have sizes like "100K", "1M", etc. (default "100K") + -S, --corpus-source string Corpus source to use for the quantitative tests. Most corpus will have a source like "news", "web", "wikipedia", etc. (default "news") + -y, --corpus-year string Corpus year to use for the quantitative tests. Most corpus will have a year like "2023", "2022", etc. (default "2023") + -d, --directory string Directory where the CRS rules are stored (default ".") + -f, --file string Output file path for quantitative tests. Prints to standard output by default. + -h, --help help for quantitative + -l, --lines int Number of lines of input to process before stopping + -o, --output string Output type for quantitative tests. "normal" is the default. (default "normal") + -P, --paranoia-level int Paranoia level used to run the quantitative tests (default 1) + -p, --payload string Payload is a string you want to test using quantitative tests. Will not use the corpus. + -r, --rule int Rule ID of interest: only show false positives for specified rule ID + +Global Flags: + --cloud cloud mode: rely only on HTTP status codes for determining test success or failure (will not process any logs) + --config string specify config file (default is $PWD/.ftw.yaml) + --debug debug output + --overrides string specify file with platform specific overrides + --trace trace output: really, really verbose +``` + +### Example of running quantitative tests + +This will run with the default leipzig corpus and size of 10K payloads. +```bash +./go-ftw quantitative -d ../coreruleset -s 10K +``` + +This will run with the default leipzig corpus and size of 10K payloads, but only for the rule 920350. +```bash +./go-ftw quantitative -d ../coreruleset -s 10K -r 920350 +``` + +If you add `--debug` to the command, you will see the payloads that cause false positives. +```bash +❯ ./go-ftw quantitative -d ../coreruleset -s 10K --debug +Running quantitative tests +11:38AM DBG Preparing download of corpus file from https://downloads.wortschatz-leipzig.de/corpora/eng_news_2023_10K.tar.gz +11:38AM DBG filename eng_news_2023_10K-sentences.txt already exists +11:38AM DBG Using paranoia level: 1 + +11:38AM DBG False positive with string: And finally: "I'd also say temp nurses make a lot. +11:38AM DBG rule 932290 does not match the specific rule we wanted 0 +``` + ## Library usage `go-ftw` can be used as a library also. Just include it in your project: From 27a7aad36b781c5450f49b4caebf7a5f72c47885 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 11:56:23 -0300 Subject: [PATCH 07/14] chore: cleanup comment Signed-off-by: Felipe Zipitria --- internal/quantitative/stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go index 7fa63a3..edd93aa 100644 --- a/internal/quantitative/stats.go +++ b/internal/quantitative/stats.go @@ -46,9 +46,9 @@ func (s *QuantitativeRunStats) printSummary(out *output.Output) { out.Println("Run %d payloads in %s", s.count_, s.totalTime) out.Println("Total False positive ratio: %d/%d = %.4f", s.falsePositives, s.count_, ratio) out.Println("False positives per rule: %+v", s.falsePositivesPerRule) - // echo "| Freq. | ID # | Paranoia Level |" - // echo "| ------ | ------ | -------------- |" } + } else { + out.Println("No false positives detected with the passed corpus") } } From e8707f9d870eca9a0d491734bc5ceaf6baba476c Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 12:39:17 -0300 Subject: [PATCH 08/14] fix: counting FPs Signed-off-by: Felipe Zipitria --- README.md | 35 +++++++++++++++++++++++++-------- internal/quantitative/runner.go | 4 ++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 629900b..eff3b79 100644 --- a/README.md +++ b/README.md @@ -490,26 +490,45 @@ Global Flags: This will run with the default leipzig corpus and size of 10K payloads. ```bash -./go-ftw quantitative -d ../coreruleset -s 10K +❯ ./go-ftw quantitative -d ../coreruleset -s 10K +Running quantitative tests +Run 10000 payloads in 16.009683458s +Total False positive ratio: 47/10000 = 0.0047 +False positives per rule: map[932235:4 932270:2 932290:35 932380:2 933160:1 942100:1 942230:1 942360:1] ``` This will run with the default leipzig corpus and size of 10K payloads, but only for the rule 920350. ```bash -./go-ftw quantitative -d ../coreruleset -s 10K -r 920350 +❯ ./go-ftw quantitative -d ../coreruleset -s 10K -r 932270 +Running quantitative tests +Run 10000 payloads in 15.782435916s +Total False positive ratio: 2/10000 = 0.0002 +False positives per rule: map[932270:2] ``` If you add `--debug` to the command, you will see the payloads that cause false positives. ```bash ❯ ./go-ftw quantitative -d ../coreruleset -s 10K --debug Running quantitative tests -11:38AM DBG Preparing download of corpus file from https://downloads.wortschatz-leipzig.de/corpora/eng_news_2023_10K.tar.gz -11:38AM DBG filename eng_news_2023_10K-sentences.txt already exists -11:38AM DBG Using paranoia level: 1 - -11:38AM DBG False positive with string: And finally: "I'd also say temp nurses make a lot. -11:38AM DBG rule 932290 does not match the specific rule we wanted 0 +12:32PM DBG Preparing download of corpus file from https://downloads.wortschatz-leipzig.de/corpora/eng_news_2023_10K.tar.gz +12:32PM DBG filename eng_news_2023_10K-sentences.txt already exists +12:32PM DBG Using paranoia level: 1 + +12:32PM DBG False positive with string: And finally: "I'd also say temp nurses make a lot. +12:32PM DBG **> rule 932290 => Matched Data: "I'd found within ARGS:payload: And finally: "I'd also say temp nurses make a lot. +12:32PM DBG False positive with string: But it was an experience Seguin said she "wouldn't trade for anything." +12:32PM DBG **> rule 932290 => Matched Data: "wouldn't found within ARGS:payload: But it was an experience Seguin said she "wouldn't trade for anything." +12:32PM DBG False positive with string: Consolidated Edison () last issued its earnings results on Thursday, November 3rd. +12:32PM DBG **> rule 932235 => Matched Data: () last found within ARGS:payload: Consolidated Edison () last issued its earnings results on Thursday, November 3rd. ``` +### Future work for quantitative tests + +This feature will enable us to compare between two different versions of CRS (or any two rules) and see, for example, +if any modification to the rule has caused more false positives. + +Integrating it to the CI/CD pipeline will allow us to check every PR for false positives before merging. + ## Library usage `go-ftw` can be used as a library also. Just include it in your project: diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 1d725d4..4aa60e3 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -140,12 +140,12 @@ func doEngineCall(engine LocalEngine, payload string, specificRule int, stats *Q log.Trace().Msgf("=> rules matched: %+v", matchedRules) for rule, data := range matchedRules { // check if we only want to show false positives for a specific rule - if wantSpecificRuleResults(rule, specificRule) { + if wantSpecificRuleResults(specificRule, rule) { log.Debug().Msgf("rule %d does not match the specific rule we wanted %d", rule, specificRule) continue } stats.addFalsePositive(rule) - log.Debug().Msgf("==> rule %d matched with data: %s", rule, data) + log.Debug().Msgf("**> rule %d => %s", rule, data) } } } From 300ba7d4e9d416cf08a925d9b0125ab393e103d2 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 15:30:41 -0300 Subject: [PATCH 09/14] fix: use both phases for getting FPs Signed-off-by: Felipe Zipitria --- internal/quantitative/local_engine.go | 28 ++-- internal/quantitative/stats.go | 15 +- internal/quantitative/stats_test.go | 192 +++++++++++++------------- 3 files changed, 124 insertions(+), 111 deletions(-) diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index d1f60ee..e3563e4 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -88,28 +88,27 @@ func (e *localEngine) CrsCall(payload string) (int, map[int]string) { if e.waf == nil { log.Fatal().Msg("local engine not initialized") } + // we use the payload in the URI to rules in phase:1 can catch it + uri := fmt.Sprintf("/get?payload=%s", url.QueryEscape(payload)) + tx := e.waf.NewTransaction() tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080) - tx.ProcessURI("/post", "POST", "HTTP/1.1") + tx.ProcessURI(uri, "GET", "HTTP/1.1") tx.AddRequestHeader("Host", "localhost") tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75. 0.3770.100 Safari/537.36") - tx.AddRequestHeader("Accept", "application/json") - tx.AddRequestHeader("Content-Type", "application/x-www-form-urlencoded") - body := []byte("payload=" + url.QueryEscape(payload)) - - tx.AddRequestHeader("Content-Length", strconv.Itoa(len(body))) - tx.ProcessRequestHeaders() - if _, _, err := tx.WriteRequestBody(body); err != nil { - log.Error().Err(err).Msg("failed to write request body") - } - it, err := tx.ProcessRequestBody() - if err != nil { - log.Error().Err(err).Msg("failed to process request body") + tx.AddRequestHeader("Accept", "*/*") + + // we need to check also for phase:1 rules only + it := tx.ProcessRequestHeaders() + if it == nil { // if no interruption, we check phase:2 + it, _ = tx.ProcessRequestBody() } - if it != nil { // execution was interrupted + + if it != nil { status = obtainStatusCodeFromInterruptionOrDefault(it, http.StatusOK) matchedRules = getMatchedRules(tx) } + // We don't care about the response body for now, nor logging. if err := tx.Close(); err != nil { log.Error().Err(err).Msg("failed to close transaction") @@ -161,6 +160,7 @@ func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { } func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int { + log.Debug().Msgf("Interruption: %s", it.Action) if it.Action == "deny" { statusCode := it.Status if statusCode == 0 { diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go index edd93aa..637988a 100644 --- a/internal/quantitative/stats.go +++ b/internal/quantitative/stats.go @@ -5,6 +5,7 @@ package quantitative import ( "encoding/json" + "sort" "time" "github.com/rs/zerolog/log" @@ -45,7 +46,19 @@ func (s *QuantitativeRunStats) printSummary(out *output.Output) { ratio := float64(s.falsePositives) / float64(s.count_) out.Println("Run %d payloads in %s", s.count_, s.totalTime) out.Println("Total False positive ratio: %d/%d = %.4f", s.falsePositives, s.count_, ratio) - out.Println("False positives per rule: %+v", s.falsePositivesPerRule) + out.Println("False positives per rule id:") + // Extract and sort the keys + rules := make([]int, 0, len(s.falsePositivesPerRule)) + for rule := range s.falsePositivesPerRule { + rules = append(rules, rule) + } + sort.Ints(rules) + + // Print the sorted map + for _, rule := range rules { + count := s.falsePositivesPerRule[rule] + out.Println(" %d: %d false positives", rule, count) + } } } else { out.Println("No false positives detected with the passed corpus") diff --git a/internal/quantitative/stats_test.go b/internal/quantitative/stats_test.go index b615ebb..8ff9f45 100644 --- a/internal/quantitative/stats_test.go +++ b/internal/quantitative/stats_test.go @@ -4,128 +4,128 @@ package quantitative import ( - "bytes" - "testing" - "time" + "bytes" + "testing" + "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/suite" - "github.com/coreruleset/go-ftw/output" + "github.com/coreruleset/go-ftw/output" ) type statsTestSuite struct { - suite.Suite + suite.Suite } func TestStatsTestSuite(t *testing.T) { - suite.Run(t, new(statsTestSuite)) + suite.Run(t, new(statsTestSuite)) } func (s *statsTestSuite) TestNewQuantitativeStats() { - tests := []struct { - name string - want *QuantitativeRunStats - }{ - { - name: "Test 1", - want: &QuantitativeRunStats{ - count_: 0, - falsePositives: 0, - falsePositivesPerRule: make(map[int]int), - totalTime: 0, - }, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - got := NewQuantitativeStats() - s.Require().Equal(got, tt.want) - }) - } + tests := []struct { + name string + want *QuantitativeRunStats + }{ + { + name: "Test 1", + want: &QuantitativeRunStats{ + count_: 0, + falsePositives: 0, + falsePositivesPerRule: make(map[int]int), + totalTime: 0, + }, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + got := NewQuantitativeStats() + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_MarshalJSON() { - type fields struct { - count_ int - totalTime time.Duration - falsePositives int - falsePositivesPerRule map[int]int - } - tests := []struct { - name string - fields fields - want []byte - wantErr bool - }{ - { - name: "Test 1", - fields: fields{ - count_: 1, - totalTime: 1, - falsePositives: 1, - falsePositivesPerRule: map[int]int{920010: 1}, - }, - want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), - wantErr: false, - }, - { - name: "Test 2", - fields: fields{ - count_: 2, - totalTime: 2, - falsePositives: 2, - falsePositivesPerRule: map[int]int{933100: 2}, - }, - want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), - wantErr: false, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - q := &QuantitativeRunStats{ - count_: tt.fields.count_, - totalTime: tt.fields.totalTime, - falsePositives: tt.fields.falsePositives, - falsePositivesPerRule: tt.fields.falsePositivesPerRule, - } - got, err := q.MarshalJSON() - s.Require().NoError(err) - s.Require().Equal(got, tt.want) - }) - } + type fields struct { + count_ int + totalTime time.Duration + falsePositives int + falsePositivesPerRule map[int]int + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Test 1", + fields: fields{ + count_: 1, + totalTime: 1, + falsePositives: 1, + falsePositivesPerRule: map[int]int{920010: 1}, + }, + want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), + wantErr: false, + }, + { + name: "Test 2", + fields: fields{ + count_: 2, + totalTime: 2, + falsePositives: 2, + falsePositivesPerRule: map[int]int{933100: 2}, + }, + want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), + wantErr: false, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + q := &QuantitativeRunStats{ + count_: tt.fields.count_, + totalTime: tt.fields.totalTime, + falsePositives: tt.fields.falsePositives, + falsePositivesPerRule: tt.fields.falsePositivesPerRule, + } + got, err := q.MarshalJSON() + s.Require().NoError(err) + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_functions() { - q := NewQuantitativeStats() + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.incrementRun() - s.Require().Equal(q.Count(), 2) + q.incrementRun() + s.Require().Equal(q.Count(), 2) - q.addFalsePositive(920200) - s.Require().Equal(q.FalsePositives(), 2) + q.addFalsePositive(920200) + s.Require().Equal(q.FalsePositives(), 2) - duration := time.Duration(5000) - q.SetTotalTime(duration) - s.Require().Equal(q.TotalTime(), duration) + duration := time.Duration(5000) + q.SetTotalTime(duration) + s.Require().Equal(q.TotalTime(), duration) } func (s *statsTestSuite) TestQuantitativeRunStats_printSummary() { - var b bytes.Buffer - out := output.NewOutput("plain", &b) - q := NewQuantitativeStats() + var b bytes.Buffer + out := output.NewOutput("plain", &b) + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.printSummary(out) - s.Require().Equal(b.String(), "Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule: map[920100:1]\n") + q.printSummary(out) + s.Require().Equal("Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule id:\n 920100: 1 false positives\n", b.String()) } From 81f0e99e57973ad5548c31764e07b6f393bf5e34 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 16:28:22 -0300 Subject: [PATCH 10/14] docs: add more examples Signed-off-by: Felipe Zipitria --- README.md | 50 +++++++- internal/quantitative/stats_test.go | 192 ++++++++++++++-------------- 2 files changed, 141 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index eff3b79..450d65b 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,12 @@ You can see an example of how to implement the `corpus.Corpus` interface in the To run quantitative tests, you just need to pass the `quantitative` flag to `ftw`. +The corpus will be downloaded and cached locally for future use. You can also specify the size of the corpus, +the language, the source, and the year of the corpus. The bare minimum parameter that you must specify is the +directory where the CRS rules are stored. + +Here is the help for the `quantitative` command: + ```bash ❯ ./go-ftw quantitative -h Run all quantitative tests @@ -486,24 +492,36 @@ Global Flags: --trace trace output: really, really verbose ``` + + ### Example of running quantitative tests This will run with the default leipzig corpus and size of 10K payloads. ```bash ❯ ./go-ftw quantitative -d ../coreruleset -s 10K Running quantitative tests -Run 10000 payloads in 16.009683458s -Total False positive ratio: 47/10000 = 0.0047 -False positives per rule: map[932235:4 932270:2 932290:35 932380:2 933160:1 942100:1 942230:1 942360:1] +Run 10000 payloads in 18.482979709s +Total False positive ratio: 408/10000 = 0.0408 +False positives per rule: + Rule 920220: 198 false positives + Rule 920221: 198 false positives + Rule 932235: 4 false positives + Rule 932270: 2 false positives + Rule 932380: 2 false positives + Rule 933160: 1 false positives + Rule 942100: 1 false positives + Rule 942230: 1 false positives + Rule 942360: 1 false positives ``` This will run with the default leipzig corpus and size of 10K payloads, but only for the rule 920350. ```bash ❯ ./go-ftw quantitative -d ../coreruleset -s 10K -r 932270 Running quantitative tests -Run 10000 payloads in 15.782435916s +Run 10000 payloads in 15.218343083s Total False positive ratio: 2/10000 = 0.0002 -False positives per rule: map[932270:2] +False positives per rule: + Rule 932270: 2 false positives ``` If you add `--debug` to the command, you will see the payloads that cause false positives. @@ -522,6 +540,28 @@ Running quantitative tests 12:32PM DBG **> rule 932235 => Matched Data: () last found within ARGS:payload: Consolidated Edison () last issued its earnings results on Thursday, November 3rd. ``` +The default language for the corpus is english, but you can change it to german using the `-L` flag. +```bash +❯ ./go-ftw quantitative -d ../coreruleset -s 10K -L deu +Running quantitative tests +4:18PM INF Downloading corpus file from https://downloads.wortschatz-leipzig.de/corpora/deu_news_2023_10K.tar.gz +Moved /Users/fzipitria/.ftw/extracted/deu_news_2023_10K/deu_news_2023_10K-sentences.txt to /Users/fzipitria/.ftw/deu_news_2023_10K-sentences.txt +Run 10000 payloads in 25.169846084s +Total False positive ratio: 44/10000 = 0.0044 +False positives per rule: + Rule 920220: 19 false positives + Rule 920221: 19 false positives + Rule 932125: 1 false positives + Rule 932290: 5 false positives +``` + +Results can be shown in json format also, to be processed by other tools. +```bash +❯ ./go-ftw quantitative -d ../coreruleset -s 10K -o json + +{"count":10000,"falsePositives":408,"falsePositivesPerRule":{"920220":198,"920221":198,"932235":4,"932270":2,"932380":2,"933160":1,"942100":1,"942230":1,"942360":1},"totalTime":15031086083}% +``` + ### Future work for quantitative tests This feature will enable us to compare between two different versions of CRS (or any two rules) and see, for example, diff --git a/internal/quantitative/stats_test.go b/internal/quantitative/stats_test.go index 8ff9f45..470a3ee 100644 --- a/internal/quantitative/stats_test.go +++ b/internal/quantitative/stats_test.go @@ -4,128 +4,128 @@ package quantitative import ( - "bytes" - "testing" - "time" + "bytes" + "testing" + "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/suite" - "github.com/coreruleset/go-ftw/output" + "github.com/coreruleset/go-ftw/output" ) type statsTestSuite struct { - suite.Suite + suite.Suite } func TestStatsTestSuite(t *testing.T) { - suite.Run(t, new(statsTestSuite)) + suite.Run(t, new(statsTestSuite)) } func (s *statsTestSuite) TestNewQuantitativeStats() { - tests := []struct { - name string - want *QuantitativeRunStats - }{ - { - name: "Test 1", - want: &QuantitativeRunStats{ - count_: 0, - falsePositives: 0, - falsePositivesPerRule: make(map[int]int), - totalTime: 0, - }, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - got := NewQuantitativeStats() - s.Require().Equal(got, tt.want) - }) - } + tests := []struct { + name string + want *QuantitativeRunStats + }{ + { + name: "Test 1", + want: &QuantitativeRunStats{ + count_: 0, + falsePositives: 0, + falsePositivesPerRule: make(map[int]int), + totalTime: 0, + }, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + got := NewQuantitativeStats() + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_MarshalJSON() { - type fields struct { - count_ int - totalTime time.Duration - falsePositives int - falsePositivesPerRule map[int]int - } - tests := []struct { - name string - fields fields - want []byte - wantErr bool - }{ - { - name: "Test 1", - fields: fields{ - count_: 1, - totalTime: 1, - falsePositives: 1, - falsePositivesPerRule: map[int]int{920010: 1}, - }, - want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), - wantErr: false, - }, - { - name: "Test 2", - fields: fields{ - count_: 2, - totalTime: 2, - falsePositives: 2, - falsePositivesPerRule: map[int]int{933100: 2}, - }, - want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), - wantErr: false, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - q := &QuantitativeRunStats{ - count_: tt.fields.count_, - totalTime: tt.fields.totalTime, - falsePositives: tt.fields.falsePositives, - falsePositivesPerRule: tt.fields.falsePositivesPerRule, - } - got, err := q.MarshalJSON() - s.Require().NoError(err) - s.Require().Equal(got, tt.want) - }) - } + type fields struct { + count_ int + totalTime time.Duration + falsePositives int + falsePositivesPerRule map[int]int + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Test 1", + fields: fields{ + count_: 1, + totalTime: 1, + falsePositives: 1, + falsePositivesPerRule: map[int]int{920010: 1}, + }, + want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), + wantErr: false, + }, + { + name: "Test 2", + fields: fields{ + count_: 2, + totalTime: 2, + falsePositives: 2, + falsePositivesPerRule: map[int]int{933100: 2}, + }, + want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), + wantErr: false, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + q := &QuantitativeRunStats{ + count_: tt.fields.count_, + totalTime: tt.fields.totalTime, + falsePositives: tt.fields.falsePositives, + falsePositivesPerRule: tt.fields.falsePositivesPerRule, + } + got, err := q.MarshalJSON() + s.Require().NoError(err) + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_functions() { - q := NewQuantitativeStats() + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.incrementRun() - s.Require().Equal(q.Count(), 2) + q.incrementRun() + s.Require().Equal(q.Count(), 2) - q.addFalsePositive(920200) - s.Require().Equal(q.FalsePositives(), 2) + q.addFalsePositive(920200) + s.Require().Equal(q.FalsePositives(), 2) - duration := time.Duration(5000) - q.SetTotalTime(duration) - s.Require().Equal(q.TotalTime(), duration) + duration := time.Duration(5000) + q.SetTotalTime(duration) + s.Require().Equal(q.TotalTime(), duration) } func (s *statsTestSuite) TestQuantitativeRunStats_printSummary() { - var b bytes.Buffer - out := output.NewOutput("plain", &b) - q := NewQuantitativeStats() + var b bytes.Buffer + out := output.NewOutput("plain", &b) + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.printSummary(out) - s.Require().Equal("Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule id:\n 920100: 1 false positives\n", b.String()) + q.printSummary(out) + s.Require().Equal("Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule id:\n 920100: 1 false positives\n", b.String()) } From 503bb77946e9a89766c23536371c70406c65a5f2 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 20:56:53 -0300 Subject: [PATCH 11/14] fix: reduce noise in output Signed-off-by: Felipe Zipitria --- internal/quantitative/local_engine.go | 1 - internal/quantitative/runner.go | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index e3563e4..3b8bfd9 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -160,7 +160,6 @@ func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { } func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int { - log.Debug().Msgf("Interruption: %s", it.Action) if it.Action == "deny" { statusCode := it.Status if statusCode == 0 { diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 4aa60e3..1d4c6bb 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -55,7 +55,7 @@ func NewCorpus(corpusType corpus.Type) corpus.Corpus { // RunQuantitativeTests runs all quantitative tests func RunQuantitativeTests(params Params, out *output.Output) error { - out.Println("Running quantitative tests") + out.Println(":hourglass: Running quantitative tests") log.Trace().Msgf("Lines: %d", params.Lines) log.Trace().Msgf("Fast: %d", params.Fast) @@ -70,7 +70,7 @@ func RunQuantitativeTests(params Params, out *output.Output) error { startTime := time.Now() // create a new corpusRunner - corpusRunner := NewCorpus(corpus.Leipzig). + corpusRunner := NewCorpus(params.Corpus). WithSize(params.CorpusSize). WithYear(params.CorpusYear). WithSource(params.CorpusSource). @@ -136,12 +136,11 @@ func doEngineCall(engine LocalEngine, payload string, specificRule int, stats *Q log.Trace().Msgf("Rules: %v", matchedRules) if status == http.StatusForbidden { // append the line to the false positives - log.Debug().Msgf("False positive with string: %s", payload) + log.Trace().Msgf("False positive with string: %s", payload) log.Trace().Msgf("=> rules matched: %+v", matchedRules) for rule, data := range matchedRules { // check if we only want to show false positives for a specific rule if wantSpecificRuleResults(specificRule, rule) { - log.Debug().Msgf("rule %d does not match the specific rule we wanted %d", rule, specificRule) continue } stats.addFalsePositive(rule) From b3232d5f7216f725470d849e98a773f2b898a7c9 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 22:02:10 -0300 Subject: [PATCH 12/14] feat: add factories for creating new objects Signed-off-by: Felipe Zipitria --- experimental/corpus/types.go | 5 + internal/quantitative/factories.go | 27 ++++ internal/quantitative/leipzig/payload.go | 15 +- internal/quantitative/leipzig/payload_test.go | 129 ++---------------- internal/quantitative/local_engine.go | 4 +- internal/quantitative/runner.go | 44 +++--- internal/quantitative/runner_test.go | 9 +- 7 files changed, 90 insertions(+), 143 deletions(-) create mode 100644 internal/quantitative/factories.go diff --git a/experimental/corpus/types.go b/experimental/corpus/types.go index bc3e194..b63fc78 100644 --- a/experimental/corpus/types.go +++ b/experimental/corpus/types.go @@ -25,6 +25,7 @@ type Type string const ( Leipzig Type = "leipzig" + NoType Type = "none" ) func (t *Type) String() string { @@ -112,6 +113,10 @@ type Iterator interface { type Payload interface { // LineNumber returns the payload given a line from the Corpus Iterator LineNumber() int + // SetLineNumber sets the line number of the payload + SetLineNumber(line int) // Content returns the payload given a line from the Corpus Iterator Content() string + // SetContent sets the content of the payload + SetContent(content string) } diff --git a/internal/quantitative/factories.go b/internal/quantitative/factories.go new file mode 100644 index 0000000..17b546b --- /dev/null +++ b/internal/quantitative/factories.go @@ -0,0 +1,27 @@ +package quantitative + +import ( + "fmt" + "github.com/coreruleset/go-ftw/experimental/corpus" + "github.com/coreruleset/go-ftw/internal/quantitative/leipzig" +) + +// CorpusFactory creates a new corpus +func CorpusFactory(t corpus.Type) (corpus.Corpus, error) { + switch t { + case corpus.Leipzig: + return leipzig.NewLeipzigCorpus(), nil + default: + return nil, fmt.Errorf("unsupported corpus type: %s", t) + } +} + +// PayloadFactory creates a new Payload based on the corpus.Type +func PayloadFactory(t corpus.Type) (corpus.Payload, error) { + switch t { + case corpus.Leipzig: + return &leipzig.Payload{}, nil + default: + return nil, fmt.Errorf("unsupported corpus type: %s", t) + } +} diff --git a/internal/quantitative/leipzig/payload.go b/internal/quantitative/leipzig/payload.go index 9d60e2f..62a71d1 100644 --- a/internal/quantitative/leipzig/payload.go +++ b/internal/quantitative/leipzig/payload.go @@ -4,6 +4,7 @@ package leipzig import ( + "github.com/coreruleset/go-ftw/experimental/corpus" "strconv" "strings" ) @@ -14,8 +15,8 @@ type Payload struct { payload string } -// NewPayload returns a new Payload -func NewPayload(line string) *Payload { +// NewPayload returns a new Payload from a line in the corpus. +func NewPayload(line string) corpus.Payload { split := strings.Split(line, "\t") // convert to int num, err := strconv.Atoi(split[0]) @@ -35,7 +36,17 @@ func (p *Payload) LineNumber() int { return p.line } +// SetLineNumber sets the line number of the payload +func (p *Payload) SetLineNumber(line int) { + p.line = line +} + // Content returns the payload given a line from the Corpus Iterator func (p *Payload) Content() string { return p.payload } + +// SetContent sets the content of the payload +func (p *Payload) SetContent(content string) { + p.payload = content +} diff --git a/internal/quantitative/leipzig/payload_test.go b/internal/quantitative/leipzig/payload_test.go index 52df43f..b4dc292 100644 --- a/internal/quantitative/leipzig/payload_test.go +++ b/internal/quantitative/leipzig/payload_test.go @@ -4,7 +4,6 @@ package leipzig import ( - "reflect" "testing" "github.com/stretchr/testify/suite" @@ -19,120 +18,20 @@ func TestPayloadTestSuite(t *testing.T) { } func (s *payloadTestSuite) TestNewPayload() { - type args struct { - line string - } - tests := []struct { - name string - args args - want *Payload - }{ - { - name: "TestNewPayload", - args: args{ - line: "1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", - }, - want: &Payload{ - line: 1, - payload: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", - }, - }, - { - name: "TestAdditional", - args: args{ - line: "2000\tThis is an additional payload", - }, - want: &Payload{ - line: 2000, - payload: "This is an additional payload", - }, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - if got := NewPayload(tt.args.line); !reflect.DeepEqual(got, tt.want) { - s.Require().Equal(got, tt.want) - } - }) - } + line := "1\t$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna." + p := NewPayload(line) + s.Require().Equal(1, p.LineNumber()) + s.Require().Equal("$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", p.Content()) + line2 := "2000\tThis is an additional payload" + p2 := NewPayload(line2) + s.Require().Equal(2000, p2.LineNumber()) + s.Require().Equal("This is an additional payload", p2.Content()) } -func (s *payloadTestSuite) TestPayload_Content() { - type fields struct { - line int - payload string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "TestContent", - fields: fields{ - line: 1, - payload: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", - }, - want: "$156,834 for The Pathway to Excellence in Practice program through Neighborhood Place of Puna.", - }, - { - name: "TestContent2", - fields: fields{ - line: 2000, - payload: "This is another test payload", - }, - want: "This is another test payload", - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - p := &Payload{ - line: tt.fields.line, - payload: tt.fields.payload, - } - if got := p.Content(); got != tt.want { - s.Require().Equal(got, tt.want) - } - }) - } -} - -func (s *payloadTestSuite) TestPayload_LineNumber() { - type fields struct { - line int - payload string - } - tests := []struct { - name string - fields fields - want int - }{ - { - name: "TestLineNumber", - fields: fields{ - line: 1, - payload: "This is a test payload", - }, - want: 1, - }, - { - name: "TestLineNumber2", - fields: fields{ - line: 2000, - payload: "This is another test payload", - }, - want: 2000, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - p := &Payload{ - line: tt.fields.line, - payload: tt.fields.payload, - } - if got := p.LineNumber(); got != tt.want { - s.Require().Equal(got, tt.want) - } - }) - } +func (s *payloadTestSuite) TestPayloadSetters() { + p := &Payload{} + p.SetLineNumber(1) + s.Require().Equal(1, p.LineNumber()) + p.SetContent("test") + s.Require().Equal("test", p.Content()) } diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index 3b8bfd9..03849db 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -135,7 +135,7 @@ func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { vars := map[string]interface{}{ "ParanoiaLevel": paranoiaLevel, } - log.Debug().Msgf("Using paranoia level: %d\n", paranoiaLevel) + log.Debug().Msgf("Using paranoia level: %d", paranoiaLevel) // set up configuration from template configTmpl, err := template.New("crs-config").Parse(testingConfigTmpl) if err != nil { @@ -163,7 +163,7 @@ func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultSt if it.Action == "deny" { statusCode := it.Status if statusCode == 0 { - statusCode = 403 + statusCode = http.StatusForbidden } return statusCode diff --git a/internal/quantitative/runner.go b/internal/quantitative/runner.go index 1d4c6bb..58c9d65 100644 --- a/internal/quantitative/runner.go +++ b/internal/quantitative/runner.go @@ -10,7 +10,6 @@ import ( "github.com/rs/zerolog/log" "github.com/coreruleset/go-ftw/experimental/corpus" - "github.com/coreruleset/go-ftw/internal/quantitative/leipzig" "github.com/coreruleset/go-ftw/output" ) @@ -42,19 +41,9 @@ type Params struct { CorpusSource string } -// NewCorpus creates a new corpus -func NewCorpus(corpusType corpus.Type) corpus.Corpus { - switch corpusType { - case corpus.Leipzig: - return leipzig.NewLeipzigCorpus() - default: - log.Fatal().Msgf("Unknown corpus implementation: %s", corpusType) - return nil - } -} - // RunQuantitativeTests runs all quantitative tests func RunQuantitativeTests(params Params, out *output.Output) error { + var lc corpus.File out.Println(":hourglass: Running quantitative tests") log.Trace().Msgf("Lines: %d", params.Lines) @@ -70,14 +59,20 @@ func RunQuantitativeTests(params Params, out *output.Output) error { startTime := time.Now() // create a new corpusRunner - corpusRunner := NewCorpus(params.Corpus). + corpusRunner, err := CorpusFactory(params.Corpus) + if err != nil { + return err + } + corpusRunner = corpusRunner. WithSize(params.CorpusSize). WithYear(params.CorpusYear). WithSource(params.CorpusSource). WithLanguage(params.CorpusLang) - // download the corpusRunner file - lc := corpusRunner.FetchCorpusFile() + // download the corpusRunner file if no payload is provided + if params.Payload == "" { + lc = corpusRunner.FetchCorpusFile() + } // create the results stats := NewQuantitativeStats() @@ -85,17 +80,22 @@ func RunQuantitativeTests(params Params, out *output.Output) error { runner := engine.Create(params.Directory, params.ParanoiaLevel) // Are we using the corpus at all? + // TODO: this could be moved to a generic "file" iterator (instead of "corpus), with a Factory method if params.Payload != "" { log.Trace().Msgf("Payload received from cmdline: %s", params.Payload) + p, err := PayloadFactory(params.Corpus) + if err != nil { + return err + } // CrsCall with payload - doEngineCall(runner, params.Payload, params.Rule, stats) + doEngineCall(runner, p, params.Rule, stats) } else { // iterate over the corpus log.Trace().Msgf("Iterating over corpus") for iter := corpusRunner.GetIterator(lc); iter.HasNext(); { - p := iter.Next() + payload := iter.Next() stats.incrementRun() - payload := p.Content() - log.Trace().Msgf("Line: %s", payload) + content := payload.Content() + log.Trace().Msgf("Line: %s", content) // check if we are looking for a specific payload line # if needSpecificPayload(params.Number, stats.Count()) { continue @@ -130,8 +130,8 @@ func wantSpecificRuleResults(specific int, rule int) bool { } // doEngineCall -func doEngineCall(engine LocalEngine, payload string, specificRule int, stats *QuantitativeRunStats) { - status, matchedRules := engine.CrsCall(payload) +func doEngineCall(engine LocalEngine, payload corpus.Payload, specificRule int, stats *QuantitativeRunStats) { + status, matchedRules := engine.CrsCall(payload.Content()) log.Trace().Msgf("Status: %d", status) log.Trace().Msgf("Rules: %v", matchedRules) if status == http.StatusForbidden { @@ -144,7 +144,7 @@ func doEngineCall(engine LocalEngine, payload string, specificRule int, stats *Q continue } stats.addFalsePositive(rule) - log.Debug().Msgf("**> rule %d => %s", rule, data) + log.Debug().Msgf("**> rule %d with payload %d => %s", rule, payload.LineNumber(), data) } } } diff --git a/internal/quantitative/runner_test.go b/internal/quantitative/runner_test.go index 275f7cb..0478bf5 100644 --- a/internal/quantitative/runner_test.go +++ b/internal/quantitative/runner_test.go @@ -60,10 +60,15 @@ func (s *runnerTestSuite) TeardownTest() { s.Require().NoError(err) } -func (s *runnerTestSuite) TestNewCorpus() { - s.c = NewCorpus(corpus.Leipzig) +func (s *runnerTestSuite) TestCorpusFactory() { + var err error + s.c, err = CorpusFactory(corpus.Leipzig) + s.Require().NoError(err) s.Require().NotNil(s.c) s.Require().Equal(s.c.URL(), "https://downloads.wortschatz-leipzig.de/corpora") + + s.c, err = CorpusFactory(corpus.NoType) + s.Require().Error(err) } func (s *runnerTestSuite) TestRunQuantitativeTests() { From 5b94d9af604f38608d4336a976bcc1037224ab0e Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 22:13:25 -0300 Subject: [PATCH 13/14] test: simplify file tests Signed-off-by: Felipe Zipitria --- internal/quantitative/leipzig/file_test.go | 182 ++------------------- 1 file changed, 12 insertions(+), 170 deletions(-) diff --git a/internal/quantitative/leipzig/file_test.go b/internal/quantitative/leipzig/file_test.go index 1fcb878..212bcdd 100644 --- a/internal/quantitative/leipzig/file_test.go +++ b/internal/quantitative/leipzig/file_test.go @@ -4,183 +4,25 @@ package leipzig import ( - "reflect" + "github.com/stretchr/testify/suite" "testing" "github.com/coreruleset/go-ftw/experimental/corpus" ) -func TestFile_CacheDir(t *testing.T) { - type fields struct { - cacheDir string - filePath string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "Test 1", - fields: fields{ - cacheDir: "cacheDir", - filePath: "filePath", - }, - want: "cacheDir", - }, - { - name: "Test 2", - fields: fields{ - cacheDir: "cacheDir2", - filePath: "filePath2", - }, - want: "cacheDir2", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := File{ - cacheDir: tt.fields.cacheDir, - filePath: tt.fields.filePath, - } - if got := f.CacheDir(); got != tt.want { - t.Errorf("CacheDir() = %v, want %v", got, tt.want) - } - }) - } +type fileTestSuite struct { + suite.Suite + cache corpus.File } -func TestFile_FilePath(t *testing.T) { - type fields struct { - cacheDir string - filePath string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "Test 1", - fields: fields{ - cacheDir: "cacheDir", - filePath: "filePath", - }, - want: "filePath", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := File{ - cacheDir: tt.fields.cacheDir, - filePath: tt.fields.filePath, - } - if got := f.FilePath(); got != tt.want { - t.Errorf("FilePath() = %v, want %v", got, tt.want) - } - }) - } +func TestFileSuite(t *testing.T) { + suite.Run(t, new(fileTestSuite)) } -func TestFile_WithCacheDir(t *testing.T) { - type fields struct { - cacheDir string - filePath string - } - type args struct { - cacheDir string - } - tests := []struct { - name string - fields fields - args args - want corpus.File - }{ - { - name: "Test 1", - fields: fields{ - cacheDir: "cacheDir1", - filePath: "filePath", - }, - args: args{ - cacheDir: "cacheDir10", - }, - want: File{ - cacheDir: "cacheDir10", - filePath: "filePath", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := File{ - cacheDir: tt.fields.cacheDir, - filePath: tt.fields.filePath, - } - if got := f.WithCacheDir(tt.args.cacheDir); !reflect.DeepEqual(got, tt.want) { - t.Errorf("WithCacheDir() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestFile_WithFilePath(t *testing.T) { - type fields struct { - cacheDir string - filePath string - } - type args struct { - filePath string - } - tests := []struct { - name string - fields fields - args args - want corpus.File - }{ - { - name: "Test 1", - fields: fields{ - cacheDir: "cacheDir", - filePath: "filePath1", - }, - args: args{ - filePath: "filePath2", - }, - want: File{ - cacheDir: "cacheDir", - filePath: "filePath2", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := File{ - cacheDir: tt.fields.cacheDir, - filePath: tt.fields.filePath, - } - if got := f.WithFilePath(tt.args.filePath); !reflect.DeepEqual(got, tt.want) { - t.Errorf("WithFilePath() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNewFile(t *testing.T) { - tests := []struct { - name string - want corpus.File - }{ - { - name: "Test 1", - want: File{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewFile(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewFile() = %v, want %v", got, tt.want) - } - }) - } +func (s *fileTestSuite) TestFile_CacheDir() { + f := NewFile() + f = f.WithCacheDir("cacheDir") + s.Require().Equal("cacheDir", f.CacheDir()) + f = f.WithFilePath("filePath") + s.Require().Equal("filePath", f.FilePath()) } From f3e4ed97844df2a8fa4a8310d2dee36f05f7e744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Zipitr=C3=ADa?= <3012076+fzipi@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:53:51 -0300 Subject: [PATCH 14/14] fix: apply suggestions from code review Co-authored-by: Max Leske <250711+theseion@users.noreply.github.com> --- README.md | 27 +++++++++++++++------------ internal/quantitative/local_engine.go | 4 ++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 450d65b..49e0689 100644 --- a/README.md +++ b/README.md @@ -429,25 +429,28 @@ This feature is still experimental and may change in the future. ### What is the idea behind quantitative tests? -Quantitative tests allow you to run tests using payloads to quantify the amount of false positives you might get when running in production. -We use a well-known corpora of text to generate payloads that are sent to the WAF. The WAF should not block these payloads, as they are not malicious. +Quantitative testing mode provides a means to to quantify the amount of false positives to be expected in production for a given rule. +We use well-known corpora of texts to generate plausible, non-malicious payloads. Whenever such a payload is blocked by the WAF, the detection is considered to be a false positive. -Anyone can create their own corpora of text and use it to test their WAF. The corpora of text is a list of strings that are sent to the WAF to check if it blocks them. +Anyone can create their own corpora of texts and use them to test their WAF. Each corpus essentially consists of a list of strings, which may be sent to the WAF, depending on the configuration of the run. -The result of this test is a percentage of false positives. The lower the percentage, the better the WAF is at not blocking benign payloads. +The result of a test run is a percentage of false positives. The lower the percentage, the better the WAF is at not blocking benign payloads for a given rule. However, since we use generic corpora in our tests, the strings in those corpora will not necessarily be representative of the domain of a specific site. This means that a rule with a low false positive rate can still produce many false positives in specific contexts, e.g., when a website contains programming language code. ### What is a corpus? Why do I need one? -A corpus is a collection of text that is used to generate payloads. -The text can be anything, from news articles to books. The idea is to have a large collection of text that can be used to generate payloads. +A corpus is a collection of texts that is used to generate payloads. +The texts can contain anything, from news articles to books. The idea is to have a large collection of texts that can be used to generate payloads. Well-known corpora usually have a domain or context, e.g., news headlines, or English books of the 18th century. -The default corpus is the [Leipzig Corpora Collection](https://wortschatz.uni-leipzig.de/en/download/), which is a collection of text from the web. +The default corpus is the [Leipzig Corpora Collection](https://wortschatz.uni-leipzig.de/en/download/), which is a collection of texts from the web. ### How to create a corpus? -You can create your own corpus by collecting text from the web or using text from books, articles, etc. -Or even use it with your own website! What you will need to do is to implement the interface `corpus.Corpus`, the `corpus.File`, -and for iterating over the corpus, the `corpus.Iterator` and `corpus.Payload` interfaces. +You can create your own corpus by collecting texts from the web, or from books, articles, etc. +You could even use the contents of your own website as a corpus! What you will need to do is to implement the following interfaces: +- `corpus.Corpus` +- `corpus.File` +- `corpus.Iterator` +- `corpus.Payload` You can see an example of how to implement the `corpus.Corpus` interface in the `corpus/leipzig` package. @@ -540,7 +543,7 @@ Running quantitative tests 12:32PM DBG **> rule 932235 => Matched Data: () last found within ARGS:payload: Consolidated Edison () last issued its earnings results on Thursday, November 3rd. ``` -The default language for the corpus is english, but you can change it to german using the `-L` flag. +The default language for the corpus is English, but you can change it to German using the `-L` flag. ```bash ❯ ./go-ftw quantitative -d ../coreruleset -s 10K -L deu Running quantitative tests @@ -555,7 +558,7 @@ False positives per rule: Rule 932290: 5 false positives ``` -Results can be shown in json format also, to be processed by other tools. +Results can be shown in JSON format also, to be processed by other tools. ```bash ❯ ./go-ftw quantitative -d ../coreruleset -s 10K -o json diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index 03849db..3440f4d 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -61,7 +61,7 @@ SecAction \ type LocalEngine interface { // Create creates a new engine to test payloads Create(prefix string, paranoia int) LocalEngine - // CrsCall benchmarks the CRS WAF using a POST request with the payload + // CrsCall benchmarks the CRS WAF using a GET request with the payload CrsCall(payload string) (int, map[int]string) } @@ -88,7 +88,7 @@ func (e *localEngine) CrsCall(payload string) (int, map[int]string) { if e.waf == nil { log.Fatal().Msg("local engine not initialized") } - // we use the payload in the URI to rules in phase:1 can catch it + // we use the payload in the URI so rules in phase 1 can catch it uri := fmt.Sprintf("/get?payload=%s", url.QueryEscape(payload)) tx := e.waf.NewTransaction()