diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 797db99b7ca..8eb756e4bdf 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -111,6 +111,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Set timeout of 1 minute for FQDN requests {pull}37756[37756] - Fix issue where old data could be saved in the memory queue after acknowledgment, increasing memory use {pull}41356[41356] - Ensure Elasticsearch output can always recover from network errors {pull}40794[40794] +- Add `translate_guid` processor for windows platforms. {pull}41472[41472] *Auditbeat* diff --git a/libbeat/docs/processors-list.asciidoc b/libbeat/docs/processors-list.asciidoc index 4105666049d..63985fd8f5a 100644 --- a/libbeat/docs/processors-list.asciidoc +++ b/libbeat/docs/processors-list.asciidoc @@ -131,6 +131,9 @@ endif::[] ifndef::no_timestamp_processor[] * <> endif::[] +ifndef::no_translate_guid_processor[] +* <> +endif::[] ifndef::no_translate_sid_processor[] * <> endif::[] @@ -279,6 +282,9 @@ endif::[] ifndef::no_timestamp_processor[] include::{libbeat-processors-dir}/timestamp/docs/timestamp.asciidoc[] endif::[] +ifndef::no_translate_guid_processor[] +include::{libbeat-processors-dir}/translate_guid/docs/translate_guid.asciidoc[] +endif::[] ifndef::no_translate_sid_processor[] include::{libbeat-processors-dir}/translate_sid/docs/translate_sid.asciidoc[] endif::[] diff --git a/libbeat/processors/translate_guid/config.go b/libbeat/processors/translate_guid/config.go new file mode 100644 index 00000000000..51b12fec2b0 --- /dev/null +++ b/libbeat/processors/translate_guid/config.go @@ -0,0 +1,40 @@ +// 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 translate_guid + +import ( + "github.com/elastic/elastic-agent-libs/transport/tlscommon" +) + +type config struct { + Field string `config:"field" validate:"required"` + TargetField string `config:"target_field"` + LDAPAddress string `config:"ldap_address" validate:"required"` + LDAPBaseDN string `config:"ldap_base_dn" validate:"required"` + LDAPUser string `config:"ldap_user"` + LDAPPassword string `config:"ldap_password"` + LDAPSearchTimeLimit int `config:"ldap_search_time_limit"` + LDAPTLS *tlscommon.Config `config:"ldap_ssl"` + + IgnoreMissing bool `config:"ignore_missing"` + IgnoreFailure bool `config:"ignore_failure"` +} + +func defaultConfig() config { + return config{LDAPSearchTimeLimit: 30} +} diff --git a/libbeat/processors/translate_guid/doc.go b/libbeat/processors/translate_guid/doc.go new file mode 100644 index 00000000000..b0d4ec27efb --- /dev/null +++ b/libbeat/processors/translate_guid/doc.go @@ -0,0 +1,20 @@ +// 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 translate_guid provides a Beat processor for converting Windows +// Global Unique Identifiers (GUIDs) to object names. +package translate_guid diff --git a/libbeat/processors/translate_guid/docs/translate_guid.asciidoc b/libbeat/processors/translate_guid/docs/translate_guid.asciidoc new file mode 100644 index 00000000000..7405dd7765b --- /dev/null +++ b/libbeat/processors/translate_guid/docs/translate_guid.asciidoc @@ -0,0 +1,48 @@ +[[processor-translate-guid]] +=== Translate GUID + +++++ +translate_guid +++++ + +The `translate_guid` processor translates an LDAP Global Unique Identifier (GUID) +into its common name. + +Every object on an Active Directory is issued a GUID. Internal processes +refer to their GUID's rather than the object's name and these values +sometimes appear in logs. + +If the GUID is invalid (malformed) or does not map to any object on the domain +then this will result in the processor returning an error unless `ignore_failure` +is set. + +[source,yaml] +---- +processors: + - translate_guid: + field: winlog.event_data.ObjectGuid + ldap_address: "ldap://" + ldap_base_dn: "dc=example,dc=com" + ignore_missing: true + ignore_failure: true +---- + +The `translate_guid` processor has the following configuration settings: + +.Translate GUID options +[options="header"] +|====== +| Name | Required | Default | Description +| `field` | yes | | Source field containing a GUID. +| `target_field` | no | | Target field for the common name. If not set it will be replaced in place. +| `ldap_address` | yes | | LDAP server address. eg: `ldap://ds.example.com:389` +| `ldap_base_dn` | yes | | LDAP base DN. eg: `dc=example,dc=com` +| `ldap_user` | no | | LDAP user. +| `ldap_password` | no | | LDAP password. +| `ldap_search_time_limit` | no | 30 | LDAP search time limit in seconds. +| `ldap_ssl`* | no | 30 | LDAP TLS/SSL connection settings. +| `ignore_missing` | no | false | Ignore errors when the source field is missing. +| `ignore_failure` | no | false | Ignore all errors produced by the processor. +|====== + +* Also see <> for a full description of the `ldap_ssl` options. diff --git a/libbeat/processors/translate_guid/ldap.go b/libbeat/processors/translate_guid/ldap.go new file mode 100644 index 00000000000..8ffb351f7c1 --- /dev/null +++ b/libbeat/processors/translate_guid/ldap.go @@ -0,0 +1,145 @@ +// 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 translate_guid + +import ( + "crypto/tls" + "fmt" + "strings" + "sync" + + "github.com/go-ldap/ldap/v3" +) + +// ldapClient manages a single reusable LDAP connection +type ldapClient struct { + conn *ldap.Conn + mu sync.Mutex + *ldapConfig +} + +type ldapConfig struct { + address string + baseDN string + username string + password string + searchTimeLimit int + tlsConfig *tls.Config +} + +// newLDAPClient initializes a new ldapClient with a single connection +func newLDAPClient(config *ldapConfig) (*ldapClient, error) { + client := &ldapClient{ldapConfig: config} + + // Establish initial connection + if err := client.connect(); err != nil { + return nil, err + } + + return client, nil +} + +// connect establishes a new connection to the LDAP server +func (client *ldapClient) connect() error { + client.mu.Lock() + defer client.mu.Unlock() + + // Connect with or without TLS based on configuration + var conn *ldap.Conn + var err error + if client.tlsConfig != nil { + conn, err = ldap.DialTLS("tcp", client.address, client.tlsConfig) + } else { + conn, err = ldap.Dial("tcp", client.address) + } + if err != nil { + return fmt.Errorf("failed to dial LDAP server: %v", err) + } + + if client.password != "" { + err = conn.Bind(client.username, client.password) + } else { + err = conn.UnauthenticatedBind(client.username) + } + + if err != nil { + conn.Close() + return fmt.Errorf("failed to bind to LDAP server: %v", err) + } + + client.conn = conn + return nil +} + +// reconnect checks the connection's health and reconnects if necessary +func (client *ldapClient) reconnect() error { + client.mu.Lock() + defer client.mu.Unlock() + + // Check if the connection is still alive + if client.conn.IsClosing() { + return client.connect() + } + return nil +} + +// findObjectByGUID searches for an AD object by GUID and returns its Common Name (CN) +func (client *ldapClient) findObjectByGUID(objectGUID string) (string, error) { + // Ensure the connection is alive or reconnect if necessary + if err := client.reconnect(); err != nil { + return "", fmt.Errorf("failed to reconnect: %v", err) + } + + client.mu.Lock() + defer client.mu.Unlock() + + // Format the GUID filter and perform the search + filter := fmt.Sprintf("(objectGUID=%s)", encodeGUID(objectGUID)) + searchRequest := ldap.NewSearchRequest( + client.baseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, client.searchTimeLimit, false, + filter, []string{"cn"}, nil, + ) + + // Execute search + result, err := client.conn.Search(searchRequest) + if err != nil { + return "", fmt.Errorf("search failed: %v", err) + } + if len(result.Entries) == 0 { + return "", fmt.Errorf("no entries found for GUID %s", objectGUID) + } + + // Retrieve the CN attribute + cn := result.Entries[0].GetAttributeValue("cn") + return cn, nil +} + +// encodeGUID converts a GUID into LDAP filter format +func encodeGUID(guid string) string { + return fmt.Sprintf("\\%s", strings.Trim(guid, "{}")) +} + +// close closes the LDAP connection +func (client *ldapClient) close() { + client.mu.Lock() + defer client.mu.Unlock() + if client.conn != nil { + client.conn.Close() + } +} diff --git a/libbeat/processors/translate_guid/translateguid.go b/libbeat/processors/translate_guid/translateguid.go new file mode 100644 index 00000000000..42816e46dbb --- /dev/null +++ b/libbeat/processors/translate_guid/translateguid.go @@ -0,0 +1,126 @@ +// 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 translate_guid + +import ( + "errors" + "fmt" + + "github.com/elastic/beats/v7/libbeat/beat" + "github.com/elastic/beats/v7/libbeat/processors" + jsprocessor "github.com/elastic/beats/v7/libbeat/processors/script/javascript/module/processor" + conf "github.com/elastic/elastic-agent-libs/config" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" + "github.com/elastic/elastic-agent-libs/transport/tlscommon" +) + +const logName = "processor.translate_guid" + +var errInvalidType = errors.New("GUID field value is not a string") + +func init() { + processors.RegisterPlugin("translate_guid", New) + jsprocessor.RegisterPlugin("TranslateGUID", New) +} + +type processor struct { + config + client *ldapClient + log *logp.Logger +} + +// New returns a new translate_guid processor for converting windows GUID values +// to object names. +func New(cfg *conf.C) (beat.Processor, error) { + c := defaultConfig() + if err := cfg.Unpack(&c); err != nil { + return nil, fmt.Errorf("fail to unpack the translate_guid configuration: %w", err) + } + + return newFromConfig(c) +} + +func newFromConfig(c config) (*processor, error) { + ldapConfig := &ldapConfig{ + address: c.LDAPAddress, + baseDN: c.LDAPBaseDN, + username: c.LDAPUser, + password: c.LDAPPassword, + searchTimeLimit: c.LDAPSearchTimeLimit, + } + if c.LDAPTLS != nil { + tlsConfig, err := tlscommon.LoadTLSConfig(c.LDAPTLS) + if err != nil { + return nil, fmt.Errorf("could not load provided LDAP TLS configuration: %w", err) + } + ldapConfig.tlsConfig = tlsConfig.ToConfig() + } + client, err := newLDAPClient(ldapConfig) + if err != nil { + return nil, err + } + return &processor{ + config: c, + client: client, + log: logp.NewLogger(logName), + }, nil +} + +func (p *processor) String() string { + return fmt.Sprintf("translate_guid=[field=%s, ldap_address=%s, ldap_base_dn=%s, ldap_user=%s]", + p.Field, p.LDAPAddress, p.LDAPBaseDN, p.LDAPUser) +} + +func (p *processor) Run(event *beat.Event) (*beat.Event, error) { + err := p.translateGUID(event) + if err == nil || p.IgnoreFailure || (p.IgnoreMissing && errors.Is(err, mapstr.ErrKeyNotFound)) { + return event, nil + } + return event, err +} + +func (p *processor) translateGUID(event *beat.Event) error { + v, err := event.GetValue(p.Field) + if err != nil { + return err + } + + guidString, ok := v.(string) + if !ok { + return errInvalidType + } + + // XXX: May want to introduce an in-memory cache if the lookups are time consuming. + cn, err := p.client.findObjectByGUID(guidString) + if err != nil { + return err + } + + field := p.Field + if p.TargetField != "" { + field = p.TargetField + } + _, err = event.PutValue(field, cn) + return err +} + +func (p *processor) Close() error { + p.client.close() + return nil +}