diff --git a/libbeat/cmd/instance/imports_common.go b/libbeat/cmd/instance/imports_common.go index d8fa4b9f0cf..b90820daeb9 100644 --- a/libbeat/cmd/instance/imports_common.go +++ b/libbeat/cmd/instance/imports_common.go @@ -35,5 +35,6 @@ import ( _ "github.com/elastic/beats/libbeat/processors/extract_array" _ "github.com/elastic/beats/libbeat/processors/fingerprint" _ "github.com/elastic/beats/libbeat/processors/registered_domain" + _ "github.com/elastic/beats/libbeat/processors/translate_sid" _ "github.com/elastic/beats/libbeat/publisher/includes" // Register publisher pipeline modules ) diff --git a/libbeat/docs/processors-list.asciidoc b/libbeat/docs/processors-list.asciidoc index a9a4356377a..8bf06205038 100644 --- a/libbeat/docs/processors-list.asciidoc +++ b/libbeat/docs/processors-list.asciidoc @@ -95,6 +95,9 @@ endif::[] ifndef::no_truncate_fields_processor[] * <> endif::[] +ifdef::no_translate_sid_processor[] +* <> +endif::[] //# end::processors-list[] //# tag::processors-include[] @@ -191,6 +194,9 @@ endif::[] ifndef::no_truncate_fields_processor[] include::{libbeat-processors-dir}/actions/docs/truncate_fields.asciidoc[] endif::[] +ifdef::no_translate_sid_processor[] +include::{libbeat-processors-dir}/translate_sid/docs/translate_sid.asciidoc[] +endif::[] //# end::processors-include[] diff --git a/libbeat/docs/shared-beats-attributes.asciidoc b/libbeat/docs/shared-beats-attributes.asciidoc index 8a5a9f86986..c4c01bb2b4e 100644 --- a/libbeat/docs/shared-beats-attributes.asciidoc +++ b/libbeat/docs/shared-beats-attributes.asciidoc @@ -6,6 +6,7 @@ :libbeat-processors-dir: {beats-root}/libbeat/processors :libbeat-outputs-dir: {beats-root}/libbeat/outputs :x-filebeat-processors-dir: {beats-root}/x-pack/filebeat/processors +:winlogbeat-processors-dir: {beats-root}/winlogbeat/processors :cm-ui: Central Management :libbeat-docs: Beats Platform Reference diff --git a/libbeat/processors/translate_sid/config.go b/libbeat/processors/translate_sid/config.go new file mode 100644 index 00000000000..192d8fd6e0c --- /dev/null +++ b/libbeat/processors/translate_sid/config.go @@ -0,0 +1,41 @@ +// 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_sid + +import "github.com/pkg/errors" + +type config struct { + Field string `config:"field" validate:"required"` + AccountNameTarget string `config:"account_name_target"` + AccountTypeTarget string `config:"account_type_target"` + DomainTarget string `config:"domain_target"` + IgnoreMissing bool `config:"ignore_missing"` + IgnoreFailure bool `config:"ignore_failure"` +} + +func (c *config) Validate() error { + if c.AccountNameTarget == "" && c.AccountTypeTarget == "" && c.DomainTarget == "" { + return errors.New("at least one target field must be configured " + + "(set account_name_target, account_type_target, and/or domain_target)") + } + return nil +} + +func defaultConfig() config { + return config{} +} diff --git a/libbeat/processors/translate_sid/doc.go b/libbeat/processors/translate_sid/doc.go new file mode 100644 index 00000000000..8c3721b374f --- /dev/null +++ b/libbeat/processors/translate_sid/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_sid provides a Beat processor for converting Windows +// security identifiers (SIDs) to account names. +package translate_sid diff --git a/libbeat/processors/translate_sid/docs/translate_sid.asciidoc b/libbeat/processors/translate_sid/docs/translate_sid.asciidoc new file mode 100644 index 00000000000..68f7fd2542d --- /dev/null +++ b/libbeat/processors/translate_sid/docs/translate_sid.asciidoc @@ -0,0 +1,46 @@ +[[processor-translate-sid]] +=== Translate SID + +beta[] + +The `translate_sid` processor translates a Windows security identifier (SID) +into an account name. It retrieves the name of the account associated with the +SID, the first domain on which the SID is found, and the type of account. This +is only available on Windows. + +Every account on a network is issued a unique SID when the account is first +created. Internal processes in Windows refer to an account's SID rather than +the account's user or group name and these values sometimes appear in logs. + +If the SID is invalid (malformed) or does not map to any account on the local +system or domain then this will result in the processor returning an error +unless `ignore_failure` is set. + +[source,yaml] +---- +processors: +- translate_sid: + field: winlog.event_data.MemberSid + account_name_target: user.name + domain_target: user.domain + ignore_missing: true + ignore_failure: true +---- + +The `translate_sid` processor has the following configuration settings: + +.Translate SID options +[options="header"] +|====== +| Name | Required | Default | Description +| `field` | yes | | Source field containing a Windows security identifier (SID). +| `account_name_target` | yes* | | Target field for the account name value. +| `account_type_target` | yes* | | Target field for the account type value. +| `domain_target` | yes* | | Target field for the domain value. +| `ignore_missing` | no | false | Ignore errors when the source field is missing. +| `ignore_failure` | no | false | Ignore all errors produced by the processor. +|====== + +* At least one of `account_name_target`, `account_type_target`, and +`domain_target` is required to be configured. + diff --git a/libbeat/processors/translate_sid/translatesid.go b/libbeat/processors/translate_sid/translatesid.go new file mode 100644 index 00000000000..92e121d3824 --- /dev/null +++ b/libbeat/processors/translate_sid/translatesid.go @@ -0,0 +1,124 @@ +// 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. + +// +build windows + +package translate_sid + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "go.uber.org/multierr" + "golang.org/x/sys/windows" + + "github.com/elastic/beats/libbeat/beat" + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/logp" + "github.com/elastic/beats/libbeat/processors" + jsprocessor "github.com/elastic/beats/libbeat/processors/script/javascript/module/processor" + "github.com/elastic/beats/winlogbeat/sys" +) + +const logName = "processor.translate_sid" + +var errInvalidType = errors.New("SID field value is not a string") + +func init() { + processors.RegisterPlugin("translate_sid", New) + jsprocessor.RegisterPlugin("TranslateSID", New) +} + +type processor struct { + config + log *logp.Logger +} + +// New returns a new translate_sid processor for converting windows SID values +// to names. +func New(cfg *common.Config) (processors.Processor, error) { + c := defaultConfig() + if err := cfg.Unpack(&c); err != nil { + return nil, errors.Wrap(err, "fail to unpack the translate_sid configuration") + } + + return newFromConfig(c) +} + +func newFromConfig(c config) (*processor, error) { + return &processor{ + config: c, + log: logp.NewLogger(logName), + }, nil +} + +func (p *processor) String() string { + return fmt.Sprintf("translate_sid=[field=%s, account_name_target=%s, account_type_target=%s, domain_target=%s]", + p.Field, p.AccountNameTarget, p.AccountTypeTarget, p.DomainTarget) +} + +func (p *processor) Run(event *beat.Event) (*beat.Event, error) { + err := p.translateSID(event) + if err == nil || p.IgnoreFailure || (p.IgnoreMissing && common.ErrKeyNotFound == errors.Cause(err)) { + return event, nil + } + return event, err +} + +func (p *processor) translateSID(event *beat.Event) error { + v, err := event.GetValue(p.Field) + if err != nil { + return err + } + sidString, ok := v.(string) + if !ok { + return errInvalidType + } + + // All SIDs starting with S-1-15-3 are capability SIDs. Active Directory + // does not resolve them so don't try. + // Reference: https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems + if strings.HasPrefix(sidString, "S-1-15-3-") { + return windows.ERROR_NONE_MAPPED + + } + + sid, err := windows.StringToSid(sidString) + if err != nil { + return err + } + + // XXX: May want to introduce an in-memory cache if the lookups are time consuming. + account, domain, accountType, err := sid.LookupAccount("") + if err != nil { + return err + } + + // Do all operations even if one fails. + var errs []error + if _, err = event.PutValue(p.AccountNameTarget, account); err != nil { + errs = append(errs, err) + } + if _, err = event.PutValue(p.AccountTypeTarget, sys.SIDType(accountType).String()); err != nil { + errs = append(errs, err) + } + if _, err = event.PutValue(p.DomainTarget, domain); err != nil { + errs = append(errs, err) + } + return multierr.Combine(errs...) +} diff --git a/libbeat/processors/translate_sid/translatesid_test.go b/libbeat/processors/translate_sid/translatesid_test.go new file mode 100644 index 00000000000..d0c0ee2206c --- /dev/null +++ b/libbeat/processors/translate_sid/translatesid_test.go @@ -0,0 +1,150 @@ +// 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. + +// +build windows + +package translate_sid + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/windows" + + "github.com/elastic/beats/libbeat/beat" + "github.com/elastic/beats/winlogbeat/sys" +) + +func TestTranslateSID(t *testing.T) { + var tests = []struct { + SID string + Account string + AccountType sys.SIDType + Domain string + Assert func(*testing.T, *beat.Event, error) + }{ + {SID: "S-1-5-7", Domain: "NT AUTHORITY", Account: "ANONYMOUS LOGON"}, + {SID: "S-1-0-0", Account: "NULL SID"}, + {SID: "S-1-1-0", Account: "Everyone"}, + {SID: "S-1-5-32-544", Domain: "BUILTIN", Account: "Administrators", AccountType: sys.SidTypeAlias}, + {SID: "S-1-5-113", Domain: "NT AUTHORITY", Account: "Local Account"}, + {SID: "", Assert: assertInvalidSID}, + {SID: "Not a SID", Assert: assertInvalidSID}, + {SID: "S-1-5-2025429265-500", Assert: assertNoMapping}, + } + + for n, tc := range tests { + t.Run(fmt.Sprintf("test%d_%s", n, tc.SID), func(t *testing.T) { + p, err := newFromConfig(config{ + Field: "sid", + DomainTarget: "domain", + AccountNameTarget: "account", + AccountTypeTarget: "type", + }) + if err != nil { + t.Fatal(err) + } + + evt := &beat.Event{Fields: map[string]interface{}{ + "sid": tc.SID, + }} + + evt, err = p.Run(evt) + if tc.Assert != nil { + tc.Assert(t, evt, err) + return + } + if err != nil { + t.Fatalf("%+v", err) + } + t.Logf("%v", evt.Fields.StringToPrint()) + assertEqualIgnoreCase(t, tc.Domain, evt.Fields["domain"]) + assertEqualIgnoreCase(t, tc.Account, evt.Fields["account"]) + if tc.AccountType > 0 { + assert.Equal(t, tc.AccountType.String(), evt.Fields["type"]) + } + }) + } +} + +func BenchmarkProcessor_Run(b *testing.B) { + p, err := newFromConfig(config{ + Field: "sid", + DomainTarget: "domain", + AccountNameTarget: "account", + }) + if err != nil { + b.Fatal(err) + } + + b.Run("builtin", func(b *testing.B) { + evt := &beat.Event{Fields: map[string]interface{}{ + "sid": "S-1-5-7", + }} + + for i := 0; i < b.N; i++ { + _, err = p.Run(evt) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("no_mapping", func(b *testing.B) { + evt := &beat.Event{Fields: map[string]interface{}{ + "sid": "S-1-5-2025429265-500", + }} + + for i := 0; i < b.N; i++ { + _, err = p.Run(evt) + if err != windows.ERROR_NONE_MAPPED { + b.Fatal(err) + } + } + }) +} + +func assertEqualIgnoreCase(t *testing.T, expected string, actual interface{}) { + t.Helper() + actualStr, ok := actual.(string) + if !ok { + assert.Fail(t, "actual value is not a string: %T %#v", actual, actual) + } + assert.Equal(t, strings.ToLower(expected), strings.ToLower(actualStr)) +} + +func assertInvalidSID(t *testing.T, event *beat.Event, err error) { + if assert.Error(t, err) { + // The security ID structure is invalid. + assert.Equal(t, windows.ERROR_INVALID_SID, err) + } + assert.Nil(t, event.Fields["domain"]) + assert.Nil(t, event.Fields["account"]) + assert.Nil(t, event.Fields["type"]) +} + +func assertNoMapping(t *testing.T, event *beat.Event, err error) { + if assert.Error(t, err) { + // No mapping between account names and security IDs was done. + assert.Equal(t, windows.ERROR_NONE_MAPPED, err) + } + assert.Nil(t, event.Fields["domain"]) + assert.Nil(t, event.Fields["account"]) + assert.Nil(t, event.Fields["type"]) +} diff --git a/winlogbeat/docs/index.asciidoc b/winlogbeat/docs/index.asciidoc index 4046b891ac8..e09c75ec313 100644 --- a/winlogbeat/docs/index.asciidoc +++ b/winlogbeat/docs/index.asciidoc @@ -20,6 +20,7 @@ include::{asciidoc-dir}/../../shared/attributes.asciidoc[] :win_only: :no_decode_cef_processor: :no_decode_csv_fields_processor: +:include_translate_sid_processor: include::{libbeat-dir}/shared-beats-attributes.asciidoc[] diff --git a/winlogbeat/sys/sid.go b/winlogbeat/sys/sid.go index c629af4722e..1f09c1b8f8f 100644 --- a/winlogbeat/sys/sid.go +++ b/winlogbeat/sys/sid.go @@ -19,6 +19,7 @@ package sys import ( "fmt" + "strconv" ) // SID represents the Windows Security Identifier for an account. @@ -51,6 +52,7 @@ const ( SidTypeUnknown SidTypeComputer SidTypeLabel + SidTypeLogonSession ) // sidTypeToString is a mapping of SID types to their string representations. @@ -65,9 +67,15 @@ var sidTypeToString = map[SIDType]string{ SidTypeUnknown: "Unknown", SidTypeComputer: "Computer", SidTypeLabel: "Label", + SidTypeLogonSession: "Logon Session", } // String returns string representation of SIDType. func (st SIDType) String() string { - return sidTypeToString[st] + if typ, found := sidTypeToString[st]; found { + return typ + } else if st > 0 { + return strconv.FormatUint(uint64(st), 10) + } + return "" }