From 81eccfa2ac31bd243b4fde2d8f283a9a3d913d9e 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 This backport updates heartbeat/include/fields.go which had a conflict via make update --- 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 a1693983e5d..5c0ca102716 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -47,6 +47,8 @@ https://github.com/elastic/beats/compare/v6.4.0...6.x[Check the HEAD diff] *Heartbeat* +- Added support for extra TLS/x509 metadata. {pull}7944[7944] + *Metricbeat* *Packetbeat* diff --git a/heartbeat/docs/fields.asciidoc b/heartbeat/docs/fields.asciidoc index 03144a26ed0..a489149f7d8 100644 --- a/heartbeat/docs/fields.asciidoc +++ b/heartbeat/docs/fields.asciidoc @@ -888,6 +888,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 4392f7a0f3f..e568a2f7057 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 }