diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ccc7c4a7..af206bb8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,7 +31,6 @@ List here all the items you have verified BEFORE sending this PR. Please DO NOT - [ ] My code follows the style guidelines of this project - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have added an entry in `CHANGELOG.md` ## Author's Checklist diff --git a/.github/workflows/opentelemetry.yml b/.github/workflows/opentelemetry.yml index 76ce0531..16d0101e 100644 --- a/.github/workflows/opentelemetry.yml +++ b/.github/workflows/opentelemetry.yml @@ -1,12 +1,16 @@ --- +# Look up results at https://ela.st/oblt-ci-cd-stats +# There will be one service per GitHub repository, including the org name, and one Transaction per Workflow. name: OpenTelemetry Export Trace on: workflow_run: - workflows: - - golangci-lint + workflows: [ "*" ] types: [completed] +permissions: + contents: read + jobs: otel-export-trace: runs-on: ubuntu-latest diff --git a/.go-version b/.go-version index c262b1f0..ae7bbdf0 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.21.6 +1.21.10 diff --git a/.golangci.yml b/.golangci.yml index 67ba3b88..82e3bbfb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,7 +83,7 @@ linters-settings: gosimple: # Select the Go version to target. The default is '1.13'. - go: "1.21.6" + go: "1.21.10" nakedret: # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 @@ -117,19 +117,19 @@ linters-settings: staticcheck: # Select the Go version to target. The default is '1.13'. - go: "1.21.6" + go: "1.21.10" # https://staticcheck.io/docs/options#checks checks: ["all"] stylecheck: # Select the Go version to target. The default is '1.13'. - go: "1.21.6" + go: "1.21.10" # https://staticcheck.io/docs/options#checks checks: ["all"] unused: # Select the Go version to target. The default is '1.13'. - go: "1.21.6" + go: "1.21.10" gosec: excludes: diff --git a/NOTICE.txt b/NOTICE.txt index 875ef9aa..042f6eb6 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -664,6 +664,36 @@ Contents of probable licence file $GOMODCACHE/github.com/elastic/go-ucfg@v0.8.5/ limitations under the License. +-------------------------------------------------------------------------------- +Dependency : github.com/elastic/pkcs8 +Version: v1.0.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/elastic/pkcs8@v1.0.0/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2014 youmark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + -------------------------------------------------------------------------------- Dependency : github.com/fatih/color Version: v1.13.0 @@ -1251,36 +1281,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -Dependency : github.com/youmark/pkcs8 -Version: v0.0.0-20201027041543-1326539a0a0a -Licence type (autodetected): MIT --------------------------------------------------------------------------------- - -Contents of probable licence file $GOMODCACHE/github.com/youmark/pkcs8@v0.0.0-20201027041543-1326539a0a0a/LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 youmark - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -------------------------------------------------------------------------------- Dependency : go.elastic.co/apm/module/apmhttp/v2 Version: v2.0.0 @@ -1917,11 +1917,11 @@ Contents of probable licence file $GOMODCACHE/go.elastic.co/go-licence-detector@ -------------------------------------------------------------------------------- Dependency : go.uber.org/zap -Version: v1.21.0 +Version: v1.27.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/zap@v1.21.0/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/go.uber.org/zap@v1.27.0/LICENSE: Copyright (c) 2016-2017 Uber Technologies, Inc. @@ -1946,11 +1946,11 @@ THE SOFTWARE. -------------------------------------------------------------------------------- Dependency : golang.org/x/crypto -Version: v0.17.0 +Version: v0.22.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/crypto@v0.17.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/crypto@v0.22.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -1983,11 +1983,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : golang.org/x/net -Version: v0.17.0 +Version: v0.23.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.17.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/net@v0.23.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -2020,11 +2020,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : golang.org/x/sys -Version: v0.15.0 +Version: v0.19.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.15.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/sys@v0.19.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -2641,11 +2641,11 @@ Contents of probable licence file $GOMODCACHE/github.com/elastic/go-licenser@v0. -------------------------------------------------------------------------------- Dependency : github.com/elastic/go-sysinfo -Version: v1.7.1 +Version: v1.14.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/go-sysinfo@v1.7.1/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/github.com/elastic/go-sysinfo@v1.14.0/LICENSE.txt: Apache License @@ -3096,11 +3096,11 @@ SOFTWARE. -------------------------------------------------------------------------------- Dependency : github.com/google/go-cmp -Version: v0.5.4 +Version: v0.6.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/google/go-cmp@v0.5.4/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/google/go-cmp@v0.6.0/LICENSE: Copyright (c) 2017 The Go Authors. All rights reserved. @@ -3933,11 +3933,11 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- Dependency : github.com/kr/text -Version: v0.1.0 +Version: v0.2.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/kr/text@v0.1.0/License: +Contents of probable licence file $GOMODCACHE/github.com/kr/text@v0.2.0/License: Copyright 2012 Keith Rarick @@ -4111,11 +4111,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : github.com/prometheus/procfs -Version: v0.7.3 +Version: v0.13.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/prometheus/procfs@v0.7.3/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/prometheus/procfs@v0.13.0/LICENSE: Apache License Version 2.0, January 2004 @@ -4803,11 +4803,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI -------------------------------------------------------------------------------- Dependency : go.uber.org/atomic -Version: v1.9.0 +Version: v1.7.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/atomic@v1.9.0/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/go.uber.org/atomic@v1.7.0/LICENSE.txt: Copyright (c) 2016 Uber Technologies, Inc. @@ -4832,11 +4832,11 @@ THE SOFTWARE. -------------------------------------------------------------------------------- Dependency : go.uber.org/goleak -Version: v1.1.12 +Version: v1.3.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/goleak@v1.1.12/LICENSE: +Contents of probable licence file $GOMODCACHE/go.uber.org/goleak@v1.3.0/LICENSE: The MIT License (MIT) @@ -4863,11 +4863,11 @@ THE SOFTWARE. -------------------------------------------------------------------------------- Dependency : go.uber.org/multierr -Version: v1.8.0 +Version: v1.11.0 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.uber.org/multierr@v1.8.0/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/go.uber.org/multierr@v1.11.0/LICENSE.txt: Copyright (c) 2017-2021 Uber Technologies, Inc. @@ -4966,11 +4966,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : golang.org/x/sync -Version: v0.1.0 +Version: v0.6.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.1.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/sync@v0.6.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5003,11 +5003,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : golang.org/x/term -Version: v0.15.0 +Version: v0.19.0 Licence type (autodetected): BSD-3-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/golang.org/x/term@v0.15.0/LICENSE: +Contents of probable licence file $GOMODCACHE/golang.org/x/term@v0.19.0/LICENSE: Copyright (c) 2009 The Go Authors. All rights reserved. @@ -5475,11 +5475,11 @@ limitations under the License. -------------------------------------------------------------------------------- Dependency : howett.net/plist -Version: v1.0.0 +Version: v1.0.1 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/howett.net/plist@v1.0.0/LICENSE: +Contents of probable licence file $GOMODCACHE/howett.net/plist@v1.0.1/LICENSE: Copyright (c) 2013, Dustin L. Howett. All rights reserved. diff --git a/api/server.go b/api/server.go index c7acdda3..703b2469 100644 --- a/api/server.go +++ b/api/server.go @@ -56,6 +56,16 @@ func New(log *logp.Logger, mux *http.ServeMux, c *config.C) (*Server, error) { if err != nil { return nil, err } + return new(log, mux, cfg) +} + +// NewFromConfig creates a new API server from the given Config object. +func NewFromConfig(log *logp.Logger, mux *http.ServeMux, cfg Config) (*Server, error) { + return new(log, mux, cfg) +} + +// new creates the server from a config struct +func new(log *logp.Logger, mux *http.ServeMux, cfg Config) (*Server, error) { srv := &http.Server{ReadHeaderTimeout: cfg.Timeout} l, err := makeListener(cfg) if err != nil { @@ -91,6 +101,12 @@ func (s *Server) Shutdown(ctx context.Context) error { return s.srv.Shutdown(ctx) } +// Addr returns the network address of the server +// This is useful for tests, where we usually pass the port as `0` to get allocated a random free port +func (s *Server) Addr() net.Addr { + return s.l.Addr() +} + // AttachHandler will attach a handler at the specified route and return an error instead of panicing. func (s *Server) AttachHandler(route string, h http.Handler) (err error) { defer func() { diff --git a/go.mod b/go.mod index a9808540..54c46dc1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/docker/go-units v0.5.0 github.com/elastic/go-structform v0.0.9 github.com/elastic/go-ucfg v0.8.5 + github.com/elastic/pkcs8 v1.0.0 github.com/fatih/color v1.13.0 github.com/gofrs/uuid v4.4.0+incompatible github.com/magefile/mage v1.13.0 @@ -15,14 +16,13 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.2 - github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a go.elastic.co/apm/module/apmhttp/v2 v2.0.0 go.elastic.co/ecszap v1.0.1 go.elastic.co/go-licence-detector v0.5.0 - go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.17.0 - golang.org/x/net v0.17.0 - golang.org/x/sys v0.15.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.22.0 + golang.org/x/net v0.23.0 + golang.org/x/sys v0.19.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -31,31 +31,28 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/elastic/go-licenser v0.4.0 // indirect - github.com/elastic/go-sysinfo v1.7.1 // indirect + github.com/elastic/go-sysinfo v1.14.0 // indirect github.com/elastic/go-windows v1.0.1 // indirect github.com/gobuffalo/here v0.6.0 // indirect github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jcchavezs/porto v0.1.0 // indirect - github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/karrick/godirwalk v1.15.6 // indirect github.com/markbates/pkger v0.17.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/procfs v0.13.0 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.elastic.co/apm/v2 v2.0.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/goleak v1.1.12 // indirect - go.uber.org/multierr v1.8.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.6.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - howett.net/plist v1.0.0 // indirect + howett.net/plist v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index 089e8192..12d79124 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VM github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= @@ -17,13 +16,16 @@ github.com/elastic/go-licenser v0.4.0 h1:jLq6A5SilDS/Iz1ABRkO6BHy91B9jBora8FwGRs github.com/elastic/go-licenser v0.4.0/go.mod h1:V56wHMpmdURfibNBggaSBfqgPxyT1Tldns1i87iTEvU= github.com/elastic/go-structform v0.0.9 h1:HpcS7xljL4kSyUfDJ8cXTJC6rU5ChL1wYb6cx3HLD+o= github.com/elastic/go-structform v0.0.9/go.mod h1:CZWf9aIRYY5SuKSmOhtXScE5uQiLZNqAFnwKR4OrIM4= -github.com/elastic/go-sysinfo v1.7.1 h1:Wx4DSARcKLllpKT2TnFVdSUJOsybqMYCNQZq1/wO+s0= github.com/elastic/go-sysinfo v1.7.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= +github.com/elastic/go-sysinfo v1.14.0 h1:dQRtiqLycoOOla7IflZg3aN213vqJmP0lpVpKQ9lUEY= +github.com/elastic/go-sysinfo v1.14.0/go.mod h1:FKUXnZWhnYI0ueO7jhsGV3uQJ5hiz8OqM5b3oGyaRr8= github.com/elastic/go-ucfg v0.8.5 h1:4GB/rMpuh7qTcSFaxJUk97a/JyvFzhi6t+kaskTTLdM= github.com/elastic/go-ucfg v0.8.5/go.mod h1:4E8mPOLSUV9hQ7sgLEJ4bvt0KhMuDJa8joDT2QGAEKA= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= +github.com/elastic/pkcs8 v1.0.0 h1:HhitlUKxhN288kcNcYkjW6/ouvuwJWd9ioxpjnD9jVA= +github.com/elastic/pkcs8 v1.0.0/go.mod h1:ipsZToJfq1MxclVTwpG7U/bgeDtf+0HkUiOxebk95+0= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= @@ -31,8 +33,9 @@ github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PL github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/licenseclassifier v0.0.0-20200402202327-879cb1424de0 h1:OggOMmdI0JLwg1FkOKH9S7fVHF0oEm8PX6S8kAdpOps= github.com/google/licenseclassifier v0.0.0-20200402202327-879cb1424de0/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -40,14 +43,14 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jcchavezs/porto v0.1.0 h1:Xmxxn25zQMmgE7/yHYmh19KcItG81hIwfbEEFnd6w/Q= github.com/jcchavezs/porto v0.1.0/go.mod h1:fESH0gzDHiutHRdX2hv27ojnOVFco37hg1W6E9EZF4A= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA= github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.13.0 h1:XtLJl8bcCM7EFoO8FyH8XK3t7G5hQAeK+i4tq+veT9M= github.com/magefile/mage v1.13.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= @@ -70,8 +73,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -97,12 +101,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= -github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.elastic.co/apm/module/apmhttp/v2 v2.0.0 h1:GNfmK1LD4nE5fYqbLxROCpg1ucyjSFG5iwulxwAJ+3o= go.elastic.co/apm/module/apmhttp/v2 v2.0.0/go.mod h1:5KmxcNN7hkJh8sVW3Ggl/pYgnwiNenygE46bZoUb9RE= go.elastic.co/apm/v2 v2.0.0 h1:5BeBh+oIrVbMwPrW3uO9Uxm4w7HpKy92lYl5Rfj69Kg= @@ -114,21 +117,21 @@ go.elastic.co/fastjson v1.1.0/go.mod h1:boNGISWMjQsUPy/t6yqt2/1Wx4YNPSe+mZjlyw9v go.elastic.co/go-licence-detector v0.5.0 h1:YXPCyt9faKMdJ8uMrkcI4patk8WZ0ME5oaIhYBUsRU4= go.elastic.co/go-licence-detector v0.5.0/go.mod h1:fSJQU8au4SAgDK+UQFbgUPsXKYNBDv4E/dwWevrMpXU= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -136,25 +139,33 @@ 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.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20190620200207-3b0461eec859/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -167,19 +178,32 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210615035016-665e8c7367d1/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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -190,6 +214,7 @@ golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -215,5 +240,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/logp/config.go b/logp/config.go index 1e82b6e7..8d890658 100644 --- a/logp/config.go +++ b/logp/config.go @@ -88,6 +88,30 @@ func DefaultConfig(environment Environment) Config { } } +// DefaultEventConfig returns the default config options for the event logger in +// a given environment the Beat is supposed to be run within. +func DefaultEventConfig(environment Environment) Config { + return Config{ + Level: defaultLevel, + ToFiles: true, + ToStderr: false, + Files: FileConfig{ + MaxSize: 5 * 1024 * 1024, // 5Mb + MaxBackups: 2, + Permissions: 0600, + Interval: 0, + RotateOnStartup: false, + RedirectStderr: false, + Name: "event-data", + }, + Metrics: MetricsConfig{ + Enabled: false, + }, + environment: environment, + addCaller: true, + } +} + // LogFilename returns the base filename to which logs will be written for // the "files" log output. If another log output is used, or `logging.files.name` // is unspecified, then the beat name will be returned. diff --git a/logp/configure/logging.go b/logp/configure/logging.go index f23e1792..b4a3d480 100644 --- a/logp/configure/logging.go +++ b/logp/configure/logging.go @@ -45,6 +45,10 @@ func init() { flag.Var((*environmentVar)(&environment), "environment", "set environment being ran in") } +func GetEnvironment() logp.Environment { + return environment +} + // Logging builds a logp.Config based on the given common.Config and the specified // CLI flags. func Logging(beatName string, cfg *config.C) error { @@ -75,6 +79,39 @@ func LoggingWithOutputs(beatName string, cfg *config.C, outputs ...zapcore.Core) return logp.ConfigureWithOutputs(config, outputs...) } +// LoggingWithTypedOutputs applies some defaults then calls ConfigureWithTypedOutputs +func LoggingWithTypedOutputs(beatName string, cfg, typedCfg *config.C, logKey, kind string, outputs ...zapcore.Core) error { + config := logp.DefaultConfig(environment) + config.Beat = beatName + if cfg != nil { + if err := cfg.Unpack(&config); err != nil { + return err + } + } + + applyFlags(&config) + + typedLogpConfig := logp.DefaultEventConfig(environment) + defaultName := typedLogpConfig.Files.Name + typedLogpConfig.Beat = beatName + if typedCfg != nil { + if err := typedCfg.Unpack(&typedLogpConfig); err != nil { + return fmt.Errorf("cannot unpack typed output config: %w", err) + } + } + + // Make sure we're always running on the same log level + typedLogpConfig.Level = config.Level + typedLogpConfig.Selectors = config.Selectors + + // If the name has not been configured, make it {beatName}-events-data + if typedLogpConfig.Files.Name == defaultName { + typedLogpConfig.Files.Name = beatName + "-events-data" + } + + return logp.ConfigureWithTypedOutput(config, typedLogpConfig, logKey, kind, outputs...) +} + func applyFlags(cfg *logp.Config) { if toStderr { cfg.ToStderr = true diff --git a/logp/core.go b/logp/core.go index 321a20d4..5b42d846 100644 --- a/logp/core.go +++ b/logp/core.go @@ -21,7 +21,7 @@ import ( "errors" "flag" "fmt" - "io/ioutil" + "io" golog "log" "os" "path/filepath" @@ -68,10 +68,7 @@ func Configure(cfg Config) error { return ConfigureWithOutputs(cfg) } -// ConfigureWithOutputs XXX: is used by elastic-agent only (See file: x-pack/elastic-agent/pkg/core/logger/logger.go). -// The agent requires that the output specified in the config object is configured and merged with the -// logging outputs given. -func ConfigureWithOutputs(cfg Config, outputs ...zapcore.Core) error { +func createSink(defaultLoggerCfg Config, outputs ...zapcore.Core) (zapcore.Core, zap.AtomicLevel, *observer.ObservedLogs, map[string]struct{}, error) { var ( sink zapcore.Core observedLogs *observer.ObservedLogs @@ -79,25 +76,25 @@ func ConfigureWithOutputs(cfg Config, outputs ...zapcore.Core) error { level zap.AtomicLevel ) - level = zap.NewAtomicLevelAt(cfg.Level.ZapLevel()) + level = zap.NewAtomicLevelAt(defaultLoggerCfg.Level.ZapLevel()) // Build a single output (stderr has priority if more than one are enabled). - if cfg.toObserver { + if defaultLoggerCfg.toObserver { sink, observedLogs = observer.New(level) } else { - sink, err = createLogOutput(cfg, level) + sink, err = createLogOutput(defaultLoggerCfg, level) } if err != nil { - return fmt.Errorf("failed to build log output: %w", err) + return nil, level, nil, nil, fmt.Errorf("failed to build log output: %w", err) } // Default logger is always discard, debug level below will // possibly re-enable it. - golog.SetOutput(ioutil.Discard) + golog.SetOutput(io.Discard) // Enabled selectors when debug is enabled. - selectors := make(map[string]struct{}, len(cfg.Selectors)) - if cfg.Level.Enabled(DebugLevel) && len(cfg.Selectors) > 0 { - for _, sel := range cfg.Selectors { + selectors := make(map[string]struct{}, len(defaultLoggerCfg.Selectors)) + if defaultLoggerCfg.Level.Enabled(DebugLevel) && len(defaultLoggerCfg.Selectors) > 0 { + for _, sel := range defaultLoggerCfg.Selectors { selectors[strings.TrimSpace(sel)] = struct{}{} } @@ -118,7 +115,73 @@ func ConfigureWithOutputs(cfg Config, outputs ...zapcore.Core) error { } sink = newMultiCore(append(outputs, sink)...) - root := zap.New(sink, makeOptions(cfg)...) + + return sink, level, observedLogs, selectors, err +} + +// ConfigureWithOutputs configures the global logger to use an output created +// from `defaultLoggerCfg` and all the outputs passed by `outputs`. +// This function needs to be exported because it's used by `logp/configure` +func ConfigureWithOutputs(defaultLoggerCfg Config, outputs ...zapcore.Core) error { + sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...) + if err != nil { + return err + } + root := zap.New(sink, makeOptions(defaultLoggerCfg)...) + storeLogger(&coreLogger{ + selectors: selectors, + rootLogger: root, + globalLogger: root.WithOptions(zap.AddCallerSkip(1)), + logger: newLogger(root, ""), + level: level, + observedLogs: observedLogs, + }) + return nil +} + +// ConfigureWithTypedOutput configures the global logger to use typed outputs. +// +// If a log entry matches the defined key/value, this entry is logged using the +// core generated from `typedLoggerCfg`, otherwise it will be logged by all +// cores in `outputs` and the one generated from `defaultLoggerCfg`. +// Arguments: +// - `defaultLoggerCfg` is used to create a new core that will be the default +// output from the logger +// - `typedLoggerCfg` is used to create a new output that will only be used +// when the log entry matches `entry[logKey] = kind` +// - `key` is the key the typed logger will look at +// - `value` is the value compared against the `logKey` entry +// - `outputs` is a list of cores that will be added together with the core +// generated by `defaultLoggerCfg` as the default output for the loggger. +// +// If `defaultLoggerCfg.toObserver` is true, then `typedLoggerCfg` is ignored +// and a single sink is used so all logs can be observed. +func ConfigureWithTypedOutput(defaultLoggerCfg, typedLoggerCfg Config, key, value string, outputs ...zapcore.Core) error { + sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...) + if err != nil { + return err + } + + var typedCore zapcore.Core + if defaultLoggerCfg.toObserver { + typedCore = sink + } else { + typedCore, err = createLogOutput(typedLoggerCfg, level) + } + if err != nil { + return fmt.Errorf("could not create typed logger output: %w", err) + } + + sink = &typedLoggerCore{ + defaultCore: sink, + typedCore: typedCore, + key: key, + value: value, + } + + sink = selectiveWrapper(sink, selectors) + + root := zap.New(sink, makeOptions(defaultLoggerCfg)...) storeLogger(&coreLogger{ selectors: selectors, rootLogger: root, @@ -148,9 +211,9 @@ func createLogOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, error case SystemdEnvironment, ContainerEnvironment: return makeStderrOutput(cfg, enab) case MacOSServiceEnvironment, WindowsServiceEnvironment: - fallthrough - default: return makeFileOutput(cfg, enab) + default: + return zapcore.NewNopCore(), nil } } @@ -215,7 +278,7 @@ func makeStderrOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, erro } func makeDiscardOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, error) { - discard := zapcore.AddSync(ioutil.Discard) + discard := zapcore.AddSync(io.Discard) return newCore(buildEncoder(cfg), discard, enab), nil } diff --git a/logp/core_mock_test.go b/logp/core_mock_test.go new file mode 100644 index 00000000..bbfba810 --- /dev/null +++ b/logp/core_mock_test.go @@ -0,0 +1,270 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package logp + +import ( + "sync" + + "go.uber.org/zap/zapcore" +) + +// ZapCoreMock is a mock implementation of zapcore.Core. +// +// func TestSomethingThatUsesCore(t *testing.T) { +// +// // make and configure a mocked zapcore.Core +// mockedCore := &ZapCoreMock{ +// CheckFunc: func(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { +// panic("mock out the Check method") +// }, +// EnabledFunc: func(level zapcore.Level) bool { +// panic("mock out the Enabled method") +// }, +// SyncFunc: func() error { +// panic("mock out the Sync method") +// }, +// WithFunc: func(fields []zapcore.Field) zapcore.Core { +// panic("mock out the With method") +// }, +// WriteFunc: func(entry zapcore.Entry, fields []zapcore.Field) error { +// panic("mock out the Write method") +// }, +// } +// +// // use mockedCore in code that requires zapcore.Core +// // and then make assertions. +// +// } +type ZapCoreMock struct { + // CheckFunc mocks the Check method. + CheckFunc func(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry + + // EnabledFunc mocks the Enabled method. + EnabledFunc func(level zapcore.Level) bool + + // SyncFunc mocks the Sync method. + SyncFunc func() error + + // WithFunc mocks the With method. + WithFunc func(fields []zapcore.Field) zapcore.Core + + // WriteFunc mocks the Write method. + WriteFunc func(entry zapcore.Entry, fields []zapcore.Field) error + + // calls tracks calls to the methods. + calls struct { + // Check holds details about calls to the Check method. + Check []struct { + // Entry is the entry argument value. + Entry zapcore.Entry + // CheckedEntry is the checkedEntry argument value. + CheckedEntry *zapcore.CheckedEntry + } + // Enabled holds details about calls to the Enabled method. + Enabled []struct { + // Level is the level argument value. + Level zapcore.Level + } + // Sync holds details about calls to the Sync method. + Sync []struct { + } + // With holds details about calls to the With method. + With []struct { + // Fields is the fields argument value. + Fields []zapcore.Field + } + // Write holds details about calls to the Write method. + Write []struct { + // Entry is the entry argument value. + Entry zapcore.Entry + // Fields is the fields argument value. + Fields []zapcore.Field + } + } + lockCheck sync.RWMutex + lockEnabled sync.RWMutex + lockSync sync.RWMutex + lockWith sync.RWMutex + lockWrite sync.RWMutex +} + +// Check calls CheckFunc. +func (mock *ZapCoreMock) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if mock.CheckFunc == nil { + panic("ZapCoreMock.CheckFunc: method is nil but Core.Check was just called") + } + callInfo := struct { + Entry zapcore.Entry + CheckedEntry *zapcore.CheckedEntry + }{ + Entry: entry, + CheckedEntry: checkedEntry, + } + mock.lockCheck.Lock() + mock.calls.Check = append(mock.calls.Check, callInfo) + mock.lockCheck.Unlock() + return mock.CheckFunc(entry, checkedEntry) +} + +// CheckCalls gets all the calls that were made to Check. +// Check the length with: +// +// len(mockedCore.CheckCalls()) +func (mock *ZapCoreMock) CheckCalls() []struct { + Entry zapcore.Entry + CheckedEntry *zapcore.CheckedEntry +} { + var calls []struct { + Entry zapcore.Entry + CheckedEntry *zapcore.CheckedEntry + } + mock.lockCheck.RLock() + calls = mock.calls.Check + mock.lockCheck.RUnlock() + return calls +} + +// Enabled calls EnabledFunc. +func (mock *ZapCoreMock) Enabled(level zapcore.Level) bool { + if mock.EnabledFunc == nil { + panic("ZapCoreMock.EnabledFunc: method is nil but Core.Enabled was just called") + } + callInfo := struct { + Level zapcore.Level + }{ + Level: level, + } + mock.lockEnabled.Lock() + mock.calls.Enabled = append(mock.calls.Enabled, callInfo) + mock.lockEnabled.Unlock() + return mock.EnabledFunc(level) +} + +// EnabledCalls gets all the calls that were made to Enabled. +// Check the length with: +// +// len(mockedCore.EnabledCalls()) +func (mock *ZapCoreMock) EnabledCalls() []struct { + Level zapcore.Level +} { + var calls []struct { + Level zapcore.Level + } + mock.lockEnabled.RLock() + calls = mock.calls.Enabled + mock.lockEnabled.RUnlock() + return calls +} + +// Sync calls SyncFunc. +func (mock *ZapCoreMock) Sync() error { + if mock.SyncFunc == nil { + panic("ZapCoreMock.SyncFunc: method is nil but Core.Sync was just called") + } + callInfo := struct { + }{} + mock.lockSync.Lock() + mock.calls.Sync = append(mock.calls.Sync, callInfo) + mock.lockSync.Unlock() + return mock.SyncFunc() +} + +// SyncCalls gets all the calls that were made to Sync. +// Check the length with: +// +// len(mockedCore.SyncCalls()) +func (mock *ZapCoreMock) SyncCalls() []struct { +} { + var calls []struct { + } + mock.lockSync.RLock() + calls = mock.calls.Sync + mock.lockSync.RUnlock() + return calls +} + +// With calls WithFunc. +func (mock *ZapCoreMock) With(fields []zapcore.Field) zapcore.Core { + if mock.WithFunc == nil { + panic("ZapCoreMock.WithFunc: method is nil but Core.With was just called") + } + callInfo := struct { + Fields []zapcore.Field + }{ + Fields: fields, + } + mock.lockWith.Lock() + mock.calls.With = append(mock.calls.With, callInfo) + mock.lockWith.Unlock() + return mock.WithFunc(fields) +} + +// WithCalls gets all the calls that were made to With. +// Check the length with: +// +// len(mockedCore.WithCalls()) +func (mock *ZapCoreMock) WithCalls() []struct { + Fields []zapcore.Field +} { + var calls []struct { + Fields []zapcore.Field + } + mock.lockWith.RLock() + calls = mock.calls.With + mock.lockWith.RUnlock() + return calls +} + +// Write calls WriteFunc. +func (mock *ZapCoreMock) Write(entry zapcore.Entry, fields []zapcore.Field) error { + if mock.WriteFunc == nil { + panic("ZapCoreMock.WriteFunc: method is nil but Core.Write was just called") + } + callInfo := struct { + Entry zapcore.Entry + Fields []zapcore.Field + }{ + Entry: entry, + Fields: fields, + } + mock.lockWrite.Lock() + mock.calls.Write = append(mock.calls.Write, callInfo) + mock.lockWrite.Unlock() + return mock.WriteFunc(entry, fields) +} + +// WriteCalls gets all the calls that were made to Write. +// Check the length with: +// +// len(mockedCore.WriteCalls()) +func (mock *ZapCoreMock) WriteCalls() []struct { + Entry zapcore.Entry + Fields []zapcore.Field +} { + var calls []struct { + Entry zapcore.Entry + Fields []zapcore.Field + } + mock.lockWrite.RLock() + calls = mock.calls.Write + mock.lockWrite.RUnlock() + return calls +} diff --git a/logp/core_test.go b/logp/core_test.go index a881200b..d00d47b4 100644 --- a/logp/core_test.go +++ b/logp/core_test.go @@ -18,13 +18,21 @@ package logp import ( - "io/ioutil" + "bufio" + "encoding/json" + "errors" + "io" golog "log" + "os" + "path/filepath" + "runtime" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) func TestLogger(t *testing.T) { @@ -167,25 +175,25 @@ func TestDebugAllStdoutEnablesDefaultGoLogger(t *testing.T) { err = DevelopmentSetup(WithSelectors("other")) require.NoError(t, err) - assert.Equal(t, ioutil.Discard, golog.Writer()) + assert.Equal(t, io.Discard, golog.Writer()) } func TestNotDebugAllStdoutDisablesDefaultGoLogger(t *testing.T) { err := DevelopmentSetup(WithSelectors("*"), WithLevel(InfoLevel)) require.NoError(t, err) - assert.Equal(t, ioutil.Discard, golog.Writer()) + assert.Equal(t, io.Discard, golog.Writer()) err = DevelopmentSetup(WithSelectors("stdlog"), WithLevel(InfoLevel)) require.NoError(t, err) - assert.Equal(t, ioutil.Discard, golog.Writer()) + assert.Equal(t, io.Discard, golog.Writer()) err = DevelopmentSetup(WithSelectors("*", "stdlog"), WithLevel(InfoLevel)) require.NoError(t, err) - assert.Equal(t, ioutil.Discard, golog.Writer()) + assert.Equal(t, io.Discard, golog.Writer()) err = DevelopmentSetup(WithSelectors("other"), WithLevel(InfoLevel)) require.NoError(t, err) - assert.Equal(t, ioutil.Discard, golog.Writer()) + assert.Equal(t, io.Discard, golog.Writer()) } func TestLoggingECSFields(t *testing.T) { @@ -212,3 +220,421 @@ func TestLoggingECSFields(t *testing.T) { } } } + +func TestCreatingNewLoggerWithDifferentOutput(t *testing.T) { + var tempDir1, tempDir2 string + // Because of the way logp and zap work, when the test finishes, the log + // file is still open, this creates a problem on Windows because the + // temporary directory cannot be removed if a file inside it is still + // open. + // See https://github.com/elastic/elastic-agent-libs/issues/179 + // for more details + // + // To circumvent this problem on Windows we use os.MkdirTemp + // leaving it behind and delegating to the OS the responsibility + // of cleaning it up (usually on restart). + if runtime.GOOS == "windows" { + var err error + tempDir1, err = os.MkdirTemp("", t.Name()+"-*") + if err != nil { + t.Fatalf("could not create temporary directory: %s", err) + } + tempDir2, err = os.MkdirTemp("", t.Name()+"-*") + if err != nil { + t.Fatalf("could not create temporary directory: %s", err) + } + } else { + // We have no problems on Linux and Darwin, so we can rely on t.TempDir + // that will remove the files once the tests finishes. + tempDir1 = t.TempDir() + tempDir2 = t.TempDir() + } + + secondLoggerMessage := "this is a log message" + secondLoggerName := t.Name() + "-second" + + // We follow the same approach as on a Beat, first the logger + // (always global) is configured and used, then we instantiate + // a new one, secondLogger, and perform the tests on it. + loggerCfg := DefaultConfig(DefaultEnvironment) + loggerCfg.Beat = t.Name() + "-first" + loggerCfg.ToFiles = true + loggerCfg.ToStderr = false + loggerCfg.Files.Name = "test-log-file-first" + // We want a separate directory for this logger + // and we don't need to inspect it. + loggerCfg.Files.Path = tempDir1 + + // Configures the global logger with the "default" log configuration. + if err := Configure(loggerCfg); err != nil { + t.Errorf("could not initialise logger: %s", err) + } + + // Create a log entry just to "test" the logger + firstLoggerName := "default-beat-logger" + firstLoggerMessage := "not the message we want" + + logger := L().Named(firstLoggerName) + logger.Info(firstLoggerMessage) + if err := logger.Sync(); err != nil { + t.Fatalf("could not sync log file from fist logger: %s", err) + } + + // Actually clones the logger and use the "WithFileOutput" function + secondCfg := DefaultConfig(DefaultEnvironment) + secondCfg.ToFiles = true + secondCfg.ToStderr = false + secondCfg.Files.Name = "test-log-file" + secondCfg.Files.Path = tempDir2 + + // Create a new output for the second logger using the same level + // as the global logger + out, err := createLogOutput(secondCfg, loggerCfg.Level.ZapLevel()) + if err != nil { + t.Fatalf("could not create output for second config") + } + outCore := func(zapcore.Core) zapcore.Core { return out } + + // We do not call Configure here as we do not want to affect + // the global logger configuration + secondLogger := NewLogger(secondLoggerName) + secondLogger = secondLogger.WithOptions(zap.WrapCore(outCore)) + secondLogger.Info(secondLoggerMessage) + if err := secondLogger.Sync(); err != nil { + t.Fatalf("could not sync log file from second logger: %s", err) + } + + // Write again with the first logger to ensure it has not been affected + // by the new configuration on the second logger. + logger.Info(firstLoggerMessage) + if err := logger.Sync(); err != nil { + t.Fatalf("could not sync log file from fist logger: %s", err) + } + + // Ensure the second logger is working as expected + assertKVinLogentry(t, tempDir2, "log.logger", secondLoggerName) + assertKVinLogentry(t, tempDir2, "message", secondLoggerMessage) + + // Ensure the first logger is working as expected + assertKVinLogentry(t, tempDir1, "log.logger", firstLoggerName) + assertKVinLogentry(t, tempDir1, "message", firstLoggerMessage) +} + +func assertKVinLogentry(t *testing.T, dir, key, value string) { + t.Helper() + + // Find the log file. The file name gets the date added, so we list the + // directory and ensure there is only one file there. + files, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("could not read temporary directory '%s': %s", dir, err) + } + + // If there is more than one file, list all files + // and fail the test. + if len(files) != 1 { + t.Errorf("found %d files in '%s', there must be only one", len(files), dir) + t.Errorf("Files in '%s':", dir) + for _, f := range files { + t.Error(f.Name()) + } + t.FailNow() + } + + fullPath := filepath.Join(dir, files[0].Name()) + f, err := os.Open(fullPath) + if err != nil { + t.Fatalf("could not open '%s' for reading: %s", fullPath, err) + } + defer f.Close() + sc := bufio.NewScanner(f) + lines := []string{} + for sc.Scan() { + logData := sc.Bytes() + + logEntry := map[string]any{} + if err := json.Unmarshal(logData, &logEntry); err != nil { + t.Fatalf("could not read log entry as JSON. Log entry: '%s'", string(logData)) + } + + if logEntry[key] == value { + return + } + lines = append(lines, string(logData)) + } + + t.Errorf("could not find [%s]='%s' in any log line.", key, value) + t.Log("Log lines:") + for _, l := range lines { + t.Log(l) + } +} + +type writeSyncer struct { + strings.Builder +} + +// Sync is a no-op +func (w writeSyncer) Sync() error { + return nil +} + +func TestTypedLoggerCore(t *testing.T) { + testCases := []struct { + name string + entry zapcore.Entry + field zapcore.Field + expectedDefaultLog string + expectedTypedLog string + }{ + { + name: "info level default logger", + entry: zapcore.Entry{Level: zapcore.InfoLevel, Message: "msg"}, + field: skipField(), + expectedDefaultLog: `{"level":"info","msg":"msg"}`, + }, + { + name: "info level typed logger", + entry: zapcore.Entry{Level: zapcore.InfoLevel, Message: "msg"}, + field: strField("log.type", "sensitive"), + expectedTypedLog: `{"level":"info","msg":"msg","log.type":"sensitive"}`, + }, + + { + name: "debug level typed logger", + entry: zapcore.Entry{Level: zapcore.DebugLevel, Message: "msg"}, + field: skipField(), + }, + { + name: "debug level typed logger", + entry: zapcore.Entry{Level: zapcore.DebugLevel, Message: "msg"}, + field: strField("log.type", "sensitive"), + }, + } + + defaultWriter := writeSyncer{} + typedWriter := writeSyncer{} + + cfg := zap.NewProductionEncoderConfig() + cfg.TimeKey = "" // remove the time to make the log entry consistent + + defaultCore := zapcore.NewCore( + zapcore.NewJSONEncoder(cfg), + &defaultWriter, + zapcore.InfoLevel, + ) + + typedCore := zapcore.NewCore( + zapcore.NewJSONEncoder(cfg), + &typedWriter, + zapcore.InfoLevel, + ) + + core := typedLoggerCore{ + defaultCore: defaultCore, + typedCore: typedCore, + key: "log.type", + value: "sensitive", + } + + for _, tc := range testCases { + t.Run(tc.name+" Check method", func(t *testing.T) { + defaultWriter.Reset() + typedWriter.Reset() + + if ce := core.Check(tc.entry, nil); ce != nil { + ce.Write(tc.field) + } + defaultLog := strings.TrimSpace(defaultWriter.String()) + typedLog := strings.TrimSpace(typedWriter.String()) + + if tc.expectedDefaultLog != defaultLog { + t.Errorf("expecting default log to be %q, got %q", tc.expectedDefaultLog, defaultLog) + } + if tc.expectedTypedLog != typedLog { + t.Errorf("expecting typed log to be %q, got %q", tc.expectedTypedLog, typedLog) + } + }) + + // The write method does not check the level, so we skip + // this test if the test case is a lower level + if tc.entry.Level < zapcore.InfoLevel { + continue + } + + t.Run(tc.name+" Write method", func(t *testing.T) { + defaultWriter.Reset() + typedWriter.Reset() + + //nolint:errcheck // It's a test and the underlying writer never fails. + core.Write(tc.entry, []zapcore.Field{tc.field}) + + defaultLog := strings.TrimSpace(defaultWriter.String()) + typedLog := strings.TrimSpace(typedWriter.String()) + + if tc.expectedDefaultLog != defaultLog { + t.Errorf("expecting default log to be %q, got %q", tc.expectedDefaultLog, defaultLog) + } + if tc.expectedTypedLog != typedLog { + t.Errorf("expecting typed log to be %q, got %q", tc.expectedTypedLog, typedLog) + } + + }) + } + + t.Run("method Enabled", func(t *testing.T) { + if !core.Enabled(zapcore.InfoLevel) { + t.Error("core.Enable must return true for level info") + } + + if core.Enabled(zapcore.DebugLevel) { + t.Error("core.Enable must return true for level debug") + } + }) +} + +func TestTypedLoggerCoreSync(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + core := typedLoggerCore{ + defaultCore: &ZapCoreMock{ + SyncFunc: func() error { return nil }, + }, + typedCore: &ZapCoreMock{ + SyncFunc: func() error { return nil }, + }, + } + + if err := core.Sync(); err != nil { + t.Fatalf("Sync must not return an error: %s", err) + } + }) + + t.Run("both cores return error", func(t *testing.T) { + errMsg1 := "some error from defaultCore" + errMsg2 := "some error from typedCore" + core := typedLoggerCore{ + defaultCore: &ZapCoreMock{ + SyncFunc: func() error { return errors.New(errMsg1) }, + }, + typedCore: &ZapCoreMock{ + SyncFunc: func() error { return errors.New(errMsg2) }, + }, + } + + err := core.Sync() + if err == nil { + t.Fatal("Sync must return an error") + } + + gotMsg := err.Error() + if !strings.Contains(gotMsg, errMsg1) { + t.Errorf("expecting %q in the error string: %q", errMsg1, gotMsg) + } + if !strings.Contains(gotMsg, errMsg2) { + t.Errorf("expecting %q in the error string: %q", errMsg2, gotMsg) + } + }) +} + +func TestTypedLoggerCoreWith(t *testing.T) { + defaultWriter := writeSyncer{} + typedWriter := writeSyncer{} + + cfg := zap.NewProductionEncoderConfig() + cfg.TimeKey = "" // remove the time to make the log entry consistent + + defaultCore := zapcore.NewCore( + zapcore.NewJSONEncoder(cfg), + &defaultWriter, + zapcore.InfoLevel, + ) + + typedCore := zapcore.NewCore( + zapcore.NewJSONEncoder(cfg), + &typedWriter, + zapcore.InfoLevel, + ) + + core := typedLoggerCore{ + defaultCore: defaultCore, + typedCore: typedCore, + key: "log.type", + value: "sensitive", + } + + expectedLines := []string{ + // First/Default logger + `{"level":"info","msg":"Very first message"}`, + + // Two messages after calling With + `{"level":"info","msg":"a message with extra fields","foo":"bar"}`, + `{"level":"info","msg":"another message with extra fields","foo":"bar"}`, + + // A message with the default logger + `{"level":"info","msg":"a message without extra fields"}`, + + // Two more messages with a different field + `{"level":"info","msg":"a message with an answer","answer":"42"}`, + `{"level":"info","msg":"another message with an answer","answer":"42"}`, + + // One last message with the default logger + `{"level":"info","msg":"another message without any extra fields"}`, + } + + // The default logger, it should not be modified by any call to With. + logger := zap.New(&core) + logger.Info("Very first message") + + // Add a field and write messages + loggerWithFields := logger.With(strField("foo", "bar")) + loggerWithFields.Info("a message with extra fields") + loggerWithFields.Info("another message with extra fields") + + // Use the default logger again + logger.Info("a message without extra fields") + + // New logger with other fields + loggerWithFields = logger.With(strField("answer", "42")) + loggerWithFields.Info("a message with an answer") + loggerWithFields.Info("another message with an answer") + + // One last message with the default logger + logger.Info("another message without any extra fields") + + scanner := bufio.NewScanner(strings.NewReader(defaultWriter.String())) + count := 0 + for scanner.Scan() { + l := scanner.Text() + if l != expectedLines[count] { + t.Error("Expecting:\n", l, "\nGot:\n", expectedLines[count]) + } + count++ + } +} + +func TestCreateLogOutputAllDisabled(t *testing.T) { + cfg := DefaultConfig(DefaultEnvironment) + cfg.toIODiscard = false + cfg.toObserver = false + cfg.ToEventLog = false + cfg.ToFiles = false + cfg.ToStderr = false + cfg.ToSyslog = false + + out, err := createLogOutput(cfg, zap.DebugLevel) + if err != nil { + t.Fatalf("did not expect an error calling createLogOutput: %s", err) + } + + if out.Enabled(zap.DebugLevel) { + t.Fatal("the output must be disabled to all log levels") + } +} + +func strField(key, val string) zapcore.Field { + return zapcore.Field{Type: zapcore.StringType, Key: key, String: val} +} + +func skipField() zapcore.Field { + return zapcore.Field{Type: zapcore.SkipType} +} diff --git a/logp/logger.go b/logp/logger.go index 32fd8782..88ee1752 100644 --- a/logp/logger.go +++ b/logp/logger.go @@ -18,8 +18,10 @@ package logp import ( + "bytes" "fmt" + "go.elastic.co/ecszap" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -50,6 +52,34 @@ func NewLogger(selector string, options ...LogOption) *Logger { return newLogger(loadLogger().rootLogger, selector, options...) } +// NewInMemory returns a new in-memory logger along with the buffer to which it +// logs. It's goroutine safe, but operating directly on the returned buffer is not. +// This logger is primary intended for short and simple use-cases such as printing +// the full logs only when an operation fails. +// encCfg configures the log format, use logp.ConsoleEncoderConfig for console +// format, logp.JSONEncoderConfig for JSON or any other valid zapcore.EncoderConfig. +func NewInMemory(selector string, encCfg zapcore.EncoderConfig) (*Logger, *bytes.Buffer) { + buff := bytes.Buffer{} + + encoderConfig := ecszap.ECSCompatibleEncoderConfig(encCfg) + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoder := zapcore.NewConsoleEncoder(encoderConfig) + + core := zapcore.NewCore( + encoder, + zapcore.Lock(zapcore.AddSync(&buff)), + zap.NewAtomicLevelAt(zap.DebugLevel)) + ecszap.ECSCompatibleEncoderConfig(ConsoleEncoderConfig()) + + logger := NewLogger( + selector, + zap.WrapCore(func(in zapcore.Core) zapcore.Core { + return core + })) + + return logger, &buff +} + // WithOptions returns a clone of l with options applied. func (l *Logger) WithOptions(options ...LogOption) *Logger { cloned := l.logger.WithOptions(options...) diff --git a/logp/logger_test.go b/logp/logger_test.go index eaf8a107..256ff92f 100644 --- a/logp/logger_test.go +++ b/logp/logger_test.go @@ -18,6 +18,7 @@ package logp import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -50,3 +51,31 @@ func TestLoggerWithOptions(t *testing.T) { require.Len(t, observedEntries2, 1) assert.Equal(t, "hello logger1 and logger2", observedEntries2[0].Message) } + +func TestNewInMemory(t *testing.T) { + log, buff := NewInMemory("in_memory", ConsoleEncoderConfig()) + + log.Debugw("a debug message", "debug_key", "debug_val") + log.Infow("a info message", "info_key", "info_val") + log.Warnw("a warn message", "warn_key", "warn_val") + log.Errorw("an error message", "error_key", "error_val") + + logs := strings.Split(strings.TrimSpace(buff.String()), "\n") + assert.Len(t, logs, 4, "expected 4 log entries") + + assert.Contains(t, logs[0], "a debug message") + assert.Contains(t, logs[0], "debug_key") + assert.Contains(t, logs[0], "debug_val") + + assert.Contains(t, logs[1], "a info message") + assert.Contains(t, logs[1], "info_key") + assert.Contains(t, logs[1], "info_val") + + assert.Contains(t, logs[2], "a warn message") + assert.Contains(t, logs[2], "warn_key") + assert.Contains(t, logs[2], "warn_val") + + assert.Contains(t, logs[3], "an error message") + assert.Contains(t, logs[3], "error_key") + assert.Contains(t, logs[3], "error_val") +} diff --git a/logp/selective_test.go b/logp/selective_test.go index e03d5bd5..16c07d87 100644 --- a/logp/selective_test.go +++ b/logp/selective_test.go @@ -54,3 +54,68 @@ func TestLoggerSelectors(t *testing.T) { logs = ObserverLogs().TakeAll() assert.Len(t, logs, 1) } + +func TestTypedCoreSelectors(t *testing.T) { + logSelector := "enabled-log-selector" + expectedMsg := "this should be logged" + + defaultCfg := DefaultConfig(DefaultEnvironment) + eventsCfg := DefaultEventConfig(DefaultEnvironment) + + defaultCfg.Level = DebugLevel + defaultCfg.toObserver = true + defaultCfg.ToStderr = false + defaultCfg.ToFiles = false + defaultCfg.Selectors = []string{logSelector} + + eventsCfg.Level = defaultCfg.Level + eventsCfg.toObserver = defaultCfg.toObserver + eventsCfg.ToStderr = defaultCfg.ToStderr + eventsCfg.ToFiles = defaultCfg.ToFiles + eventsCfg.Selectors = defaultCfg.Selectors + + if err := ConfigureWithTypedOutput(defaultCfg, eventsCfg, "log.type", "event"); err != nil { + t.Fatalf("could not configure logger: %s", err) + } + + enabledSelector := NewLogger(logSelector) + disabledSelector := NewLogger("foo-selector") + + enabledSelector.Debugw(expectedMsg) + enabledSelector.Debugw(expectedMsg, "log.type", "event") + disabledSelector.Debug("this should not be logged") + + logEntries := ObserverLogs().TakeAll() + if len(logEntries) != 2 { + t.Errorf("expecting 2 log entries, got %d", len(logEntries)) + t.Log("Log entries:") + for _, e := range logEntries { + t.Log("Message:", e.Message, "Fields:", e.Context) + } + t.FailNow() + } + + for i, logEntry := range logEntries { + msg := logEntry.Message + if msg != expectedMsg { + t.Fatalf("[%d] expecting log message '%s', got '%s'", i, expectedMsg, msg) + } + + // The second entry should also contain `log.type: event` + if i == 1 { + fields := logEntry.Context + if len(fields) != 1 { + t.Errorf("expecting one field, got %d", len(fields)) + } + + k := fields[0].Key + v := fields[0].String + if k != "log.type" { + t.Errorf("expecting key 'log.type', got '%s'", k) + } + if v != "event" { + t.Errorf("expecting value 'event', got '%s'", v) + } + } + } +} diff --git a/logp/typedloggercore.go b/logp/typedloggercore.go new file mode 100644 index 00000000..eaae4037 --- /dev/null +++ b/logp/typedloggercore.go @@ -0,0 +1,104 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package logp + +import ( + "fmt" + + "go.uber.org/zap/zapcore" +) + +// TypeKey is the default key to define log types. +// +// Different log types can be handled by different cores, the `typedLoggerCore` +// allows for choosing a different core based on a key/value pair. TypeKey +// is the default key for using the typedLoggerCore. +// +// It should be used in conjunction with the defined types on this package. +const TypeKey = "log.type" + +// DefaultType is the default log type. If `log.type` is not defined a log +// entry is considered of type `DefaultType`. Those log entries should follow +// the default logging configuration. +const DefaultType = "default" + +// EventType is the type for log entries containing event data. +// Beats and Elastic-Agent use this with the `typedLoggerCore` to direct +// those log entries to a different file. +const EventType = "event" + +// typedLoggerCore takes two cores and directs logs entries to one of them +// with the value of the field defined by the pair `key` and `value` +// +// If `entry[key] == value` the typedCore is used, otherwise the +// defaultCore is used. +// WARNING: The level of both cores must always be the same! +// typedLoggerCore will only use the defaultCore level to decide +// whether to log an entry or not +type typedLoggerCore struct { + typedCore zapcore.Core + defaultCore zapcore.Core + value string + key string +} + +func (t *typedLoggerCore) Enabled(l zapcore.Level) bool { + return t.defaultCore.Enabled(l) +} + +func (t *typedLoggerCore) With(fields []zapcore.Field) zapcore.Core { + newCore := typedLoggerCore{ + defaultCore: t.defaultCore.With(fields), + typedCore: t.typedCore.With(fields), + key: t.key, + value: t.value, + } + return &newCore +} + +func (t *typedLoggerCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if t.defaultCore.Enabled(e.Level) { + return ce.AddCore(e, t) + } + + return ce +} + +func (t *typedLoggerCore) Sync() error { + defaultErr := t.defaultCore.Sync() + typedErr := t.typedCore.Sync() + + if defaultErr != nil || typedErr != nil { + return fmt.Errorf("error syncing logger. DefaultCore: '%w', typedCore: '%w'", defaultErr, typedErr) + } + + return nil +} + +func (t *typedLoggerCore) Write(e zapcore.Entry, fields []zapcore.Field) error { + for _, f := range fields { + if f.Key == t.key { + if f.String == t.value { + return t.typedCore.Write(e, fields) + } + return t.defaultCore.Write(e, fields) + } + } + + return t.defaultCore.Write(e, fields) +} diff --git a/testing/certutil/certutil.go b/testing/certutil/certutil.go new file mode 100644 index 00000000..075460ed --- /dev/null +++ b/testing/certutil/certutil.go @@ -0,0 +1,214 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package certutil + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "time" +) + +// Pair is a certificate and its private key in PEM format. +type Pair struct { + Cert []byte + Key []byte +} + +// NewRootCA generates a new x509 Certificate and returns: +// - the private key +// - the certificate +// - the certificate in PEM format as a byte slice. +// +// If any error occurs during the generation process, a non-nil error is returned. +func NewRootCA() (*ecdsa.PrivateKey, *x509.Certificate, Pair, error) { + rootKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err) + } + + notBefore := time.Now() + notAfter := notBefore.Add(3 * time.Hour) + + rootTemplate := x509.Certificate{ + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + SerialNumber: big.NewInt(1653), + Subject: pkix.Name{ + Organization: []string{"Gallifrey"}, + CommonName: "localhost", + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + rootCertRawBytes, err := x509.CreateCertificate( + rand.Reader, &rootTemplate, &rootTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not create CA: %w", err) + } + + rootPrivKeyDER, err := x509.MarshalECPrivateKey(rootKey) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err) + } + + // PEM private key + var rootPrivBytesOut []byte + rootPrivateKeyBuff := bytes.NewBuffer(rootPrivBytesOut) + err = pem.Encode(rootPrivateKeyBuff, &pem.Block{ + Type: "EC PRIVATE KEY", Bytes: rootPrivKeyDER}) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err) + } + + // PEM certificate + var rootCertBytesOut []byte + rootCertPemBuff := bytes.NewBuffer(rootCertBytesOut) + err = pem.Encode(rootCertPemBuff, &pem.Block{ + Type: "CERTIFICATE", Bytes: rootCertRawBytes}) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err) + } + + // tls.Certificate + rootTLSCert, err := tls.X509KeyPair( + rootCertPemBuff.Bytes(), rootPrivateKeyBuff.Bytes()) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not create key pair: %w", err) + } + + rootCACert, err := x509.ParseCertificate(rootTLSCert.Certificate[0]) + if err != nil { + return nil, nil, Pair{}, fmt.Errorf("could not parse certificate: %w", err) + } + + return rootKey, rootCACert, Pair{ + Cert: rootCertPemBuff.Bytes(), + Key: rootPrivateKeyBuff.Bytes(), + }, nil +} + +// GenerateChildCert generates a x509 Certificate as a child of caCert and +// returns the following: +// - the certificate in PEM format as a byte slice +// - the private key in PEM format as a byte slice +// - the certificate and private key as a tls.Certificate +// +// If any error occurs during the generation process, a non-nil error is returned. +func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate) (*tls.Certificate, Pair, error) { + + notBefore := time.Now() + notAfter := notBefore.Add(3 * time.Hour) + + certTemplate := &x509.Certificate{ + DNSNames: []string{name}, + IPAddresses: ips, + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + Organization: []string{"Gallifrey"}, + CommonName: name, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + } + + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not create private key: %w", err) + } + + certRawBytes, err := x509.CreateCertificate( + rand.Reader, certTemplate, caCert, &privateKey.PublicKey, caPrivKey) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not create CA: %w", err) + } + + privateKeyDER, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not marshal private key: %w", err) + } + + // PEM private key + var privBytesOut []byte + privateKeyBuff := bytes.NewBuffer(privBytesOut) + err = pem.Encode(privateKeyBuff, &pem.Block{ + Type: "EC PRIVATE KEY", Bytes: privateKeyDER}) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not pem.Encode private key: %w", err) + } + privateKeyPemBytes := privateKeyBuff.Bytes() + + // PEM certificate + var certBytesOut []byte + certBuff := bytes.NewBuffer(certBytesOut) + err = pem.Encode(certBuff, &pem.Block{ + Type: "CERTIFICATE", Bytes: certRawBytes}) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not pem.Encode certificate: %w", err) + } + certPemBytes := certBuff.Bytes() + + // TLS Certificate + tlsCert, err := tls.X509KeyPair(certPemBytes, privateKeyPemBytes) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not create key pair: %w", err) + } + + return &tlsCert, Pair{ + Cert: certPemBytes, + Key: privateKeyPemBytes, + }, nil +} + +// NewRootAndChildCerts returns a root CA and a child certificate and their keys +// for "localhost" and "127.0.0.1". +func NewRootAndChildCerts() (Pair, Pair, error) { + rootKey, rootCACert, rootPair, err := NewRootCA() + if err != nil { + return Pair{}, Pair{}, fmt.Errorf("could not generate root CA: %w", err) + } + + _, childPair, err := + GenerateChildCert( + "localhost", + []net.IP{net.ParseIP("127.0.0.1")}, + rootKey, + rootCACert) + if err != nil { + return Pair{}, Pair{}, fmt.Errorf( + "could not generate child TLS certificate CA: %w", err) + } + + return rootPair, childPair, nil +} diff --git a/testing/certutil/cmd/main.go b/testing/certutil/cmd/main.go new file mode 100644 index 00000000..d833d689 --- /dev/null +++ b/testing/certutil/cmd/main.go @@ -0,0 +1,119 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package main + +import ( + "crypto" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "net" + "os" + "path/filepath" + "strings" + + "github.com/elastic/elastic-agent-libs/testing/certutil" +) + +func main() { + var caPath, caKeyPath, dest, name, ipList string + flag.StringVar(&caPath, "ca", "", + "File path for CA in PEM format") + flag.StringVar(&caKeyPath, "ca-key", "", + "File path for the CA key in PEM format") + flag.StringVar(&caKeyPath, "dest", "", + "Directory to save the generated files") + flag.StringVar(&name, "name", "localhost", + "used as \"distinguished name\" and \"Subject Alternate Name values\" for the child certificate") + flag.StringVar(&ipList, "ips", "127.0.0.1", + "a comma separated list of IP addresses for the child certificate") + flag.Parse() + + if caPath == "" && caKeyPath != "" || caPath != "" && caKeyPath == "" { + flag.Usage() + fmt.Fprintf(flag.CommandLine.Output(), + "Both 'ca' and 'ca-key' must be specified, or neither should be provided.\nGot ca: %s, ca-key: %s\n", + caPath, caKeyPath) + + } + + ips := strings.Split(ipList, ",") + var netIPs []net.IP + for _, ip := range ips { + netIPs = append(netIPs, net.ParseIP(ip)) + } + + var rootCert *x509.Certificate + var rootKey crypto.PrivateKey + var err error + if caPath == "" && caKeyPath == "" { + var pair certutil.Pair + rootKey, rootCert, pair, err = certutil.NewRootCA() + if err != nil { + panic(fmt.Errorf("could not create root CA certificate: %w", err)) + } + + savePair(dest, "ca", pair) + } else { + rootKey, rootCert = loadCA(caPath, caKeyPath) + } + + _, childPair, err := certutil.GenerateChildCert(name, netIPs, rootKey, rootCert) + if err != nil { + panic(fmt.Errorf("error generating child certificate: %w", err)) + } + + savePair(dest, name, childPair) +} + +func loadCA(caPath string, keyPath string) (crypto.PrivateKey, *x509.Certificate) { + caBytes, err := os.ReadFile(caPath) + if err != nil { + panic(fmt.Errorf("failed reading CA file: %w", err)) + } + + keyBytes, err := os.ReadFile(keyPath) + if err != nil { + panic(fmt.Errorf("failed reading CA key file: %w", err)) + } + + tlsCert, err := tls.X509KeyPair(caBytes, keyBytes) + if err != nil { + panic(fmt.Errorf("failed generating TLS key pair: %w", err)) + } + + rootCACert, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + panic(fmt.Errorf("could not parse certificate: %w", err)) + } + + return tlsCert.PrivateKey, rootCACert +} + +func savePair(dest string, name string, pair certutil.Pair) { + err := os.WriteFile(filepath.Join(dest, name+".pem"), pair.Cert, 0o600) + if err != nil { + panic(fmt.Errorf("could not save %s certificate: %w", name, err)) + } + + err = os.WriteFile(filepath.Join(dest, name+"_key.pem"), pair.Key, 0o600) + if err != nil { + panic(fmt.Errorf("could not save %s certificate key: %w", name, err)) + } +} diff --git a/transport/httpcommon/httpcommon.go b/transport/httpcommon/httpcommon.go index a3acf9e4..cf8c3f88 100644 --- a/transport/httpcommon/httpcommon.go +++ b/transport/httpcommon/httpcommon.go @@ -18,6 +18,10 @@ package httpcommon import ( + "bytes" + "errors" + "fmt" + "io" "net/http" "time" @@ -30,6 +34,10 @@ import ( "github.com/elastic/elastic-agent-libs/transport/tlscommon" ) +var ( + ErrResponseLimit = errors.New("HTTP response length limit was reached") +) + // HTTPTransportSettings provides common HTTP settings for HTTP clients. type HTTPTransportSettings struct { // TLS provides ssl/tls setup settings @@ -410,3 +418,68 @@ func WithLogger(logger *logp.Logger) TransportOption { s.logger = logger }) } + +// ReadAll returns the whole response body as bytes. +// This is an optimized version of `io.ReadAll`. +// +// Use `ReadAllWithLimit` with a reasonable limit when possible! Avoid reading HTTP responses without a limit! +// A malicious server might serve a `Content-Length` header with a value too high to handle +// or the server might serve a response body that is too long and can crash the client with OOM. +func ReadAll(resp *http.Response) ([]byte, error) { + return ReadAllWithLimit(resp, -1) +} + +// ReadAllWithLimit returns the whole response body as bytes respecting the given limit. +// This is an optimized version of `io.ReadAll`. +// +// If the `limit` is 0, an empty byte slice is returned. +// If the `limit` is a negative value, e.g `-1`, the limit is ignored and the entire response body is returned. +// If the `Content-Length` header was served and its value exceeds the limit, the `ErrResponseLimit` error is returned. +// If the body length is not known in advance, it reads from the body up to the set limit and returns a partial response without an error. +// +// Avoid reading HTTP responses without a limit and use a reasonable limit instead! +// A malicious server might serve a `Content-Length` header with a value too high to handle +// or the server might serve a response body that is too long and can crash the client with OOM. +func ReadAllWithLimit(resp *http.Response, limit int64) ([]byte, error) { + if resp == nil { + return nil, errors.New("response cannot be nil") + } + switch { + // nothing to read according to the server or limit + case resp.ContentLength == 0 || limit == 0 || resp.StatusCode == http.StatusNoContent: + return []byte{}, nil + + // here if the limit is negative, e.g. `-1` it's ignored, + // limit == 0 is handled above + case limit > 0 && resp.ContentLength > limit: + return nil, fmt.Errorf("received Content-Length %d exceeds the set limit %d: %w", resp.ContentLength, limit, ErrResponseLimit) + + // if we know the body length, we can allocate the buffer only once for the most efficient read + case resp.ContentLength >= 0: + body := make([]byte, resp.ContentLength) + _, err := io.ReadFull(resp.Body, body) + if err != nil { + return nil, fmt.Errorf("failed to read the response body with a known length %d: %w", resp.ContentLength, err) + } + return body, nil + + default: + // using `bytes.NewBuffer` + `io.Copy` is much faster than `io.ReadAll` + // see https://github.com/elastic/beats/issues/36151#issuecomment-1931696767 + buf := bytes.NewBuffer(nil) + var err error + if limit > 0 { + _, err = io.Copy(buf, io.LimitReader(resp.Body, limit)) + } else { + _, err = io.Copy(buf, resp.Body) + } + if err != nil { + return nil, fmt.Errorf("failed to read the response body with unknown length: %w", err) + } + body := buf.Bytes() + if body == nil { + body = []byte{} + } + return body, nil + } +} diff --git a/transport/httpcommon/httpcommon_test.go b/transport/httpcommon/httpcommon_test.go index f9883738..a36308cd 100644 --- a/transport/httpcommon/httpcommon_test.go +++ b/transport/httpcommon/httpcommon_test.go @@ -18,6 +18,10 @@ package httpcommon import ( + "bytes" + "fmt" + "io" + "net/http" "testing" "time" @@ -92,3 +96,150 @@ ssl: }) } } + +func TestReadAllWithLimit(t *testing.T) { + size := 100 + body := bytes.Repeat([]byte{'a'}, size) + cases := []struct { + name string + resp *http.Response + limit int64 + expBody []byte + expErr error + }{ + { + name: "reads known size without limit", + resp: &http.Response{ + ContentLength: int64(size), + Body: io.NopCloser(bytes.NewBuffer(body)), + }, + limit: -1, + expBody: body, + }, + { + name: "does not read known size if exceeds limit", + resp: &http.Response{ + ContentLength: int64(size), + Body: io.NopCloser(bytes.NewBuffer(body)), + }, + limit: 10, + expErr: ErrResponseLimit, + }, + { + name: "reads unknown size without limit", + resp: &http.Response{ + ContentLength: -1, + Body: io.NopCloser(bytes.NewBuffer(body)), + }, + limit: -1, + expBody: body, + }, + { + name: "partially reads unknown size with limit", + resp: &http.Response{ + ContentLength: -1, + Body: io.NopCloser(bytes.NewBuffer(body)), + }, + limit: 10, + expBody: body[:10], + }, + { + name: "supports empty with size=0", + resp: &http.Response{ + ContentLength: 0, + }, + limit: -1, + expBody: []byte{}, + }, + { + name: "does not read the body if `No Content` status", + resp: &http.Response{ + StatusCode: http.StatusNoContent, + }, + limit: -1, + expBody: []byte{}, + }, + { + name: "supports empty with unknown size", + resp: &http.Response{ + ContentLength: -1, + Body: io.NopCloser(bytes.NewBuffer(nil)), + }, + limit: -1, + expBody: []byte{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actBody, err := ReadAllWithLimit(tc.resp, tc.limit) + if tc.expErr != nil { + require.ErrorIs(t, err, tc.expErr) + require.Nil(t, actBody) + return + } + require.NoError(t, err) + require.Equal(t, tc.expBody, actBody) + }) + } +} + +func BenchmarkReadAll(b *testing.B) { + sizes := []int{ + 1024, // 1KB + 100 * 1024, // 100KB + 1024 * 1024, // 1MB + } + for _, size := range sizes { + b.Run(fmt.Sprintf("size: %d", size), func(b *testing.B) { + + // emulate a file or an HTTP response + generated := bytes.Repeat([]byte{'a'}, size) + content := bytes.NewReader(generated) + cases := []struct { + name string + resp *http.Response + }{ + { + name: "unknown length", + resp: &http.Response{ + ContentLength: -1, + Body: io.NopCloser(content), + }, + }, + { + name: "known length", + resp: &http.Response{ + ContentLength: int64(size), + Body: io.NopCloser(content), + }, + }, + } + + b.ResetTimer() + + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + b.Run("io.ReadAll", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := content.Seek(0, io.SeekStart) // reset + require.NoError(b, err) + data, err := io.ReadAll(tc.resp.Body) + require.NoError(b, err) + require.Equalf(b, size, len(data), "size does not match, expected %d, actual %d", size, len(data)) + } + }) + b.Run("bytes.Buffer+io.Copy", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := content.Seek(0, io.SeekStart) // reset + require.NoError(b, err) + data, err := ReadAll(tc.resp) + require.NoError(b, err) + require.Equalf(b, size, len(data), "size does not match, expected %d, actual %d", size, len(data)) + } + }) + }) + } + }) + } +} diff --git a/transport/tlscommon/server_config_test.go b/transport/tlscommon/server_config_test.go index b12b98be..9f9bc8b9 100644 --- a/transport/tlscommon/server_config_test.go +++ b/transport/tlscommon/server_config_test.go @@ -20,6 +20,7 @@ package tlscommon import ( "testing" + "github.com/elastic/go-ucfg" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) @@ -92,3 +93,160 @@ func Test_ServerConfig_Serialization_ClientAuth(t *testing.T) { }) } } + +func Test_ServerConfig_Repack(t *testing.T) { + tests := []struct { + name string + yaml string + auth *TLSClientAuth + }{{ + name: "with client auth", + yaml: ` + enabled: true + verification_mode: certificate + supported_protocols: [TLSv1.1, TLSv1.2] + cipher_suites: + - RSA-AES-256-CBC-SHA + certificate_authorities: + - /path/to/ca.crt + certificate: /path/to/cert.cry + key: /path/to/key/crt + curve_types: + - P-521 + client_authentication: optional + ca_sha256: + - example`, + auth: &optional, + }, { + name: "nil client auth", + yaml: ` + enabled: true + verification_mode: certificate + supported_protocols: [TLSv1.1, TLSv1.2] + cipher_suites: + - RSA-AES-256-CBC-SHA + certificate_authorities: + - /path/to/ca.crt + certificate: /path/to/cert.cry + key: /path/to/key/crt + curve_types: + - P-521 + ca_sha256: + - example`, + auth: &required, + }, { + name: "nil client auth, no cas", + yaml: ` + enabled: true + verification_mode: certificate + supported_protocols: [TLSv1.1, TLSv1.2] + cipher_suites: + - RSA-AES-256-CBC-SHA + certificate: /path/to/cert.cry + key: /path/to/key/crt + curve_types: + - P-521 + ca_sha256: + - example`, + auth: nil, + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := mustLoadServerConfig(t, tc.yaml) + if tc.auth != nil { + require.Equal(t, *tc.auth, *cfg.ClientAuth) + } else { + require.Nil(t, cfg.ClientAuth) + } + + tmp, err := ucfg.NewFrom(cfg) + require.NoError(t, err) + + err = tmp.Unpack(&cfg) + require.NoError(t, err) + if tc.auth != nil { + require.Equal(t, *tc.auth, *cfg.ClientAuth) + } else { + require.Nil(t, cfg.ClientAuth) + } + }) + } +} + +func Test_ServerConfig_RepackJSON(t *testing.T) { + tests := []struct { + name string + json string + auth *TLSClientAuth + }{{ + name: "with client auth", + json: `{ + "enabled": true, + "verification_mode": "certificate", + "supported_protocols": ["TLSv1.1", "TLSv1.2"], + "cipher_suites": ["RSA-AES-256-CBC-SHA"], + "certificate_authorities": ["/path/to/ca.crt"], + "certificate": "/path/to/cert.crt", + "key": "/path/to/key.crt", + "curve_types": "P-521", + "renegotiation": "freely", + "ca_sha256": ["example"], + "ca_trusted_fingerprint": "fingerprint", + "client_authentication": "optional" + }`, + auth: &optional, + }, { + name: "nil client auth", + json: `{ + "enabled": true, + "verification_mode": "certificate", + "supported_protocols": ["TLSv1.1", "TLSv1.2"], + "cipher_suites": ["RSA-AES-256-CBC-SHA"], + "certificate_authorities": ["/path/to/ca.crt"], + "certificate": "/path/to/cert.crt", + "key": "/path/to/key.crt", + "curve_types": "P-521", + "renegotiation": "freely", + "ca_sha256": ["example"], + "ca_trusted_fingerprint": "fingerprint" + }`, + auth: &required, + }, { + name: "nil client auth, no cas", + json: `{ + "enabled": true, + "verification_mode": "certificate", + "supported_protocols": ["TLSv1.1", "TLSv1.2"], + "cipher_suites": ["RSA-AES-256-CBC-SHA"], + "certificate": "/path/to/cert.crt", + "key": "/path/to/key.crt", + "curve_types": "P-521", + "renegotiation": "freely", + "ca_sha256": ["example"] + }`, + auth: nil, + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := mustLoadServerConfigJSON(t, tc.json) + if tc.auth != nil { + require.Equal(t, *tc.auth, *cfg.ClientAuth) + } else { + require.Nil(t, cfg.ClientAuth) + } + + tmp, err := ucfg.NewFrom(cfg) + require.NoError(t, err) + + err = tmp.Unpack(&cfg) + require.NoError(t, err) + if tc.auth != nil { + require.Equal(t, *tc.auth, *cfg.ClientAuth) + } else { + require.Nil(t, cfg.ClientAuth) + } + }) + } +} diff --git a/transport/tlscommon/tls.go b/transport/tlscommon/tls.go index fb3c031a..c5bf2b43 100644 --- a/transport/tlscommon/tls.go +++ b/transport/tlscommon/tls.go @@ -27,13 +27,11 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strings" - "github.com/youmark/pkcs8" - "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/pkcs8" ) const logSelector = "tls" @@ -101,7 +99,7 @@ func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) { } defer r.Close() - content, err := ioutil.ReadAll(r) + content, err := io.ReadAll(r) if err != nil { return nil, err } @@ -219,7 +217,7 @@ func LoadCertificateAuthorities(CAs []string) (*x509.CertPool, []error) { } defer r.Close() - pemData, err := ioutil.ReadAll(r) + pemData, err := io.ReadAll(r) if err != nil { log.Errorf("Failed reading CA certificate: %+v", err) errors = append(errors, fmt.Errorf("%w reading %v", err, r)) @@ -276,7 +274,7 @@ type PEMReader struct { // NewPEMReader returns a new PEMReader. func NewPEMReader(certificate string) (*PEMReader, error) { if IsPEMString(certificate) { - return &PEMReader{reader: ioutil.NopCloser(strings.NewReader(certificate)), debugStr: "inline"}, nil + return &PEMReader{reader: io.NopCloser(strings.NewReader(certificate)), debugStr: "inline"}, nil } r, err := os.Open(certificate) diff --git a/transport/tlscommon/tls_test.go b/transport/tlscommon/tls_test.go index 27df2b89..0de8872b 100644 --- a/transport/tlscommon/tls_test.go +++ b/transport/tlscommon/tls_test.go @@ -29,6 +29,9 @@ import ( "github.com/stretchr/testify/require" "github.com/elastic/elastic-agent-libs/config" + + ucfg "github.com/elastic/go-ucfg" + "github.com/elastic/go-ucfg/json" ) const ( @@ -76,6 +79,50 @@ func mustLoad(t *testing.T, yamlStr string) *Config { return cfg } +// copied from config.fromConfig +func cfgConvert(in *ucfg.Config) *config.C { + return (*config.C)(in) +} + +func loadJSON(jsonStr string) (*Config, error) { + var cfg Config + uc, err := json.NewConfig([]byte(jsonStr), ucfg.PathSep("."), ucfg.VarExp) + if err != nil { + return nil, err + } + + c := cfgConvert(uc) + + if err = c.Unpack(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func loadServerConfigJSON(jsonStr string) (*ServerConfig, error) { + var cfg ServerConfig + uc, err := json.NewConfig([]byte(jsonStr), ucfg.PathSep("."), ucfg.VarExp) + if err != nil { + return nil, err + } + + c := cfgConvert(uc) + + if err = c.Unpack(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func mustLoadServerConfigJSON(t *testing.T, jsonStr string) *ServerConfig { + t.Helper() + cfg, err := loadServerConfigJSON(jsonStr) + if err != nil { + t.Fatal(err) + } + return cfg +} + func writeTestFile(t *testing.T, content string) string { t.Helper() f, err := os.CreateTemp(t.TempDir(), "") @@ -647,6 +694,7 @@ mrPVWmOCMtwHJrO7kF1ENDgHPkhoZFcpFhu3lzOY7mhpW5mPZPVs87ZmI75G7zMV AcV8KJqa/7XTTpvIzXePw9FtSSux5SkU6iKAKqwUt82D1E73bbppSg== -----END CERTIFICATE----- ` + //nolint:gosec // testing key key := ` -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED diff --git a/transport/tlscommon/types.go b/transport/tlscommon/types.go index 2dbf5359..531023f7 100644 --- a/transport/tlscommon/types.go +++ b/transport/tlscommon/types.go @@ -159,28 +159,38 @@ func (m TLSVerificationMode) MarshalText() ([]byte, error) { return nil, fmt.Errorf("could not marshal '%+v' to text", m) } -// Unpack unpacks the string into constants. +// Unpack unpacks the input into a TLSVerificationMode. func (m *TLSVerificationMode) Unpack(in interface{}) error { if in == nil { *m = VerifyFull return nil } - - s, ok := in.(string) - if !ok { - return fmt.Errorf("verification mode must be an identifier") - } - if s == "" { - *m = VerifyFull - return nil + switch o := in.(type) { + case string: + if o == "" { + *m = VerifyFull + return nil + } + + mode, found := tlsVerificationModes[o] + if !found { + return fmt.Errorf("unknown verification mode '%v'", o) + } + *m = mode + case int64: + *m = TLSVerificationMode(o) + case uint64: + *m = TLSVerificationMode(o) + default: + return fmt.Errorf("verification mode is an unknown type: %T", o) } + return nil +} - mode, found := tlsVerificationModes[s] - if !found { - return fmt.Errorf("unknown verification mode '%v'", s) +func (m *TLSVerificationMode) Validate() error { + if *m > VerifyStrict { + return fmt.Errorf("unsupported verification mode: %v", m) } - - *m = mode return nil } @@ -198,29 +208,51 @@ func (m TLSClientAuth) MarshalText() ([]byte, error) { return nil, fmt.Errorf("could not marshal '%+v' to text", m) } -func (m *TLSClientAuth) Unpack(s string) error { - if s == "" { +func (m *TLSClientAuth) Unpack(in interface{}) error { + if in == nil { *m = TLSClientAuthNone return nil } - mode, found := tlsClientAuthTypes[s] - if !found { - return fmt.Errorf("unknown client authentication mode '%v'", s) + switch o := in.(type) { + case string: + if o == "" { + *m = TLSClientAuthNone + return nil + } + mode, found := tlsClientAuthTypes[o] + if !found { + return fmt.Errorf("unknown client authentication mode '%v'", o) + } + + *m = mode + case uint64: + *m = TLSClientAuth(o) + case int64: // underlying type is int so we need both uint64 and int64 as options for TLSClientAuth + *m = TLSClientAuth(o) + default: + return fmt.Errorf("client auth mode is an unknown type: %T", o) } - - *m = mode return nil } type CipherSuite uint16 -func (cs *CipherSuite) Unpack(s string) error { - suite, found := tlsCipherSuites[s] - if !found { - return fmt.Errorf("invalid tls cipher suite '%v'", s) +func (cs *CipherSuite) Unpack(i interface{}) error { + switch o := i.(type) { + case string: + suite, found := tlsCipherSuites[o] + if !found { + return fmt.Errorf("invalid tls cipher suite '%v'", o) + } + + *cs = suite + case int64: + *cs = CipherSuite(o) + case uint64: + *cs = CipherSuite(o) + default: + return fmt.Errorf("cipher suite is an unknown type: %T", o) } - - *cs = suite return nil } @@ -233,13 +265,22 @@ func (cs CipherSuite) String() string { type tlsCurveType tls.CurveID -func (ct *tlsCurveType) Unpack(s string) error { - t, found := tlsCurveTypes[s] - if !found { - return fmt.Errorf("invalid tls curve type '%v'", s) +func (ct *tlsCurveType) Unpack(i interface{}) error { + switch o := i.(type) { + case string: + t, found := tlsCurveTypes[o] + if !found { + return fmt.Errorf("invalid tls curve type '%v'", o) + } + + *ct = t + case int64: + *ct = tlsCurveType(o) + case uint64: + *ct = tlsCurveType(o) + default: + return fmt.Errorf("tls curve type is an unsupported input type: %T", o) } - - *ct = t return nil } @@ -252,13 +293,22 @@ func (r TLSRenegotiationSupport) String() string { return "<" + unknownType + ">" } -func (r *TLSRenegotiationSupport) Unpack(s string) error { - t, found := tlsRenegotiationSupportTypes[s] - if !found { - return fmt.Errorf("invalid tls renegotiation type '%v'", s) +func (r *TLSRenegotiationSupport) Unpack(i interface{}) error { + switch o := i.(type) { + case string: + t, found := tlsRenegotiationSupportTypes[o] + if !found { + return fmt.Errorf("invalid tls renegotiation type '%v'", o) + } + + *r = t + case int64: + *r = TLSRenegotiationSupport(o) + case uint64: + *r = TLSRenegotiationSupport(o) + default: + return fmt.Errorf("tls renegotation support is an unknown type: %T", o) } - - *r = t return nil } diff --git a/transport/tlscommon/types_test.go b/transport/tlscommon/types_test.go index 7a58c5e9..e0c12cc7 100644 --- a/transport/tlscommon/types_test.go +++ b/transport/tlscommon/types_test.go @@ -18,10 +18,12 @@ package tlscommon import ( + "crypto/tls" "fmt" "testing" "github.com/elastic/elastic-agent-libs/config" + "github.com/elastic/go-ucfg" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -69,6 +71,62 @@ func TestLoadWithEmptyVerificationMode(t *testing.T) { assert.Equal(t, cfg.VerificationMode, VerifyFull) } +func TestRepackConfig(t *testing.T) { + cfg, err := load(` + enabled: true + verification_mode: certificate + supported_protocols: [TLSv1.1, TLSv1.2] + cipher_suites: + - RSA-AES-256-CBC-SHA + certificate_authorities: + - /path/to/ca.crt + certificate: /path/to/cert.crt + key: /path/to/key.crt + curve_types: + - P-521 + renegotiation: freely + ca_sha256: + - example + ca_trusted_fingerprint: fingerprint + `) + + assert.NoError(t, err) + assert.Equal(t, cfg.VerificationMode, VerifyCertificate) + + tmp, err := ucfg.NewFrom(cfg) + assert.NoError(t, err) + + err = tmp.Unpack(cfg) + assert.NoError(t, err) + assert.Equal(t, cfg.VerificationMode, VerifyCertificate) +} + +func TestRepackConfigFromJSON(t *testing.T) { + cfg, err := loadJSON(`{ + "enabled": true, + "verification_mode": "certificate", + "supported_protocols": ["TLSv1.1", "TLSv1.2"], + "cipher_suites": ["RSA-AES-256-CBC-SHA"], + "certificate_authorities": ["/path/to/ca.crt"], + "certificate": "/path/to/cert.crt", + "key": "/path/to/key.crt", + "curve_types": "P-521", + "renegotiation": "freely", + "ca_sha256": ["example"], + "ca_trusted_fingerprint": "fingerprint" + }`) + + assert.NoError(t, err) + assert.Equal(t, cfg.VerificationMode, VerifyCertificate) + + tmp, err := ucfg.NewFrom(cfg) + assert.NoError(t, err) + + err = tmp.Unpack(cfg) + assert.NoError(t, err) + assert.Equal(t, cfg.VerificationMode, VerifyCertificate) +} + func TestTLSClientAuthUnpack(t *testing.T) { tests := []struct { val string @@ -231,3 +289,243 @@ func mustLoadServerConfig(t *testing.T, yamlStr string) *ServerConfig { } return cfg } + +func Test_TLSVerificaionMode_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp TLSVerificationMode + }{{ + name: "nil", + hasErr: false, + in: nil, + exp: VerifyFull, + }, { + name: "empty string", + hasErr: false, + in: "", + exp: VerifyFull, + }, { + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "strict", + exp: VerifyStrict, + }, { + name: "int64", + hasErr: false, + in: int64(1), + exp: VerifyNone, + }, { + name: "uint64", + hasErr: false, + in: uint64(1), + exp: VerifyNone, + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(TLSVerificationMode) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +} + +func Test_TLSClientAuth_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp TLSClientAuth + }{{ + name: "nil", + hasErr: false, + in: nil, + exp: TLSClientAuthNone, + }, { + name: "empty string", + hasErr: false, + in: "", + exp: TLSClientAuthNone, + }, { + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "optional", + exp: TLSClientAuthOptional, + }, { + name: "int64", + hasErr: false, + in: int64(3), + exp: TLSClientAuthOptional, + }, { + name: "uint64", + hasErr: false, + in: uint64(3), + exp: TLSClientAuthOptional, + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(TLSClientAuth) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +} + +func Test_CipherSuite_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp CipherSuite + }{{ + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "RSA-AES-128-CBC-SHA", + exp: CipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA), + }, { + name: "int64", + hasErr: false, + in: int64(47), + exp: CipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA), + }, { + name: "uint64", + hasErr: false, + in: uint64(47), + exp: CipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA), + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(CipherSuite) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +} + +func Test_tlsCurveType_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp tlsCurveType + }{{ + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "P-256", + exp: tlsCurveType(tls.CurveP256), + }, { + name: "int64", + hasErr: false, + in: int64(23), + exp: tlsCurveType(tls.CurveP256), + }, { + name: "uint64", + hasErr: false, + in: uint64(23), + exp: tlsCurveType(tls.CurveP256), + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(tlsCurveType) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +} + +func Test_TLSRenegotiationSupport_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp TLSRenegotiationSupport + }{{ + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "never", + exp: TLSRenegotiationSupport(tls.RenegotiateNever), + }, { + name: "int64", + hasErr: false, + in: int64(0), + exp: TLSRenegotiationSupport(tls.RenegotiateNever), + }, { + name: "uint64", + hasErr: false, + in: uint64(0), + exp: TLSRenegotiationSupport(tls.RenegotiateNever), + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(TLSRenegotiationSupport) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +} diff --git a/transport/tlscommon/versions.go b/transport/tlscommon/versions.go index e630ef4d..7c4d613a 100644 --- a/transport/tlscommon/versions.go +++ b/transport/tlscommon/versions.go @@ -38,12 +38,27 @@ func (v TLSVersion) Details() *TLSVersionDetails { } // Unpack transforms the string into a constant. -func (v *TLSVersion) Unpack(s string) error { - version, found := tlsProtocolVersions[s] - if !found { - return fmt.Errorf("invalid tls version '%v'", s) +func (v *TLSVersion) Unpack(i interface{}) error { + switch o := i.(type) { + case string: + version, found := tlsProtocolVersions[o] + if !found { + return fmt.Errorf("invalid tls version '%v'", o) + } + *v = version + case int64: + *v = TLSVersion(o) + case uint64: + *v = TLSVersion(o) + default: + return fmt.Errorf("tls version is an unknown type: %T", o) } + return nil +} - *v = version +func (v *TLSVersion) Validate() error { + if *v < TLSVersionMin || *v > TLSVersionMax { + return fmt.Errorf("unsupported tls version: %v", v) + } return nil } diff --git a/transport/tlscommon/versions_test.go b/transport/tlscommon/versions_test.go index 7f2b2e02..c779ca1f 100644 --- a/transport/tlscommon/versions_test.go +++ b/transport/tlscommon/versions_test.go @@ -21,6 +21,7 @@ import ( "crypto/tls" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -70,3 +71,47 @@ func TestTLSVersion(t *testing.T) { }) } } + +func Test_TLSVersion_Unpack(t *testing.T) { + tests := []struct { + name string + hasErr bool + in interface{} + exp TLSVersion + }{{ + name: "unknown string", + hasErr: true, + in: "unknown", + }, { + name: "string", + hasErr: false, + in: "TLSv1.2", + exp: TLSVersion12, + }, { + name: "int64", + hasErr: false, + in: int64(0x303), + exp: TLSVersion12, + }, { + name: "uint64", + hasErr: false, + in: uint64(0x303), + exp: TLSVersion12, + }, { + name: "unknown type", + hasErr: true, + in: uint8(1), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v := new(TLSVersion) + err := v.Unpack(tc.in) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.exp, *v) + } + }) + } +}