diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e6912c82..0a0ed37ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - feat(receiver/monitorinjob): add Monitoring Job receiver [#1292] +- feat(receiver/activedirectoryinvreceiver): Add the Active Directory inventory receiver [#1273] ### Changed @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#1274]: https://github.com/SumoLogic/sumologic-otel-collector/pull/1274 [#1292]: https://github.com/SumoLogic/sumologic-otel-collector/pull/1292 +[#1273]: https://github.com/SumoLogic/sumologic-otel-collector/pull/1273 [Unreleased]: https://github.com/SumoLogic/sumologic-otel-collector/compare/v0.87.0-sumo-0...main ## [v0.87.0-sumo-0] diff --git a/otelcolbuilder/.otelcol-builder.yaml b/otelcolbuilder/.otelcol-builder.yaml index 274d8d4b42..800a96df85 100644 --- a/otelcolbuilder/.otelcol-builder.yaml +++ b/otelcolbuilder/.otelcol-builder.yaml @@ -88,7 +88,7 @@ receivers: - gomod: github.com/SumoLogic/sumologic-otel-collector/pkg/receiver/jobreceiver v0.0.0-00010101000000-000000000000 path: ./../pkg/receiver/jobreceiver - + - gomod: github.com/SumoLogic/sumologic-otel-collector/pkg/receiver/activedirectoryinvreceiver v0.0.0-00010101000000-000000000000 path: ./../pkg/receiver/activedirectoryinvreceiver # Upstream receivers: diff --git a/pkg/receiver/activedirectoryinvreceiver/adinvreceiver.go b/pkg/receiver/activedirectoryinvreceiver/adinvreceiver.go index dd23dbb4a5..a1d6967c76 100644 --- a/pkg/receiver/activedirectoryinvreceiver/adinvreceiver.go +++ b/pkg/receiver/activedirectoryinvreceiver/adinvreceiver.go @@ -17,7 +17,6 @@ package activedirectoryinvreceiver import ( "context" "fmt" - "log" "sync" "time" @@ -29,19 +28,31 @@ import ( "go.uber.org/zap" ) +// Client is an interface for an Active Directory client type Client interface { - Open(path string) (interface{}, error) + Open(path string, resourceLogs *plog.ResourceLogs) (Container, error) } +// ADSIClient is a wrapper for an Active Directory client type ADSIClient struct{} -func (c ADSIClient) Open(path string) (interface{}, error) { +// Open an Active Directory container +func (c *ADSIClient) Open(path string, resourceLogs *plog.ResourceLogs) (Container, error) { client, err := adsi.NewClient() if err != nil { return nil, err } ldapPath := fmt.Sprintf("LDAP://%s", path) - return client.Open(ldapPath) + root, err := client.Open(ldapPath) + if err != nil { + return nil, err + } + rootContainer, err := root.ToContainer() + if err != nil { + return nil, err + } + windowsContainer := ADSIContainer{rootContainer} + return windowsContainer, nil } type ADReceiver struct { @@ -53,6 +64,7 @@ type ADReceiver struct { doneChan chan bool } +// newLogsReceiver creates a new Active Directory Inventory receiver func newLogsReceiver(cfg *ADConfig, logger *zap.Logger, client Client, consumer consumer.Logs) *ADReceiver { return &ADReceiver{ @@ -65,20 +77,23 @@ func newLogsReceiver(cfg *ADConfig, logger *zap.Logger, client Client, consumer } } +// Start the logs receiver func (l *ADReceiver) Start(ctx context.Context, _ component.Host) error { - l.logger.Debug("starting to poll for active directory inventory records") + l.logger.Debug("Starting to poll for active directory inventory records") l.wg.Add(1) go l.startPolling(ctx) return nil } +// Shutdown the logs receiver func (l *ADReceiver) Shutdown(_ context.Context) error { - l.logger.Debug("shutting down logs receiver") + l.logger.Debug("Shutting down logs receiver") close(l.doneChan) l.wg.Wait() return nil } +// Start polling for Active Directory inventory records func (l *ADReceiver) startPolling(ctx context.Context) { defer l.wg.Done() t := time.NewTicker(l.config.PollInterval * time.Second) @@ -97,40 +112,56 @@ func (l *ADReceiver) startPolling(ctx context.Context) { } } -func (r *ADReceiver) poll(ctx context.Context) error { - go func() { - root, err := r.client.Open(r.config.DN) - if err != nil { - r.logger.Error("Failed to open root object:", zap.Error(err)) - return - } - rootObject := root.(*adsi.Object) - rootContainer, err := rootObject.ToContainer() +// Traverse the Active Directory tree and set user attributes to log records +func (r *ADReceiver) traverse(node Container, attrs []string, resourceLogs *plog.ResourceLogs) { + nodeObject, err := node.ToObject() + if err != nil { + r.logger.Error("Failed to convert container to object", zap.Error(err)) + return + } + setUserAttributes(nodeObject, attrs, resourceLogs) + children, err := node.Children() + if err != nil { + r.logger.Error("Failed to retrieve children", zap.Error(err)) + return + } + for child, err := children.Next(); err == nil; child, err = children.Next() { + windowsChildContainer, err := child.ToContainer() if err != nil { - r.logger.Error("Failed to open root object:", zap.Error(err)) + r.logger.Error("Failed to convert child object to container", zap.Error(err)) return } - defer rootContainer.Close() - logs := plog.NewLogs() - rl := logs.ResourceLogs().AppendEmpty() - resourceLogs := &rl - _ = resourceLogs.ScopeLogs().AppendEmpty() - r.traverse(rootContainer, resourceLogs) - err = r.consumer.ConsumeLogs(ctx, logs) - if err != nil { - r.logger.Error("Error consuming log", zap.Error(err)) - } - }() + childContainer := ADSIContainer{windowsChildContainer} + r.traverse(childContainer, attrs, resourceLogs) + } + defer node.Close() + children.Close() +} +// Poll for Active Directory inventory records +func (r *ADReceiver) poll(ctx context.Context) error { + logs := plog.NewLogs() + rl := logs.ResourceLogs().AppendEmpty() + resourceLogs := &rl + _ = resourceLogs.ScopeLogs().AppendEmpty() + root, err := r.client.Open(r.config.DN, resourceLogs) + r.traverse(root, r.config.Attributes, resourceLogs) + if err != nil { + return err + } + err = r.consumer.ConsumeLogs(ctx, logs) + if err != nil { + r.logger.Error("Error consuming log", zap.Error(err)) + } return nil } -func (l *ADReceiver) printAttrs(user *adsi.Object, resourceLogs *plog.ResourceLogs) { - attrs := l.config.Attributes +// Set user attributes to a log record body +func setUserAttributes(user Object, attrs []string, resourceLogs *plog.ResourceLogs) { observedTime := pcommon.NewTimestampFromTime(time.Now()) attributes := "" for _, attr := range attrs { - values, err := user.Attr(attr) + values, err := user.Attrs(attr) if err == nil && len(values) > 0 { attributes += fmt.Sprintf("%s: %v\n", attr, values) } @@ -140,26 +171,3 @@ func (l *ADReceiver) printAttrs(user *adsi.Object, resourceLogs *plog.ResourceLo logRecord.SetTimestamp(observedTime) logRecord.Body().SetStr(attributes) } - -func (l *ADReceiver) traverse(node *adsi.Container, resourceLogs *plog.ResourceLogs) { - nodeObject, err := node.ToObject() - if err != nil { - log.Printf("Error creating objects: %v\n", err) - return - } - l.printAttrs(nodeObject, resourceLogs) - children, err := node.Children() - if err != nil { - log.Printf("Error retrieving children: %v\n", err) - return - } - for child, err := children.Next(); err == nil; child, err = children.Next() { - childContainer, err := child.ToContainer() - if err != nil { - log.Println("Failed to traverse child object:", err) - return - } - l.traverse(childContainer, resourceLogs) - } - children.Close() -} diff --git a/pkg/receiver/activedirectoryinvreceiver/adinvreceiver_test.go b/pkg/receiver/activedirectoryinvreceiver/adinvreceiver_test.go index faff7ec6b1..d447ebfc5e 100644 --- a/pkg/receiver/activedirectoryinvreceiver/adinvreceiver_test.go +++ b/pkg/receiver/activedirectoryinvreceiver/adinvreceiver_test.go @@ -16,18 +16,70 @@ package activedirectoryinvreceiver import ( "context" - "testing" - + "fmt" + adsi "github.com/go-adsi/adsi" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/pdata/plog" "go.uber.org/zap" + "testing" + "time" ) -type MockClient struct{} +type MockClient struct { + mock.Mock +} + +func (mc *MockClient) Open(path string, resourceLogs *plog.ResourceLogs) (Container, error) { + args := mc.Called(path, resourceLogs) + return args.Get(0).(Container), args.Error(1) +} -func (c MockClient) Open(path string) (interface{}, error) { - return nil, nil +type MockContainer struct { + mock.Mock +} + +func (mc *MockContainer) ToObject() (Object, error) { + args := mc.Called() + return args.Get(0).(Object), args.Error(1) +} + +func (mc *MockContainer) Close() { + mc.Called() +} + +func (mc *MockContainer) Children() (ObjectIter, error) { + args := mc.Called() + return args.Get(0).(ObjectIter), args.Error(1) +} + +type MockObject struct { + mock.Mock +} + +func (mo *MockObject) Attrs(key string) ([]interface{}, error) { + args := mo.Called(key) + return args.Get(0).([]interface{}), args.Error(1) +} + +func (mo *MockObject) ToContainer() (Container, error) { + args := mo.Called() + return args.Get(0).(Container), args.Error(1) +} + +type MockObjectIter struct { + mock.Mock +} + +func (mo MockObjectIter) Next() (*adsi.Object, error) { + args := mo.Called() + return args.Get(0).(*adsi.Object), args.Error(1) +} + +func (mo MockObjectIter) Close() { + mo.Called() } func TestStart(t *testing.T) { @@ -35,13 +87,50 @@ func TestStart(t *testing.T) { cfg.DN = "CN=Guest,CN=Users,DC=exampledomain,DC=com" sink := &consumertest.LogsSink{} - mockClient := MockClient{} + mockClient := &MockClient{} + + logsRcvr := newLogsReceiver(cfg, zap.NewNop(), mockClient, sink) + + err := logsRcvr.Start(context.Background(), componenttest.NewNopHost()) + require.NoError(t, err) + + err = logsRcvr.Shutdown(context.Background()) + require.NoError(t, err) +} + +func TestPoll(t *testing.T) { + cfg := CreateDefaultConfig().(*ADConfig) + cfg.DN = "CN=Guest,CN=Users,DC=exampledomain,DC=com" + cfg.PollInterval = 1 + cfg.Attributes = []string{"name"} + + sink := &consumertest.LogsSink{} + mockClient := defaultMockClient() logsRcvr := newLogsReceiver(cfg, zap.NewNop(), mockClient, sink) err := logsRcvr.Start(context.Background(), componenttest.NewNopHost()) require.NoError(t, err) + require.Eventually(t, func() bool { + return sink.LogRecordCount() > 0 + }, 2*time.Second, 10*time.Millisecond) + err = logsRcvr.Shutdown(context.Background()) require.NoError(t, err) } + +func defaultMockClient() Client { + mockClient := &MockClient{} + mockContainer := MockContainer{} + mockObject := MockObject{} + mockObjectIter := &MockObjectIter{} + attrs := []interface{}{"Guest", "test"} + mockContainer.On("ToObject").Return(&mockObject, nil) + mockContainer.On("Children").Return(mockObjectIter, fmt.Errorf("no children")) + mockContainer.On("Close").Return(nil) + mockObject.On("Attrs", mock.Anything).Return(attrs, nil) + mockObject.On("ToContainer").Return(&mockContainer, nil) + mockClient.On("Open", mock.Anything, mock.Anything).Return(&mockContainer, nil) + return mockClient +} diff --git a/pkg/receiver/activedirectoryinvreceiver/adwrappers.go b/pkg/receiver/activedirectoryinvreceiver/adwrappers.go new file mode 100644 index 0000000000..e1efee1fff --- /dev/null +++ b/pkg/receiver/activedirectoryinvreceiver/adwrappers.go @@ -0,0 +1,99 @@ +// Copyright 2021, OpenTelemetry Authors +// +// Licensed 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 activedirectoryinvreceiver + +import ( + adsi "github.com/go-adsi/adsi" +) + +// Container is an interface for an Active Directory container +type Container interface { + ToObject() (Object, error) + Close() + Children() (ObjectIter, error) +} + +// ADSIContainer is a wrapper for an Active Directory container +type ADSIContainer struct { + windowsADContainer *adsi.Container +} + +// ToObject converts an Active Directory container to an Active Directory object +func (c ADSIContainer) ToObject() (Object, error) { + object, err := c.windowsADContainer.ToObject() + if err != nil { + return nil, err + } + return ADObject{object}, nil +} + +// Close closes an Active Directory container +func (c ADSIContainer) Close() { + c.windowsADContainer.Close() +} + +// Children returns the children of an Active Directory container +func (c ADSIContainer) Children() (ObjectIter, error) { + objectIter, err := c.windowsADContainer.Children() + if err != nil { + return nil, err + } + return &ADObjectIter{objectIter}, nil +} + +// Object is an interface for an Active Directory object +type Object interface { + Attrs(key string) ([]interface{}, error) + ToContainer() (Container, error) +} + +// ADObject is a wrapper for an Active Directory object +type ADObject struct { + windowsADObject *adsi.Object +} + +// Attrs returns the attributes of an Active Directory object +func (o ADObject) Attrs(key string) ([]interface{}, error) { + return o.windowsADObject.Attr(key) +} + +func (o ADObject) ToContainer() (Container, error) { + container, err := o.windowsADObject.ToContainer() + if err != nil { + return nil, err + } + return ADSIContainer{container}, nil +} + +// ObjectIter is an interface for an Active Directory object iterator +type ObjectIter interface { + Next() (*adsi.Object, error) + Close() +} + +// ADObjectIter is a wrapper for an Active Directory object iterator +type ADObjectIter struct { + windowsADObjectIter *adsi.ObjectIter +} + +// Next returns the next Active Directory object in the iterator +func (o *ADObjectIter) Next() (*adsi.Object, error) { + return o.windowsADObjectIter.Next() +} + +// Close closes an Active Directory object iterator +func (o *ADObjectIter) Close() { + o.windowsADObjectIter.Close() +} diff --git a/pkg/receiver/activedirectoryinvreceiver/factory.go b/pkg/receiver/activedirectoryinvreceiver/factory.go index e5543b6ca6..cd82693e62 100644 --- a/pkg/receiver/activedirectoryinvreceiver/factory.go +++ b/pkg/receiver/activedirectoryinvreceiver/factory.go @@ -52,7 +52,7 @@ func createLogsReceiver( consumer consumer.Logs, ) (receiver.Logs, error) { cfg := rConf.(*ADConfig) - adsiClient := ADSIClient{} + adsiClient := &ADSIClient{} rcvr := newLogsReceiver(cfg, params.Logger, adsiClient, consumer) return rcvr, nil } diff --git a/pkg/receiver/activedirectoryinvreceiver/go.mod b/pkg/receiver/activedirectoryinvreceiver/go.mod index a792897264..a4c4200548 100644 --- a/pkg/receiver/activedirectoryinvreceiver/go.mod +++ b/pkg/receiver/activedirectoryinvreceiver/go.mod @@ -30,6 +30,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/scjalliance/comutil v0.0.0-20230315211610-645474dab300 // indirect + github.com/stretchr/objx v0.5.0 // indirect go.opentelemetry.io/collector/config/configtelemetry v0.86.0 // indirect go.opentelemetry.io/collector/confmap v0.86.0 // indirect go.opentelemetry.io/collector/featuregate v1.0.0-rcv0015 // indirect diff --git a/pkg/receiver/activedirectoryinvreceiver/go.sum b/pkg/receiver/activedirectoryinvreceiver/go.sum index c8c44d965a..2132c60bad 100644 --- a/pkg/receiver/activedirectoryinvreceiver/go.sum +++ b/pkg/receiver/activedirectoryinvreceiver/go.sum @@ -48,7 +48,12 @@ github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83 github.com/scjalliance/comutil v0.0.0-20230315211610-645474dab300 h1:6V1C2ts6l6jbyMcIR8i4HgNFIWG7oqv2uUAHz44ua1M= github.com/scjalliance/comutil v0.0.0-20230315211610-645474dab300/go.mod h1:GUhhDQ335OI3Y4WxKKkP1+L25ZJE2VzbtwvWJSew7m8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -121,5 +126,6 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=