Skip to content

Commit

Permalink
[libbeat] Add translate_ldap_attribute processor (#41472) (#41536)
Browse files Browse the repository at this point in the history
* Add translate_guid processor

* Add cache to docs

* Add more config options

* Add integration test for translate_guid processor

* Rename processor

* Fix reference to use new processor name

* Rename field in test

* Add codeowners, and various format fixes

* Fix test lint issues

* Update libbeat/processors/translate_ldap_attribute/docs/translate_ldap_attribute.asciidoc

(cherry picked from commit 87687a5)

Co-authored-by: Marc Guasch <[email protected]>
  • Loading branch information
mergify[bot] and marc-gr authored Nov 6, 2024
1 parent fb02b50 commit 95408b3
Show file tree
Hide file tree
Showing 10 changed files with 647 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ CHANGELOG*
/libbeat/processors/dns/ @elastic/sec-deployment-and-devices
/libbeat/processors/registered_domain/ @elastic/sec-deployment-and-devices
/libbeat/processors/syslog/ @elastic/sec-deployment-and-devices
/libbeat/processors/translate_ldap_attribute/ @elastic/sec-windows-platform
/libbeat/processors/translate_sid/ @elastic/sec-windows-platform
/libbeat/reader/syslog/ @elastic/sec-deployment-and-devices
/libbeat/scripts @elastic/ingest-eng-prod
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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_ldap_attribute` processor. {pull}41472[41472]

*Auditbeat*

Expand Down
216 changes: 216 additions & 0 deletions filebeat/tests/integration/translate_ldap_attribute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// 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.

//go:build integration

package integration

import (
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"testing"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/go-ldap/ldap/v3"
"github.com/stretchr/testify/require"

"github.com/elastic/beats/v7/libbeat/tests/integration"
"github.com/elastic/elastic-agent-autodiscover/docker"
)

const translateguidCfg = `
filebeat.inputs:
- type: filestream
id: "test-translateguidCfg"
paths:
- %s
queue.mem:
flush.min_events: 1
flush.timeout: 0.1s
path.home: %s
output.file:
path: ${path.home}
filename: "output-file"
logging:
metrics:
enabled: false
processors:
- add_fields:
fields:
guid: '%s'
- translate_ldap_attribute:
field: fields.guid
target_field: fields.common_name
ldap_address: 'ldap://localhost:1389'
ldap_base_dn: 'dc=example,dc=org'
ldap_bind_user: 'cn=admin,dc=example,dc=org'
ldap_bind_password: 'adminpassword'
ldap_search_attribute: 'entryUUID'
`

func TestTranslateGUIDWithLDAP(t *testing.T) {
startOpenldapContainer(t)

var entryUUID string
require.Eventually(t, func() bool {
var err error
entryUUID, err = getLDAPUserEntryUUID()
return err == nil
}, 10*time.Second, time.Second)

filebeat := integration.NewBeat(
t,
"filebeat",
"../../filebeat.test",
)
tempDir := filebeat.TempDir()

// 1. Generate the log file path
logFilePath := path.Join(tempDir, "log.log")
integration.GenerateLogFile(t, logFilePath, 1, false)

// 2. Write configuration file and start Filebeat
filebeat.WriteConfigFile(
fmt.Sprintf(translateguidCfg, logFilePath, tempDir, entryUUID),
)
filebeat.Start()

var outputFile string
require.Eventually(t, func() bool {
outputFiles, err := filepath.Glob(path.Join(tempDir, "output-file-*.ndjson"))
if err != nil {
return false
}
if len(outputFiles) != 1 {
return false
}
outputFile = outputFiles[0]
return true
}, 10*time.Second, time.Second)

// 3. Wait for the event with the expected translated guid
filebeat.WaitFileContains(
outputFile,
fmt.Sprintf(`"fields":{"guid":"%s","common_name":["User1","user01"]}`, entryUUID),
10*time.Second,
)
}

func startOpenldapContainer(t *testing.T) {
ctx := context.Background()
c, err := docker.NewClient(client.DefaultDockerHost, nil, nil)
if err != nil {
t.Fatal(err)
}

reader, err := c.ImagePull(ctx, "bitnami/openldap:2", image.PullOptions{})
if err != nil {
t.Fatal(err)
}
if _, err = io.Copy(os.Stdout, reader); err != nil {
t.Fatal(err)
}
reader.Close()

resp, err := c.ContainerCreate(ctx,
&container.Config{
Image: "bitnami/openldap:2",
ExposedPorts: nat.PortSet{
"1389/tcp": struct{}{},
},
Env: []string{
"LDAP_URI=ldap://openldap:1389",
"LDAP_BASE=dc=example,dc=org",
"LDAP_BIND_DN=cn=admin,dc=example,dc=org",
"LDAP_BIND_PASSWORD=adminpassword",
},
},
&container.HostConfig{
PortBindings: nat.PortMap{
"1389/tcp": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "1389",
},
},
},
}, nil, nil, "")
if err != nil {
t.Fatal(err)
}

if err := c.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
t.Fatal(err)
}

t.Cleanup(func() {
defer c.Close()
if err := c.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
t.Error(err)
}
})
}

func getLDAPUserEntryUUID() (string, error) {
// Connect to the LDAP server
l, err := ldap.DialURL("ldap://localhost:1389")
if err != nil {
return "", fmt.Errorf("failed to connect to LDAP server: %w", err)
}
defer l.Close()

err = l.Bind("cn=admin,dc=example,dc=org", "adminpassword")
if err != nil {
return "", fmt.Errorf("failed to bind to LDAP server: %w", err)
}

searchRequest := ldap.NewSearchRequest(
"dc=example,dc=org",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
"(cn=User1)", []string{"entryUUID"}, nil,
)

sr, err := l.Search(searchRequest)
if err != nil {
return "", fmt.Errorf("failed to execute search: %w", err)
}

// Process search results
if len(sr.Entries) == 0 {
return "", errors.New("no entries found for the specified username.")
}
entry := sr.Entries[0]
entryUUID := entry.GetAttributeValue("entryUUID")
if entryUUID == "" {
return "", errors.New("entryUUID is empty")
}
return entryUUID, nil
}
1 change: 1 addition & 0 deletions libbeat/cmd/instance/imports_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
_ "github.com/elastic/beats/v7/libbeat/processors/registered_domain"
_ "github.com/elastic/beats/v7/libbeat/processors/script"
_ "github.com/elastic/beats/v7/libbeat/processors/syslog"
_ "github.com/elastic/beats/v7/libbeat/processors/translate_ldap_attribute"
_ "github.com/elastic/beats/v7/libbeat/processors/translate_sid"
_ "github.com/elastic/beats/v7/libbeat/processors/urldecode"
_ "github.com/elastic/beats/v7/libbeat/publisher/includes" // Register publisher pipeline modules
Expand Down
6 changes: 6 additions & 0 deletions libbeat/docs/processors-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ endif::[]
ifndef::no_timestamp_processor[]
* <<processor-timestamp,`timestamp`>>
endif::[]
ifndef::no_translate_ldap_attribute_processor[]
* <<processor-translate-guid, `translate_ldap_attribute`>>
endif::[]
ifndef::no_translate_sid_processor[]
* <<processor-translate-sid, `translate_sid`>>
endif::[]
Expand Down Expand Up @@ -279,6 +282,9 @@ endif::[]
ifndef::no_timestamp_processor[]
include::{libbeat-processors-dir}/timestamp/docs/timestamp.asciidoc[]
endif::[]
ifndef::no_translate_ldap_attribute_processor[]
include::{libbeat-processors-dir}/translate_ldap_attribute/docs/translate_ldap_attribute.asciidoc[]
endif::[]
ifndef::no_translate_sid_processor[]
include::{libbeat-processors-dir}/translate_sid/docs/translate_sid.asciidoc[]
endif::[]
Expand Down
45 changes: 45 additions & 0 deletions libbeat/processors/translate_ldap_attribute/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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_ldap_attribute

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"`
LDAPBindUser string `config:"ldap_bind_user"`
LDAPBindPassword string `config:"ldap_bind_password"`
LDAPSearchAttribute string `config:"ldap_search_attribute" validate:"required"`
LDAPMappedAttribute string `config:"ldap_mapped_attribute" validate:"required"`
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{
LDAPSearchAttribute: "objectGUID",
LDAPMappedAttribute: "cn",
LDAPSearchTimeLimit: 30}
}
21 changes: 21 additions & 0 deletions libbeat/processors/translate_ldap_attribute/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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_ldap_attribute provides a Beat processor for converting
// LDAP attributes from one to another. It is typically used for converting Windows
// Global Unique Identifiers (GUIDs) to object names.
package translate_ldap_attribute
Loading

0 comments on commit 95408b3

Please sign in to comment.