From 0053aafbd357b872454a401025caa10a49d328b7 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Wed, 15 Aug 2018 12:45:54 -0500 Subject: [PATCH] Add TLS Metadata to HTTP/TCP monitors. +mapval enhancements (#7545) * We now write certificate PEM data, and chain validation times to events * Enhanced test helpers (including mapval IsDefs) to handle new testing scenarios * Improved mapval handling of type mismatches to help with the tests here * Move CertToPEMString to libbeat to share between hb and pb * Incorporate @urso 's PR feedback using registration for equality types --- CHANGELOG.asciidoc | 2 + heartbeat/docs/fields.asciidoc | 27 ++ heartbeat/hbtest/hbtestutil.go | 35 +- heartbeat/include/fields.go | 2 +- .../active/dialchain/_meta/fields.yml | 9 + heartbeat/monitors/active/dialchain/tls.go | 38 +- heartbeat/monitors/active/http/http_test.go | 52 ++- heartbeat/monitors/active/tcp/tcp_test.go | 92 ++++- libbeat/common/mapval/is_defs.go | 112 +++++- libbeat/common/mapval/is_defs_test.go | 10 + libbeat/common/mapval/path.go | 36 +- libbeat/common/mapval/path_test.go | 324 ++++++++++++++++++ libbeat/common/mapval/results.go | 4 +- libbeat/common/mapval/walk.go | 7 +- libbeat/common/x509util/x509util.go | 32 ++ libbeat/common/x509util/x509util_test.go | 71 ++++ packetbeat/protos/tls/parse.go | 10 +- 17 files changed, 817 insertions(+), 46 deletions(-) create mode 100644 libbeat/common/mapval/path_test.go create mode 100644 libbeat/common/x509util/x509util.go create mode 100644 libbeat/common/x509util/x509util_test.go diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 02f21fae3e6..4608b54a00d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -51,6 +51,8 @@ https://github.com/elastic/beats/compare/v6.4.0...master[Check the HEAD diff] *Heartbeat* +- Added support for extra TLS/x509 metadata. {pull}7944[7944] + *Metricbeat* - Fix golang.heap.gc.cpu_fraction type from long to float in Golang module. {pull}7789[7789] diff --git a/heartbeat/docs/fields.asciidoc b/heartbeat/docs/fields.asciidoc index 082b767d7ae..7dd438bd09a 100644 --- a/heartbeat/docs/fields.asciidoc +++ b/heartbeat/docs/fields.asciidoc @@ -898,6 +898,33 @@ TLS layer related fields. +*`tls.certificates`*:: ++ +-- +type: keyword + +List of PEM encoded x509 certificates. + +-- + +*`tls.certificate_not_valid_before`*:: ++ +-- +type: date + +Earliest time at which the connection's certificates are valid. + +-- + +*`tls.certificate_not_valid_after`*:: ++ +-- +type: date + +Latest time at which the connection's certificates are valid. + +-- + [float] == rtt fields diff --git a/heartbeat/hbtest/hbtestutil.go b/heartbeat/hbtest/hbtestutil.go index d6228b83f7e..6ec5801f1de 100644 --- a/heartbeat/hbtest/hbtestutil.go +++ b/heartbeat/hbtest/hbtestutil.go @@ -18,14 +18,20 @@ package hbtest import ( + "crypto/x509" "io" + "io/ioutil" "net/http" + "net/http/httptest" "net/url" + "os" "strconv" + "testing" - "net/http/httptest" + "github.com/stretchr/testify/require" "github.com/elastic/beats/libbeat/common/mapval" + "github.com/elastic/beats/libbeat/common/x509util" ) // HelloWorldBody is the body of the HelloWorldHandler. @@ -58,6 +64,19 @@ func ServerPort(server *httptest.Server) (uint16, error) { return uint16(p), nil } +// TLSChecks validates the given x509 cert at the given position. +func TLSChecks(chainIndex, certIndex int, certificate *x509.Certificate) mapval.Validator { + certPEMString := x509util.CertToPEMString(certificate) + return mapval.MustCompile(mapval.Map{ + "tls": mapval.Map{ + "rtt.handshake.us": mapval.IsDuration, + "certificates": mapval.Slice{certPEMString}, + "certificate_not_valid_before": certificate.NotBefore, + "certificate_not_valid_after": certificate.NotAfter, + }, + }) +} + // MonitorChecks creates a skima.Validator that represents the "monitor" field present // in all heartbeat events. func MonitorChecks(id string, host string, ip string, scheme string, status string) mapval.Validator { @@ -101,3 +120,17 @@ func RespondingTCPChecks(port uint16) mapval.Validator { mapval.MustCompile(mapval.Map{"tcp.rtt.connect.us": mapval.IsDuration}), ) } + +// CertToTempFile takes a certificate and returns an *os.File with a PEM encoded +// x.509 representation of that cert. Note that this takes tls.Certificate +// objects from a server like httptest. This doesn't take x509 certs. +// We never parse the x509 data in this case, we just transpose the bytes. +// This is a little confusing, but is actually less work and less code. +func CertToTempFile(t *testing.T, cert *x509.Certificate) *os.File { + // Write the certificate to a tempFile. Heartbeat would normally read certs from + // disk, not memory, so this little bit of extra work is worthwhile + certFile, err := ioutil.TempFile("", "sslcert") + require.NoError(t, err) + certFile.WriteString(x509util.CertToPEMString(cert)) + return certFile +} diff --git a/heartbeat/include/fields.go b/heartbeat/include/fields.go index 4faae76deef..4f615ffe661 100644 --- a/heartbeat/include/fields.go +++ b/heartbeat/include/fields.go @@ -31,5 +31,5 @@ func init() { // Asset returns asset data func Asset() string { - return "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" + return "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" } diff --git a/heartbeat/monitors/active/dialchain/_meta/fields.yml b/heartbeat/monitors/active/dialchain/_meta/fields.yml index f0e0a869c17..4a1fddb34af 100644 --- a/heartbeat/monitors/active/dialchain/_meta/fields.yml +++ b/heartbeat/monitors/active/dialchain/_meta/fields.yml @@ -32,6 +32,15 @@ description: > TLS layer related fields. fields: + - name: certificates + type: keyword + description: List of PEM encoded x509 certificates. + - name: certificate_not_valid_before + type: date + description: Earliest time at which the connection's certificates are valid. + - name: certificate_not_valid_after + type: date + description: Latest time at which the connection's certificates are valid. - name: rtt type: group description: > diff --git a/heartbeat/monitors/active/dialchain/tls.go b/heartbeat/monitors/active/dialchain/tls.go index 2add021b79c..255a950a79c 100644 --- a/heartbeat/monitors/active/dialchain/tls.go +++ b/heartbeat/monitors/active/dialchain/tls.go @@ -18,11 +18,14 @@ package dialchain import ( + cryptoTLS "crypto/tls" + "fmt" "net" "time" "github.com/elastic/beats/heartbeat/look" "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/common/x509util" "github.com/elastic/beats/libbeat/outputs/transport" ) @@ -49,10 +52,43 @@ func TLSLayer(cfg *transport.TLSConfig, to time.Duration) Layer { } return afterDial(dialer, func(conn net.Conn) (net.Conn, error) { - // TODO: extract TLS connection parameters from connection object. + tlsConn, ok := conn.(*cryptoTLS.Conn) + if !ok { + panic(fmt.Sprintf("TLS afterDial received a non-tls connection %t. This should never happen", conn)) + } + // TODO: extract TLS connection parameters from connection object. timer.stop() event.Put("tls.rtt.handshake", look.RTT(timer.duration())) + + var certs []string + + // Pointers because we need a nil value + var chainNotValidBefore *time.Time + var chainNotValidAfter *time.Time + + // Here we compute the minimal bounds during which this certificate chain is valid + // To do this correctly, we take the maximum NotBefore and the minimum NotAfter. + // This *should* always wind up being the terminal cert in the chain, but we should + // compute this correctly. + for _, chain := range tlsConn.ConnectionState().VerifiedChains { + for _, cert := range chain { + certs = append(certs, x509util.CertToPEMString(cert)) + + if chainNotValidBefore == nil || chainNotValidBefore.Before(cert.NotBefore) { + chainNotValidBefore = &cert.NotBefore + } + + if chainNotValidAfter == nil || chainNotValidAfter.After(cert.NotAfter) { + chainNotValidAfter = &cert.NotAfter + } + } + } + + event.Put("tls.certificate_not_valid_before", *chainNotValidBefore) + event.Put("tls.certificate_not_valid_after", *chainNotValidAfter) + event.Put("tls.certificates", certs) + return conn, nil }), nil } diff --git a/heartbeat/monitors/active/http/http_test.go b/heartbeat/monitors/active/http/http_test.go index b814cb49ecd..07645cb6461 100644 --- a/heartbeat/monitors/active/http/http_test.go +++ b/heartbeat/monitors/active/http/http_test.go @@ -18,9 +18,11 @@ package http import ( + "crypto/x509" "fmt" "net/http" "net/http/httptest" + "os" "testing" "github.com/stretchr/testify/require" @@ -34,9 +36,24 @@ import ( "github.com/elastic/beats/libbeat/testing/mapvaltest" ) -func testRequest(t *testing.T, url string) beat.Event { - config := common.NewConfig() - config.SetString("urls", 0, url) +func testRequest(t *testing.T, testURL string) beat.Event { + return testTLSRequest(t, testURL, "") +} + +// testTLSRequest tests the given request. certPath is optional, if given +// an empty string no cert will be set. +func testTLSRequest(t *testing.T, testURL string, certPath string) beat.Event { + configSrc := map[string]interface{}{ + "urls": testURL, + "timeout": "1s", + } + + if certPath != "" { + configSrc["ssl.certificate_authorities"] = certPath + } + + config, err := common.NewConfigFrom(configSrc) + require.NoError(t, err) jobs, err := create(monitors.Info{}, config) require.NoError(t, err) @@ -52,7 +69,6 @@ func testRequest(t *testing.T, url string) beat.Event { func checkServer(t *testing.T, handlerFunc http.HandlerFunc) (*httptest.Server, beat.Event) { server := httptest.NewServer(handlerFunc) defer server.Close() - event := testRequest(t, server.URL) return server, event @@ -195,6 +211,34 @@ func TestDownStatuses(t *testing.T) { } } +func TestHTTPSServer(t *testing.T) { + server := httptest.NewTLSServer(hbtest.HelloWorldHandler(http.StatusOK)) + port, err := hbtest.ServerPort(server) + require.NoError(t, err) + + // Parse the cert so we can test against it. + cert, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) + require.NoError(t, err) + + // Write the cert to a tempfile so heartbeat can use it in its config. + certFile := hbtest.CertToTempFile(t, cert) + require.NoError(t, certFile.Close()) + defer os.Remove(certFile.Name()) + + event := testTLSRequest(t, server.URL, certFile.Name()) + + mapvaltest.Test( + t, + mapval.Strict(mapval.Compose( + hbtest.MonitorChecks("http@"+server.URL, server.URL, "127.0.0.1", "https", "up"), + hbtest.RespondingTCPChecks(port), + hbtest.TLSChecks(0, 0, cert), + respondingHTTPChecks(server.URL, http.StatusOK), + )), + event.Fields, + ) +} + func TestConnRefusedJob(t *testing.T) { ip := "127.0.0.1" port, err := btesting.AvailableTCP4Port() diff --git a/heartbeat/monitors/active/tcp/tcp_test.go b/heartbeat/monitors/active/tcp/tcp_test.go index 73dcefb3a87..3a7df243798 100644 --- a/heartbeat/monitors/active/tcp/tcp_test.go +++ b/heartbeat/monitors/active/tcp/tcp_test.go @@ -18,14 +18,17 @@ package tcp import ( + "crypto/x509" "fmt" + "net" + "net/http" "net/http/httptest" + "net/url" + "os" "testing" "github.com/stretchr/testify/require" - "net/http" - "github.com/elastic/beats/heartbeat/hbtest" "github.com/elastic/beats/heartbeat/monitors" "github.com/elastic/beats/libbeat/beat" @@ -36,9 +39,32 @@ import ( ) func testTCPCheck(t *testing.T, host string, port uint16) *beat.Event { - config := common.NewConfig() - config.SetString("hosts", 0, host) - config.SetInt("ports", 0, int64(port)) + config, err := common.NewConfigFrom(common.MapStr{ + "hosts": host, + "ports": port, + "timeout": "1s", + }) + require.NoError(t, err) + + jobs, err := create(monitors.Info{}, config) + require.NoError(t, err) + + job := jobs[0] + + event, _, err := job.Run() + require.NoError(t, err) + + return &event +} + +func testTLSTCPCheck(t *testing.T, host string, port uint16, certFileName string) *beat.Event { + config, err := common.NewConfigFrom(common.MapStr{ + "hosts": host, + "ports": int64(port), + "ssl": common.MapStr{"certificate_authorities": certFileName}, + "timeout": "1s", + }) + require.NoError(t, err) jobs, err := create(monitors.Info{}, config) require.NoError(t, err) @@ -51,18 +77,24 @@ func testTCPCheck(t *testing.T, host string, port uint16) *beat.Event { return &event } +func setupServer(t *testing.T, serverCreator func(http.Handler) *httptest.Server) (*httptest.Server, uint16) { + server := serverCreator(hbtest.HelloWorldHandler(200)) + + port, err := hbtest.ServerPort(server) + require.NoError(t, err) + + return server, port +} + func tcpMonitorChecks(host string, ip string, port uint16, status string) mapval.Validator { id := fmt.Sprintf("tcp-tcp@%s:%d", host, port) return hbtest.MonitorChecks(id, host, ip, "tcp", status) } func TestUpEndpointJob(t *testing.T) { - server := httptest.NewServer(hbtest.HelloWorldHandler(http.StatusOK)) + server, port := setupServer(t, httptest.NewServer) defer server.Close() - port, err := hbtest.ServerPort(server) - require.NoError(t, err) - event := testTCPCheck(t, "localhost", port) mapvaltest.Test( @@ -88,6 +120,48 @@ func TestUpEndpointJob(t *testing.T) { ) } +func TestTLSConnection(t *testing.T) { + // Start up a TLS Server + server, port := setupServer(t, httptest.NewTLSServer) + defer server.Close() + + // Parse its URL + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + // Determine the IP address the server's hostname resolves to + ips, err := net.LookupHost(serverURL.Hostname()) + require.NoError(t, err) + require.Len(t, ips, 1) + ip := ips[0] + + // Parse the cert so we can test against it + cert, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) + require.NoError(t, err) + + // Save the server's cert to a file so heartbeat can use it + certFile := hbtest.CertToTempFile(t, cert) + require.NoError(t, certFile.Close()) + defer os.Remove(certFile.Name()) + + event := testTLSTCPCheck(t, ip, port, certFile.Name()) + mapvaltest.Test( + t, + mapval.Strict(mapval.Compose( + hbtest.TLSChecks(0, 0, cert), + hbtest.MonitorChecks( + fmt.Sprintf("tcp-ssl@%s:%d", ip, port), + serverURL.Hostname(), + ip, + "ssl", + "up", + ), + hbtest.RespondingTCPChecks(port), + )), + event.Fields, + ) +} + func TestConnectionRefusedEndpointJob(t *testing.T) { ip := "127.0.0.1" port, err := btesting.AvailableTCP4Port() diff --git a/libbeat/common/mapval/is_defs.go b/libbeat/common/mapval/is_defs.go index 714fa499c75..7421884b5dc 100644 --- a/libbeat/common/mapval/is_defs.go +++ b/libbeat/common/mapval/is_defs.go @@ -32,6 +32,104 @@ var KeyPresent = IsDef{name: "check key present"} // KeyMissing checks that the given key is not present defined. var KeyMissing = IsDef{name: "check key not present", checkKeyMissing: true} +func init() { + MustRegisterEqual(IsEqualToTime) +} + +// InvalidEqualFnError is the error type returned by RegisterEqual when +// there is an issue with the given function. +type InvalidEqualFnError struct{ msg string } + +func (e InvalidEqualFnError) Error() string { + return fmt.Sprintf("Function is not a valid equal function: %s", e.msg) +} + +// MustRegisterEqual is the panic-ing equivalent of RegisterEqual. +func MustRegisterEqual(fn interface{}) { + if err := RegisterEqual(fn); err != nil { + panic(fmt.Sprintf("Could not register fn as equal! %v", err)) + } +} + +var equalChecks = map[reflect.Type]reflect.Value{} + +// RegisterEqual takes a function of the form fn(v someType) IsDef +// and registers it to check equality for that type. +func RegisterEqual(fn interface{}) error { + fnV := reflect.ValueOf(fn) + fnT := fnV.Type() + + if fnT.Kind() != reflect.Func { + return InvalidEqualFnError{"Provided value is not a function"} + } + if fnT.NumIn() != 1 { + return InvalidEqualFnError{"Equal FN should take one argument"} + } + if fnT.NumOut() != 1 { + return InvalidEqualFnError{"Equal FN should return one value"} + } + if fnT.Out(0) != reflect.TypeOf(IsDef{}) { + return InvalidEqualFnError{"Equal FN should return an IsDef"} + } + + inT := fnT.In(0) + if _, ok := equalChecks[inT]; ok { + return InvalidEqualFnError{fmt.Sprintf("Duplicate Equal FN for type %v encountered!", inT)} + } + + equalChecks[inT] = fnV + + return nil +} + +// IsEqual tests that the given object is equal to the actual object. +func IsEqual(to interface{}) IsDef { + toV := reflect.ValueOf(to) + isDefFactory, ok := equalChecks[toV.Type()] + + // If there are no handlers declared explicitly for this type we perform a deep equality check + if !ok { + return IsDeepEqual(to) + } + + // We know this is an isdef due to the Register check previously + checker := isDefFactory.Call([]reflect.Value{toV})[0].Interface().(IsDef).checker + + return Is("equals", func(path Path, v interface{}) *Results { + return checker(path, v) + }) +} + +// IsEqualToTime ensures that the actual value is the given time, regardless of zone. +func IsEqualToTime(to time.Time) IsDef { + return Is("equal to time", func(path Path, v interface{}) *Results { + actualTime, ok := v.(time.Time) + if !ok { + return SimpleResult(path, false, "Value %t was not a time.Time", v) + } + + if actualTime.Equal(to) { + return ValidResult(path) + } + + return SimpleResult(path, false, "actual(%v) != expected(%v)", actualTime, to) + }) +} + +// IsDeepEqual checks equality using reflect.DeepEqual. +func IsDeepEqual(to interface{}) IsDef { + return Is("equals", func(path Path, v interface{}) *Results { + if reflect.DeepEqual(v, to) { + return ValidResult(path) + } + return SimpleResult( + path, + false, + fmt.Sprintf("objects not equal: actual(%v) != expected(%v)", v, to), + ) + }) +} + // IsArrayOf validates that the array at the given key is an array of objects all validatable // via the given Validator. func IsArrayOf(validator Validator) IsDef { @@ -115,20 +213,6 @@ var IsDuration = Is("is a duration", func(path Path, v interface{}) *Results { ) }) -// IsEqual tests that the given object is equal to the actual object. -func IsEqual(to interface{}) IsDef { - return Is("equals", func(path Path, v interface{}) *Results { - if reflect.DeepEqual(v, to) { - return ValidResult(path) - } - return SimpleResult( - path, - false, - fmt.Sprintf("objects not equal: actual(%v) != expected(%v)", v, to), - ) - }) -} - // IsNil tests that a value is nil. var IsNil = Is("is nil", func(path Path, v interface{}) *Results { if v == nil { diff --git a/libbeat/common/mapval/is_defs_test.go b/libbeat/common/mapval/is_defs_test.go index 99ab0c41851..7529a569574 100644 --- a/libbeat/common/mapval/is_defs_test.go +++ b/libbeat/common/mapval/is_defs_test.go @@ -92,6 +92,16 @@ func TestIsEqual(t *testing.T) { assertIsDefInvalid(t, id, "bar") } +func TestRegisteredIsEqual(t *testing.T) { + // Time equality comes from a registered function + // so this is a quick way to test registered functions + now := time.Now() + id := IsEqual(now) + + assertIsDefValid(t, id, now) + assertIsDefInvalid(t, id, now.Add(100)) +} + func TestIsStringContaining(t *testing.T) { id := IsStringContaining("foo") diff --git a/libbeat/common/mapval/path.go b/libbeat/common/mapval/path.go index 32dbaf45863..d1fc92c678a 100644 --- a/libbeat/common/mapval/path.go +++ b/libbeat/common/mapval/path.go @@ -32,11 +32,23 @@ type PathComponentType int const ( // PCMapKey is the Type for map keys. - PCMapKey = iota + PCMapKey PathComponentType = 1 + iota // PCSliceIdx is the Type for slice indices. PCSliceIdx ) +func (pct PathComponentType) String() string { + if pct == PCMapKey { + return "map" + } else if pct == PCSliceIdx { + return "slice" + } else { + // This should never happen, but we don't want to return an + // error since that would unnecessarily complicate the fluid API + return "" + } +} + // PathComponent structs represent one breadcrumb in a Path. type PathComponent struct { Type PathComponentType // One of PCMapKey or PCSliceIdx @@ -90,9 +102,14 @@ func (p Path) String() string { return strings.Join(out, ".") } -// Last returns the last PathComponent in this Path. -func (p Path) Last() PathComponent { - return p[len(p)-1] +// Last returns a pointer to the last PathComponent in this path. If the path empty, +// a nil pointer is returned. +func (p Path) Last() *PathComponent { + idx := len(p) - 1 + if idx < 0 { + return nil + } + return &p[len(p)-1] } // GetFrom takes a map and fetches the given path from it. @@ -100,7 +117,8 @@ func (p Path) GetFrom(m common.MapStr) (value interface{}, exists bool) { value = m exists = true for _, pc := range p { - switch reflect.TypeOf(value).Kind() { + rt := reflect.TypeOf(value) + switch rt.Kind() { case reflect.Map: converted := interfaceToMapStr(value) value, exists = converted[pc.Key] @@ -114,7 +132,11 @@ func (p Path) GetFrom(m common.MapStr) (value interface{}, exists bool) { value = nil } default: - panic("Unexpected type") + // If this case has been reached this means the expected type, say a map, + // is actually something else, like a string or an array. In this case we + // simply say the value doesn't exist. From a practical perspective this is + // the right behavior since it will cause validation to fail. + return nil, false } if exists == false { @@ -141,7 +163,7 @@ func ParsePath(in string) (p Path, err error) { p = make(Path, len(keyParts)) for idx, part := range keyParts { r := arrMatcher.FindStringSubmatch(part) - pc := PathComponent{} + pc := PathComponent{Index: -1} if len(r) > 0 { pc.Type = PCSliceIdx // Cannot fail, validated by regexp already diff --git a/libbeat/common/mapval/path_test.go b/libbeat/common/mapval/path_test.go new file mode 100644 index 00000000000..5ee28da10d0 --- /dev/null +++ b/libbeat/common/mapval/path_test.go @@ -0,0 +1,324 @@ +// 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 mapval + +import ( + "reflect" + "testing" + + "github.com/elastic/beats/libbeat/common" +) + +func TestPathComponentType_String(t *testing.T) { + tests := []struct { + name string + pct PathComponentType + want string + }{ + { + "Should return the correct type", + PCMapKey, + "map", + }, + { + "Should return the correct type", + PCSliceIdx, + "slice", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.pct.String(); got != tt.want { + t.Errorf("PathComponentType.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPathComponent_String(t *testing.T) { + type fields struct { + Type PathComponentType + Key string + Index int + } + tests := []struct { + name string + fields fields + want string + }{ + { + "Map key should return a literal", + fields{PCMapKey, "foo", 0}, + "foo", + }, + { + "Array index should return a bracketed number", + fields{PCSliceIdx, "", 123}, + "[123]", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pc := PathComponent{ + Type: tt.fields.Type, + Key: tt.fields.Key, + Index: tt.fields.Index, + } + if got := pc.String(); got != tt.want { + t.Errorf("PathComponent.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_ExtendSlice(t *testing.T) { + type args struct { + index int + } + tests := []struct { + name string + p Path + args args + want Path + }{ + { + "Extending an empty slice", + Path{}, + args{123}, + Path{PathComponent{PCSliceIdx, "", 123}}, + }, + { + "Extending a non-empty slice", + Path{PathComponent{PCMapKey, "foo", -1}}, + args{123}, + Path{PathComponent{PCMapKey, "foo", -1}, PathComponent{PCSliceIdx, "", 123}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.ExtendSlice(tt.args.index); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Path.ExtendSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_ExtendMap(t *testing.T) { + type args struct { + key string + } + tests := []struct { + name string + p Path + args args + want Path + }{ + { + "Extending an empty slice", + Path{}, + args{"foo"}, + Path{PathComponent{PCMapKey, "foo", -1}}, + }, + { + "Extending a non-empty slice", + Path{}.ExtendMap("foo"), + args{"bar"}, + Path{PathComponent{PCMapKey, "foo", -1}, PathComponent{PCMapKey, "bar", -1}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.ExtendMap(tt.args.key); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Path.ExtendMap() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_Concat(t *testing.T) { + tests := []struct { + name string + p Path + arg Path + want Path + }{ + { + "simple", + Path{}.ExtendMap("foo"), + Path{}.ExtendSlice(123), + Path{}.ExtendMap("foo").ExtendSlice(123), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.Concat(tt.arg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Path.Concat() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_String(t *testing.T) { + tests := []struct { + name string + p Path + want string + }{ + { + "empty", + Path{}, + "", + }, + { + "one element", + Path{}.ExtendMap("foo"), + "foo", + }, + { + "complex", + Path{}.ExtendMap("foo").ExtendSlice(123).ExtendMap("bar"), + "foo.[123].bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.String(); got != tt.want { + t.Errorf("Path.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_Last(t *testing.T) { + tests := []struct { + name string + p Path + want *PathComponent + }{ + { + "empty path", + Path{}, + nil, + }, + { + "one element", + Path{}.ExtendMap("foo"), + &PathComponent{PCMapKey, "foo", -1}, + }, + { + "many elements", + Path{}.ExtendMap("foo").ExtendMap("bar").ExtendSlice(123), + &PathComponent{PCSliceIdx, "", 123}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.Last(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Path.Last() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPath_GetFrom(t *testing.T) { + fooPath := Path{}.ExtendMap("foo") + complexPath := Path{}.ExtendMap("foo").ExtendSlice(0).ExtendMap("bar").ExtendSlice(1) + tests := []struct { + name string + p Path + arg common.MapStr + wantValue interface{} + wantExists bool + }{ + { + "simple present", + fooPath, + common.MapStr{"foo": "bar"}, + "bar", + true, + }, + { + "simple missing", + fooPath, + common.MapStr{}, + nil, + false, + }, + { + "complex present", + complexPath, + common.MapStr{"foo": []interface{}{common.MapStr{"bar": []string{"bad", "good"}}}}, + "good", + true, + }, + { + "complex missing", + complexPath, + common.MapStr{}, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotExists := tt.p.GetFrom(tt.arg) + if !reflect.DeepEqual(gotValue, tt.wantValue) { + t.Errorf("Path.GetFrom() gotValue = %v, want %v", gotValue, tt.wantValue) + } + if gotExists != tt.wantExists { + t.Errorf("Path.GetFrom() gotExists = %v, want %v", gotExists, tt.wantExists) + } + }) + } +} + +func TestParsePath(t *testing.T) { + tests := []struct { + name string + arg string + wantP Path + wantErr bool + }{ + { + "simple", + "foo", + Path{}.ExtendMap("foo"), + false, + }, + { + "complex", + "foo.[0].bar.[1].baz", + Path{}.ExtendMap("foo").ExtendSlice(0).ExtendMap("bar").ExtendSlice(1).ExtendMap("baz"), + false, + }, + // TODO: The validation and testing for this needs to be better + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotP, err := ParsePath(tt.arg) + if (err != nil) != tt.wantErr { + t.Errorf("ParsePath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotP, tt.wantP) { + t.Errorf("ParsePath() = %v, want %v", gotP, tt.wantP) + } + }) + } +} diff --git a/libbeat/common/mapval/results.go b/libbeat/common/mapval/results.go index e6dd7e06841..4b20504cf5d 100644 --- a/libbeat/common/mapval/results.go +++ b/libbeat/common/mapval/results.go @@ -38,8 +38,8 @@ func NewResults() *Results { // SimpleResult provides a convenient and simple method for creating a *Results object for a single validation. // It's a very common way for validators to return a *Results object, and is generally simpler than // using SingleResult. -func SimpleResult(path Path, valid bool, msg string) *Results { - vr := ValueResult{valid, msg} +func SimpleResult(path Path, valid bool, msg string, args ...interface{}) *Results { + vr := ValueResult{valid, fmt.Sprintf(msg, args...)} return SingleResult(path, vr) } diff --git a/libbeat/common/mapval/walk.go b/libbeat/common/mapval/walk.go index 1f49ca2149d..118acc8f645 100644 --- a/libbeat/common/mapval/walk.go +++ b/libbeat/common/mapval/walk.go @@ -39,7 +39,12 @@ func walk(m common.MapStr, expandPaths bool, wo walkObserver) error { } func walkFull(o interface{}, root common.MapStr, path Path, expandPaths bool, wo walkObserver) (err error) { - err = wo(walkObserverInfo{path.Last(), o, root, path}) + lastPathComponent := path.Last() + if lastPathComponent == nil { + panic("Attempted to traverse an empty path in mapval.walkFull, this should never happen.") + } + + err = wo(walkObserverInfo{*lastPathComponent, o, root, path}) if err != nil { return err } diff --git a/libbeat/common/x509util/x509util.go b/libbeat/common/x509util/x509util.go new file mode 100644 index 00000000000..162d5e119fe --- /dev/null +++ b/libbeat/common/x509util/x509util.go @@ -0,0 +1,32 @@ +// 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 x509util + +import ( + "crypto/x509" + "encoding/pem" +) + +// CertToPEMString taxes an x509 cert and returns a PEM encoded string version. +func CertToPEMString(cert *x509.Certificate) string { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + return string(pem.EncodeToMemory(&block)) +} diff --git a/libbeat/common/x509util/x509util_test.go b/libbeat/common/x509util/x509util_test.go new file mode 100644 index 00000000000..285b23e1777 --- /dev/null +++ b/libbeat/common/x509util/x509util_test.go @@ -0,0 +1,71 @@ +// 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 x509util + +import ( + "crypto/x509" + "testing" + + "encoding/pem" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var certPem = `-----BEGIN CERTIFICATE----- +MIIFTDCCAzSgAwIBAgIRAOAMlgVxz4G+Zj/EtBTvpg4wDQYJKoZIhvcNAQENBQAw +LzELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB2VsYXN0aWMxDjAMBgNVBAsTBWJlYXRz +MB4XDTE3MDUxODIwMzI1MVoXDTI3MDUxODIwMzI1MVowLzELMAkGA1UEBhMCVVMx +EDAOBgNVBAoTB2VsYXN0aWMxDjAMBgNVBAsTBWJlYXRzMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAv8IiJDAIDl+roQOWe+oSq46Nyuu9R+Iis0V1i6M7 +zA6QijbxCSZ64cCFYQfKheRYQSZRstHPHSUM1gSvUih/sqZqsiNMYDbb9j7geMDv +ls4c7rsHx7xImD7nCrEVWkiapGIhkW6SOtVo18Zmw89FUuDFhoRmMHcQ+7AtM4uU +NPkSqKcXvzG093SU0oNdIBdw5PzoQlvBh5DL0iRYC6y22cwJyjWTUEB5vTjOTDxi +FzsovRtjpdjzSZACXyW68b99icLzmxzLvsZ7w8tFJ8uOPQAVxwg6SmMUorURv48s +BjfVfN487OjH3d+51ozNJjP1MmKoN2BoE8pWq0jdhOWhDQH+pRiRjfMuL+yvcIJ2 +pxdOv0F3KBkng7qEgEUA8cqaFnawDA7O3a20SeDFWSQtN6LsFjT7EDMzNkML1pJj +bGK24QFCIOOvCJtaccuREN1OfbN1yhTz3VErbJttwO6j2KueasPHXU3qLu2FKOls +XbPy1XMuLYZgv8Zprcbs4KhQ3/A7/RO1cakxWlRwta63mUIM2xLIMIgRSR+DSZ5d +JaDNO6i49eIGQXRxDb9dxA2hoCcoTv7PJKyOpNb5vyxMXJGY7H5j1jEEcqEeuI5u +vuUwugQGtsl1eFLXIeQLerOHEQoS6wMv0fHBtZOVCHu8CCrnt/ag7kn39nkwNofL +ovECAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMC +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDgQHBAUxMjM0NTAPBgNV +HREECDAGhwR/AAABMA0GCSqGSIb3DQEBDQUAA4ICAQBjeGIfFqXuwHiClMytJNZL +cRyjeZ6PJIAQtqh8Vi+XD2JiDTkwJ/g4R0FbgqE/icGkm/hsJ6BEwp8ep5eXevjS +Hb8tVbM5Uc31yyIKcJMgnfS8O0eIXi5PxgFWPcUXxrsjwHyQREqj96HImmzOm99O +MJhifWT3YP8OEMyl1KpioPaXafhc4ATEiRVZizHM9z+phyINBNghH3OaN91ZnsKJ +El7mvOLjRi7fuSxBWJntKVAZAwXK+nH+z/Ay4AZFA9HgFHo3PGpKUaLOYCIsGxAq +GP4V/WsOtEJ9rP5TR92pOvcj49T47FmwSYaRtoXHDVuoun0fdwT4DxWJdksqdWzG +ieRls2IrZIvR2FT/A/XdQG3kZ79WA/K3OAGDgxv0PCpw6ssAMvgjR03TjEXpwMmN +SNcrx1H6l8DHFHJN9f7SofO/J0hkA+fRZUFxP5R+P2BPU0hV14H9iSie/bxhSWIW +ieAh0K1SNRbffXeYUvAgrjEvG5x40TktnvjHb20lxc1F1gqB+855kfZdiJeUeizi +syq6OnCEp+RSBdK7J3scm7t6Nt3GRndJMO9hNDprogTqHxQbZ0jficntGd7Lbp+C +CBegkhOzD6cp2rGlyYI+MmvdXFaHbsUJj2tfjHQdo2YjQ1s8r2pw219LTzPvO/Dz +morZ618ezCBBqxHsDF6DCA== +-----END CERTIFICATE----- +` + +func TestCertToPEMString(t *testing.T) { + block, _ := pem.Decode([]byte(certPem)) + require.NotNil(t, block) + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + assert.Equal(t, certPem, CertToPEMString(cert)) +} diff --git a/packetbeat/protos/tls/parse.go b/packetbeat/protos/tls/parse.go index 98083de89b3..f90b2295370 100644 --- a/packetbeat/protos/tls/parse.go +++ b/packetbeat/protos/tls/parse.go @@ -24,12 +24,12 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/hex" - "encoding/pem" "fmt" "strings" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/streambuf" + "github.com/elastic/beats/libbeat/common/x509util" "github.com/elastic/beats/libbeat/logp" ) @@ -543,6 +543,8 @@ func getKeySize(key interface{}) int { return 0 } +// certToMap takes an x509 cert and converts it into a map. If includeRaw is set +// to true a PEM encoded copy of the cert is encoded into the map as well. func certToMap(cert *x509.Certificate, includeRaw bool) common.MapStr { certMap := common.MapStr{ "signature_algorithm": cert.SignatureAlgorithm.String(), @@ -566,11 +568,7 @@ func certToMap(cert *x509.Certificate, includeRaw bool) common.MapStr { certMap["alternative_names"] = san } if includeRaw { - block := pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - } - certMap["raw"] = string(pem.EncodeToMemory(&block)) + certMap["raw"] = x509util.CertToPEMString(cert) } return certMap }