mirror of
https://github.com/safing/portmaster
synced 2025-04-25 13:29:10 +00:00
Merge 42cc7ac475
into b9a9129e81
This commit is contained in:
commit
18b381db4e
14 changed files with 1608 additions and 0 deletions
systemdns
139
systemdns/dnsevtlog/events.go
Normal file
139
systemdns/dnsevtlog/events.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dnsevtlog
|
||||
|
||||
// This code is copied from Promtail v1.6.2-0.20231004111112-07cbef92268a with minor changes.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/systemdns/dnsevtlog/win_eventlog"
|
||||
)
|
||||
|
||||
type Subscription struct {
|
||||
subscription win_eventlog.EvtHandle
|
||||
fetcher *win_eventlog.EventFetcher
|
||||
|
||||
eventLogName string
|
||||
eventLogQuery string
|
||||
|
||||
ready bool
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
err error
|
||||
}
|
||||
|
||||
// NewSubscription create a new windows event subscriptions.
|
||||
func NewSubscription() (*Subscription, error) {
|
||||
sigEvent, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CloseHandle(sigEvent)
|
||||
|
||||
t := &Subscription{
|
||||
eventLogName: "Microsoft-Windows-DNS-Client/Operational",
|
||||
eventLogQuery: "*",
|
||||
done: make(chan struct{}),
|
||||
fetcher: win_eventlog.NewEventFetcher(),
|
||||
}
|
||||
|
||||
subsHandle, err := win_eventlog.EvtSubscribe(t.eventLogName, t.eventLogQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error subscribing to windows events: %w", err)
|
||||
}
|
||||
t.subscription = subsHandle
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// loop fetches new events and send them to via the Loki client.
|
||||
func (t *Subscription) ReadWorker() {
|
||||
t.ready = true
|
||||
t.wg.Add(1)
|
||||
interval := time.NewTicker(time.Second)
|
||||
defer func() {
|
||||
t.ready = false
|
||||
t.wg.Done()
|
||||
interval.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
|
||||
loop:
|
||||
for {
|
||||
// fetch events until there's no more.
|
||||
events, handles, err := t.fetcher.FetchEvents(t.subscription, 1033) // 1033: English
|
||||
if err != nil {
|
||||
if err != win_eventlog.ERROR_NO_MORE_ITEMS {
|
||||
t.err = err
|
||||
log.Warningf("dns event log: failed to fetch events: %s", err)
|
||||
} else {
|
||||
log.Debug("dns event log: no more entries")
|
||||
}
|
||||
break loop
|
||||
}
|
||||
t.err = nil
|
||||
// we have received events to handle.
|
||||
for _, entry := range events {
|
||||
log.Debugf("dns event log: %+v", entry)
|
||||
}
|
||||
win_eventlog.Close(handles)
|
||||
}
|
||||
// no more messages we wait for next poll timer tick.
|
||||
select {
|
||||
case <-t.done:
|
||||
return
|
||||
case <-interval.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderEntries renders Loki entries from windows event logs
|
||||
// func (t *Subscription) renderEntries(events []win_eventlog.Event) []api.Entry {
|
||||
// res := make([]api.Entry, 0, len(events))
|
||||
// lbs := labels.NewBuilder(nil)
|
||||
// for _, event := range events {
|
||||
// entry := api.Entry{
|
||||
// Labels: make(model.LabelSet),
|
||||
// }
|
||||
|
||||
// entry.Timestamp = time.Now()
|
||||
// if t.cfg.UseIncomingTimestamp {
|
||||
// timeStamp, err := time.Parse(time.RFC3339Nano, fmt.Sprintf("%v", event.TimeCreated.SystemTime))
|
||||
// if err != nil {
|
||||
// level.Warn(t.logger).Log("msg", "error parsing timestamp", "err", err)
|
||||
// } else {
|
||||
// entry.Timestamp = timeStamp
|
||||
// }
|
||||
// }
|
||||
|
||||
// for _, lbl := range processed {
|
||||
// if strings.HasPrefix(lbl.Name, "__") {
|
||||
// continue
|
||||
// }
|
||||
// entry.Labels[model.LabelName(lbl.Name)] = model.LabelValue(lbl.Value)
|
||||
// }
|
||||
|
||||
// line, err := formatLine(t.cfg, event)
|
||||
// if err != nil {
|
||||
// level.Warn(t.logger).Log("msg", "error formatting event", "err", err)
|
||||
// continue
|
||||
// }
|
||||
// entry.Line = line
|
||||
// res = append(res, entry)
|
||||
// }
|
||||
// return res
|
||||
// }
|
||||
|
||||
func (t *Subscription) Stop() error {
|
||||
close(t.done)
|
||||
t.wg.Wait()
|
||||
return t.err
|
||||
}
|
26
systemdns/dnsevtlog/testevtlog/main.go
Normal file
26
systemdns/dnsevtlog/testevtlog/main.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/systemdns/dnsevtlog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetLogLevel(log.DebugLevel)
|
||||
err := log.Start()
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sub, err := dnsevtlog.NewSubscription()
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sub.ReadWorker()
|
||||
}
|
22
systemdns/dnsevtlog/win_eventlog/LICENSE
Normal file
22
systemdns/dnsevtlog/win_eventlog/LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
6
systemdns/dnsevtlog/win_eventlog/README.md
Normal file
6
systemdns/dnsevtlog/win_eventlog/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Windows Event Log
|
||||
|
||||
This is a fork of https://github.com/influxdata/telegraf/tree/master/plugins/inputs/win_eventlog to re-use most of the syscall implementation for the eventlog.
|
||||
|
||||
It is simplified in order to just subscribe to events.
|
||||
|
94
systemdns/dnsevtlog/win_eventlog/event.go
Normal file
94
systemdns/dnsevtlog/win_eventlog/event.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
// Event is the event entry representation
|
||||
// Only the most common elements are processed, human-readable data is rendered in Message
|
||||
// More info on schema, if there will be need to add more:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/wes/eventschema-elements
|
||||
type Event struct {
|
||||
Source Provider `xml:"System>Provider"`
|
||||
EventID int `xml:"System>EventID"`
|
||||
Version int `xml:"System>Version"`
|
||||
Level int `xml:"System>Level"`
|
||||
Task int `xml:"System>Task"`
|
||||
Opcode int `xml:"System>Opcode"`
|
||||
Keywords string `xml:"System>Keywords"`
|
||||
TimeCreated TimeCreated `xml:"System>TimeCreated"`
|
||||
EventRecordID int `xml:"System>EventRecordID"`
|
||||
Correlation Correlation `xml:"System>Correlation"`
|
||||
Execution Execution `xml:"System>Execution"`
|
||||
Channel string `xml:"System>Channel"`
|
||||
Computer string `xml:"System>Computer"`
|
||||
Security Security `xml:"System>Security"`
|
||||
UserData UserData `xml:"UserData"`
|
||||
EventData EventData `xml:"EventData"`
|
||||
Message string
|
||||
LevelText string
|
||||
TaskText string
|
||||
OpcodeText string
|
||||
}
|
||||
|
||||
// UserData Application-provided XML data
|
||||
type UserData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// EventData Application-provided XML data
|
||||
type EventData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// Provider is the Event provider information
|
||||
type Provider struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
}
|
||||
|
||||
// Correlation is used for the event grouping
|
||||
type Correlation struct {
|
||||
ActivityID string `xml:"ActivityID,attr"`
|
||||
RelatedActivityID string `xml:"RelatedActivityID,attr"`
|
||||
}
|
||||
|
||||
// Execution Info for Event
|
||||
type Execution struct {
|
||||
ProcessID uint32 `xml:"ProcessID,attr"`
|
||||
ThreadID uint32 `xml:"ThreadID,attr"`
|
||||
ProcessName string
|
||||
}
|
||||
|
||||
// Security Data for Event
|
||||
type Security struct {
|
||||
UserID string `xml:"UserID,attr"`
|
||||
}
|
||||
|
||||
// TimeCreated field for Event
|
||||
type TimeCreated struct {
|
||||
SystemTime string `xml:"SystemTime,attr"`
|
||||
}
|
67
systemdns/dnsevtlog/win_eventlog/syscall_windows.go
Normal file
67
systemdns/dnsevtlog/win_eventlog/syscall_windows.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Event log error codes.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
ERROR_INSUFFICIENT_BUFFER syscall.Errno = 122
|
||||
ERROR_NO_MORE_ITEMS syscall.Errno = 259
|
||||
ERROR_INVALID_OPERATION syscall.Errno = 4317
|
||||
//revive:enable:var-naming
|
||||
)
|
||||
|
||||
// EvtSubscribeFlag defines the possible values that specify when to start subscribing to events.
|
||||
type EvtSubscribeFlag uint32
|
||||
|
||||
// EVT_SUBSCRIBE_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385588(v=vs.85).aspx
|
||||
const (
|
||||
EvtSubscribeToFutureEvents EvtSubscribeFlag = 1
|
||||
EvtSubscribeStartAtOldestRecord EvtSubscribeFlag = 2
|
||||
EvtSubscribeStartAfterBookmark EvtSubscribeFlag = 3
|
||||
)
|
||||
|
||||
// EvtRenderFlag uint32
|
||||
type EvtRenderFlag uint32
|
||||
|
||||
// EVT_RENDER_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385563(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
// Render the event as an XML string. For details on the contents of the
|
||||
// XML string, see the Event schema.
|
||||
EvtRenderEventXml EvtRenderFlag = 1
|
||||
//revive:enable:var-naming
|
||||
EvtRenderBookmark EvtRenderFlag = 2
|
||||
)
|
178
systemdns/dnsevtlog/win_eventlog/util.go
Normal file
178
systemdns/dnsevtlog/win_eventlog/util.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// DecodeUTF16 to UTF8 bytes
|
||||
func DecodeUTF16(b []byte) ([]byte, error) {
|
||||
|
||||
if len(b)%2 != 0 {
|
||||
return nil, fmt.Errorf("must have even length byte slice")
|
||||
}
|
||||
|
||||
u16s := make([]uint16, 1)
|
||||
|
||||
ret := &bytes.Buffer{}
|
||||
|
||||
b8buf := make([]byte, 4)
|
||||
|
||||
lb := len(b)
|
||||
for i := 0; i < lb; i += 2 {
|
||||
u16s[0] = uint16(b[i]) + (uint16(b[i+1]) << 8)
|
||||
r := utf16.Decode(u16s)
|
||||
n := utf8.EncodeRune(b8buf, r[0])
|
||||
ret.Write(b8buf[:n])
|
||||
}
|
||||
|
||||
return ret.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetFromSnapProcess finds information about process by the given pid
|
||||
// Returns process parent pid, threads info handle and process name
|
||||
func GetFromSnapProcess(pid uint32) (uint32, uint32, string, error) {
|
||||
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, uint32(pid))
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
defer windows.CloseHandle(snap)
|
||||
var pe32 windows.ProcessEntry32
|
||||
pe32.Size = uint32(unsafe.Sizeof(pe32))
|
||||
if err = windows.Process32First(snap, &pe32); err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
for {
|
||||
if pe32.ProcessID == uint32(pid) {
|
||||
szexe := windows.UTF16ToString(pe32.ExeFile[:])
|
||||
return uint32(pe32.ParentProcessID), uint32(pe32.Threads), szexe, nil
|
||||
}
|
||||
if err = windows.Process32Next(snap, &pe32); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, 0, "", fmt.Errorf("couldn't find pid: %d", pid)
|
||||
}
|
||||
|
||||
type xmlnode struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content []byte `xml:",innerxml"`
|
||||
Text string `xml:",chardata"`
|
||||
Nodes []xmlnode `xml:",any"`
|
||||
}
|
||||
|
||||
// EventField for unique rendering
|
||||
type EventField struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// UnmarshalXML redefined for xml elements walk
|
||||
func (n *xmlnode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
n.Attrs = start.Attr
|
||||
type node xmlnode
|
||||
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// UnrollXMLFields extracts fields from xml data
|
||||
func UnrollXMLFields(data []byte, fieldsUsage map[string]int, separator string) ([]EventField, map[string]int) {
|
||||
buf := bytes.NewBuffer(data)
|
||||
dec := xml.NewDecoder(buf)
|
||||
var fields []EventField
|
||||
for {
|
||||
var node xmlnode
|
||||
err := dec.Decode(&node)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// log.Fatal(err)
|
||||
break
|
||||
}
|
||||
var parents []string
|
||||
walkXML([]xmlnode{node}, parents, separator, func(node xmlnode, parents []string, separator string) bool {
|
||||
innerText := strings.TrimSpace(node.Text)
|
||||
if len(innerText) > 0 {
|
||||
valueName := strings.Join(parents, separator)
|
||||
fieldsUsage[valueName]++
|
||||
field := EventField{Name: valueName, Value: innerText}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return fields, fieldsUsage
|
||||
}
|
||||
|
||||
func walkXML(nodes []xmlnode, parents []string, separator string, f func(xmlnode, []string, string) bool) {
|
||||
for _, node := range nodes {
|
||||
parentName := node.XMLName.Local
|
||||
for _, attr := range node.Attrs {
|
||||
attrName := strings.ToLower(attr.Name.Local)
|
||||
if attrName == "name" {
|
||||
// Add Name attribute to parent name
|
||||
parentName = strings.Join([]string{parentName, attr.Value}, separator)
|
||||
}
|
||||
}
|
||||
nodeParents := append(parents, parentName)
|
||||
if f(node, nodeParents, separator) {
|
||||
walkXML(node.Nodes, nodeParents, separator, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UniqueFieldNames forms unique field names
|
||||
// by adding _<num> if there are several of them
|
||||
func UniqueFieldNames(fields []EventField, fieldsUsage map[string]int, separator string) []EventField {
|
||||
var fieldsCounter = map[string]int{}
|
||||
var fieldsUnique []EventField
|
||||
for _, field := range fields {
|
||||
fieldName := field.Name
|
||||
if fieldsUsage[field.Name] > 1 {
|
||||
fieldsCounter[field.Name]++
|
||||
fieldName = fmt.Sprint(field.Name, separator, fieldsCounter[field.Name])
|
||||
}
|
||||
fieldsUnique = append(fieldsUnique, EventField{
|
||||
Name: fieldName,
|
||||
Value: field.Value,
|
||||
})
|
||||
}
|
||||
return fieldsUnique
|
||||
}
|
223
systemdns/dnsevtlog/win_eventlog/util_test.go
Normal file
223
systemdns/dnsevtlog/win_eventlog/util_test.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func TestDecodeUTF16(t *testing.T) {
|
||||
testString := "Test String"
|
||||
utf16s := utf16.Encode([]rune(testString))
|
||||
var bytesUtf16 bytes.Buffer
|
||||
writer := io.Writer(&bytesUtf16)
|
||||
lb := len(utf16s)
|
||||
for i := 0; i < lb; i++ {
|
||||
word := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(word, utf16s[i])
|
||||
_, err := writer.Write(word)
|
||||
if err != nil {
|
||||
t.Errorf("error preparing UTF-16 test string")
|
||||
return
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
b []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Wrong UTF-16",
|
||||
args: args{b: append(bytesUtf16.Bytes(), byte('\x00'))},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "UTF-16",
|
||||
args: args{b: bytesUtf16.Bytes()},
|
||||
want: []byte(testString),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := DecodeUTF16(tt.args.b)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DecodeUTF16() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DecodeUTF16() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var xmlbroken = `
|
||||
<BrokenXML>
|
||||
<Data/>qq</Data>
|
||||
</BrokenXML>
|
||||
`
|
||||
|
||||
var xmldata = `
|
||||
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
|
||||
<UserData>
|
||||
<CbsPackageChangeState xmlns="http://manifests.microsoft.com/win/2004/08/windows/setup_provider">
|
||||
<IntendedPackageState>5111</IntendedPackageState>
|
||||
<ErrorCode><Code>0x0</Code></ErrorCode>
|
||||
</CbsPackageChangeState>
|
||||
</UserData>
|
||||
<EventData>
|
||||
<Data>2120-07-26T15:24:25Z</Data>
|
||||
<Data>RulesEngine</Data>
|
||||
<Data Name="Engine">RulesEngine</Data>
|
||||
</EventData>
|
||||
</Event>
|
||||
`
|
||||
|
||||
type testEvent struct {
|
||||
UserData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
} `xml:"UserData"`
|
||||
EventData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
} `xml:"EventData"`
|
||||
}
|
||||
|
||||
func TestUnrollXMLFields(t *testing.T) {
|
||||
container := testEvent{}
|
||||
err := xml.Unmarshal([]byte(xmldata), &container)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't unmarshal precooked xml string xmldata")
|
||||
return
|
||||
}
|
||||
|
||||
type args struct {
|
||||
data []byte
|
||||
fieldsUsage map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want1 []EventField
|
||||
want2 map[string]int
|
||||
}{
|
||||
{
|
||||
name: "Broken XML",
|
||||
args: args{
|
||||
data: []byte(xmlbroken),
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: nil,
|
||||
want2: map[string]int{},
|
||||
},
|
||||
{
|
||||
name: "EventData with non-unique names and one Name attr",
|
||||
args: args{
|
||||
data: container.EventData.InnerXML,
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: []EventField{
|
||||
{Name: "Data", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data", Value: "RulesEngine"},
|
||||
{Name: "Data_Engine", Value: "RulesEngine"},
|
||||
},
|
||||
want2: map[string]int{"Data": 2, "Data_Engine": 1},
|
||||
},
|
||||
{
|
||||
name: "UserData with non-unique names and three levels of depth",
|
||||
args: args{
|
||||
data: container.UserData.InnerXML,
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: []EventField{
|
||||
{Name: "CbsPackageChangeState_IntendedPackageState", Value: "5111"},
|
||||
{Name: "CbsPackageChangeState_ErrorCode_Code", Value: "0x0"},
|
||||
},
|
||||
want2: map[string]int{
|
||||
"CbsPackageChangeState_ErrorCode_Code": 1,
|
||||
"CbsPackageChangeState_IntendedPackageState": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := UnrollXMLFields(tt.args.data, tt.args.fieldsUsage, "_")
|
||||
if !reflect.DeepEqual(got, tt.want1) {
|
||||
t.Errorf("ExtractFields() got = %v, want %v", got, tt.want1)
|
||||
}
|
||||
if !reflect.DeepEqual(got1, tt.want2) {
|
||||
t.Errorf("ExtractFields() got1 = %v, want %v", got1, tt.want2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueFieldNames(t *testing.T) {
|
||||
type args struct {
|
||||
fields []EventField
|
||||
fieldsUsage map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []EventField
|
||||
}{
|
||||
{
|
||||
name: "Unique values",
|
||||
args: args{
|
||||
fields: []EventField{
|
||||
{Name: "Data", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data", Value: "RulesEngine"},
|
||||
{Name: "Engine", Value: "RulesEngine"},
|
||||
},
|
||||
fieldsUsage: map[string]int{"Data": 2, "Engine": 1},
|
||||
},
|
||||
want: []EventField{
|
||||
{Name: "Data_1", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data_2", Value: "RulesEngine"},
|
||||
{Name: "Engine", Value: "RulesEngine"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := UniqueFieldNames(tt.args.fields, tt.args.fieldsUsage, "_"); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("PrintFields() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
340
systemdns/dnsevtlog/win_eventlog/win_eventlog.go
Normal file
340
systemdns/dnsevtlog/win_eventlog/win_eventlog.go
Normal file
|
@ -0,0 +1,340 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// WinEventLog config
|
||||
type WinEventLog struct {
|
||||
Locale uint32 `yaml:"locale"`
|
||||
EventlogName string `yaml:"eventlog_name"`
|
||||
Query string `yaml:"xpath_query"`
|
||||
ProcessUserData bool `yaml:"process_userdata"`
|
||||
ProcessEventData bool `yaml:"process_eventdata"`
|
||||
Separator string `yaml:"separator"`
|
||||
OnlyFirstLineOfMessage bool `yaml:"only_first_line_of_message"`
|
||||
TimeStampFromEvent bool `yaml:"timestamp_from_event"`
|
||||
EventTags []string `yaml:"event_tags"`
|
||||
EventFields []string `yaml:"event_fields"`
|
||||
ExcludeFields []string `yaml:"exclude_fields"`
|
||||
ExcludeEmpty []string `yaml:"exclude_empty"`
|
||||
|
||||
subscription EvtHandle
|
||||
buf []byte
|
||||
}
|
||||
|
||||
var bufferSize = 1 << 14
|
||||
|
||||
var description = "Input plugin to collect Windows Event Log messages"
|
||||
|
||||
// Description for win_eventlog
|
||||
func (w *WinEventLog) Description() string {
|
||||
return description
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldExclude(field string) (should bool) {
|
||||
for _, excludePattern := range w.ExcludeFields {
|
||||
// Check if field name matches excluded list
|
||||
if matched, _ := filepath.Match(excludePattern, field); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldProcessField(field string) (should bool, list string) {
|
||||
for _, pattern := range w.EventTags {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
// Tags are not excluded
|
||||
return true, "tags"
|
||||
}
|
||||
}
|
||||
|
||||
for _, pattern := range w.EventFields {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
if w.shouldExclude(field) {
|
||||
return false, "excluded"
|
||||
}
|
||||
return true, "fields"
|
||||
}
|
||||
}
|
||||
return false, "excluded"
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldExcludeEmptyField(field string, fieldType string, fieldValue interface{}) (should bool) {
|
||||
for _, pattern := range w.ExcludeEmpty {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
switch fieldType {
|
||||
case "string":
|
||||
return len(fieldValue.(string)) < 1
|
||||
case "int":
|
||||
return fieldValue.(int) == 0
|
||||
case "uint32":
|
||||
return fieldValue.(uint32) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EvtSubscribe(logName, xquery string) (EvtHandle, error) {
|
||||
var logNamePtr, xqueryPtr *uint16
|
||||
|
||||
sigEvent, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer windows.CloseHandle(sigEvent)
|
||||
|
||||
logNamePtr, err = syscall.UTF16PtrFromString(logName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
xqueryPtr, err = syscall.UTF16PtrFromString(xquery)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
subsHandle, err := _EvtSubscribe(0, uintptr(sigEvent), logNamePtr, xqueryPtr,
|
||||
0, 0, 0, EvtSubscribeStartAtOldestRecord)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return subsHandle, nil
|
||||
}
|
||||
|
||||
func EvtSubscribeWithBookmark(logName, xquery string, bookMark EvtHandle) (EvtHandle, error) {
|
||||
var logNamePtr, xqueryPtr *uint16
|
||||
|
||||
sigEvent, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer windows.CloseHandle(sigEvent)
|
||||
|
||||
logNamePtr, err = syscall.UTF16PtrFromString(logName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
xqueryPtr, err = syscall.UTF16PtrFromString(xquery)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
subsHandle, err := _EvtSubscribe(0, uintptr(sigEvent), logNamePtr, xqueryPtr,
|
||||
bookMark, 0, 0, EvtSubscribeStartAfterBookmark)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return subsHandle, nil
|
||||
}
|
||||
|
||||
func fetchEventHandles(subsHandle EvtHandle) ([]EvtHandle, error) {
|
||||
var eventsNumber uint32
|
||||
var evtReturned uint32
|
||||
|
||||
eventsNumber = 5
|
||||
|
||||
eventHandles := make([]EvtHandle, eventsNumber)
|
||||
|
||||
err := _EvtNext(subsHandle, eventsNumber, &eventHandles[0], 0, 0, &evtReturned)
|
||||
if err != nil {
|
||||
if err == ERROR_INVALID_OPERATION && evtReturned == 0 {
|
||||
return nil, ERROR_NO_MORE_ITEMS
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return eventHandles[:evtReturned], nil
|
||||
}
|
||||
|
||||
type EventFetcher struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewEventFetcher() *EventFetcher {
|
||||
return &EventFetcher{}
|
||||
}
|
||||
|
||||
func (w *EventFetcher) FetchEvents(subsHandle EvtHandle, lang uint32) ([]Event, []EvtHandle, error) {
|
||||
if w.buf == nil {
|
||||
w.buf = make([]byte, bufferSize)
|
||||
}
|
||||
var events []Event
|
||||
|
||||
eventHandles, err := fetchEventHandles(subsHandle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, eventHandle := range eventHandles {
|
||||
if eventHandle != 0 {
|
||||
event, err := w.renderEvent(eventHandle, lang)
|
||||
if err == nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events, eventHandles, nil
|
||||
}
|
||||
|
||||
func Close(handles []EvtHandle) error {
|
||||
for i := 0; i < len(handles); i++ {
|
||||
err := _EvtClose(handles[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *EventFetcher) renderEvent(eventHandle EvtHandle, lang uint32) (Event, error) {
|
||||
var bufferUsed, propertyCount uint32
|
||||
|
||||
event := Event{}
|
||||
err := _EvtRender(0, eventHandle, EvtRenderEventXml, uint32(len(w.buf)), &w.buf[0], &bufferUsed, &propertyCount)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
eventXML, err := DecodeUTF16(w.buf[:bufferUsed])
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
err = xml.Unmarshal([]byte(eventXML), &event)
|
||||
if err != nil {
|
||||
// We can return event without most text values,
|
||||
// that way we will not loose information
|
||||
// This can happen when processing Forwarded Events
|
||||
return event, nil
|
||||
}
|
||||
|
||||
publisherHandle, err := openPublisherMetadata(0, event.Source.Name, lang)
|
||||
if err != nil {
|
||||
return event, nil
|
||||
}
|
||||
defer _EvtClose(publisherHandle)
|
||||
|
||||
// Populating text values
|
||||
keywords, err := formatEventString(EvtFormatMessageKeyword, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.Keywords = keywords
|
||||
}
|
||||
message, err := formatEventString(EvtFormatMessageEvent, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.Message = message
|
||||
}
|
||||
level, err := formatEventString(EvtFormatMessageLevel, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.LevelText = level
|
||||
}
|
||||
task, err := formatEventString(EvtFormatMessageTask, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.TaskText = task
|
||||
}
|
||||
opcode, err := formatEventString(EvtFormatMessageOpcode, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.OpcodeText = opcode
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func formatEventString(
|
||||
messageFlag EvtFormatMessageFlag,
|
||||
eventHandle EvtHandle,
|
||||
publisherHandle EvtHandle,
|
||||
) (string, error) {
|
||||
var bufferUsed uint32
|
||||
err := _EvtFormatMessage(publisherHandle, eventHandle, 0, 0, 0, messageFlag,
|
||||
0, nil, &bufferUsed)
|
||||
if err != nil && err != ERROR_INSUFFICIENT_BUFFER {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bufferUsed *= 2
|
||||
buffer := make([]byte, bufferUsed)
|
||||
bufferUsed = 0
|
||||
|
||||
err = _EvtFormatMessage(publisherHandle, eventHandle, 0, 0, 0, messageFlag,
|
||||
uint32(len(buffer)/2), &buffer[0], &bufferUsed)
|
||||
bufferUsed *= 2
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := DecodeUTF16(buffer[:bufferUsed])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var out string
|
||||
if messageFlag == EvtFormatMessageKeyword {
|
||||
// Keywords are returned as array of a zero-terminated strings
|
||||
splitZero := func(c rune) bool { return c == '\x00' }
|
||||
eventKeywords := strings.FieldsFunc(string(result), splitZero)
|
||||
// So convert them to comma-separated string
|
||||
out = strings.Join(eventKeywords, ",")
|
||||
} else {
|
||||
result := bytes.Trim(result, "\x00")
|
||||
out = string(result)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// openPublisherMetadata opens a handle to the publisher's metadata. Close must
|
||||
// be called on returned EvtHandle when finished with the handle.
|
||||
func openPublisherMetadata(
|
||||
session EvtHandle,
|
||||
publisherName string,
|
||||
lang uint32,
|
||||
) (EvtHandle, error) {
|
||||
p, err := syscall.UTF16PtrFromString(publisherName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
h, err := _EvtOpenPublisherMetadata(session, p, nil, lang, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
28
systemdns/dnsevtlog/win_eventlog/win_eventlog_notwindows.go
Normal file
28
systemdns/dnsevtlog/win_eventlog/win_eventlog_notwindows.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
159
systemdns/dnsevtlog/win_eventlog/win_eventlog_test.go
Normal file
159
systemdns/dnsevtlog/win_eventlog/win_eventlog_test.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWinEventLog_shouldExcludeEmptyField(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
fieldType string
|
||||
fieldValue interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
w *WinEventLog
|
||||
args args
|
||||
wantShould bool
|
||||
}{
|
||||
{
|
||||
name: "Not in list",
|
||||
args: args{field: "qq", fieldType: "string", fieldValue: ""},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
args: args{field: "test", fieldType: "string", fieldValue: ""},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-empty string",
|
||||
args: args{field: "test", fieldType: "string", fieldValue: "qq"},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Zero int",
|
||||
args: args{field: "test", fieldType: "int", fieldValue: int(0)},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-zero int",
|
||||
args: args{field: "test", fieldType: "int", fieldValue: int(-1)},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Zero uint32",
|
||||
args: args{field: "test", fieldType: "uint32", fieldValue: uint32(0)},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-zero uint32",
|
||||
args: args{field: "test", fieldType: "uint32", fieldValue: uint32(0xc0fefeed)},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotShould := tt.w.shouldExcludeEmptyField(tt.args.field, tt.args.fieldType, tt.args.fieldValue); gotShould != tt.wantShould {
|
||||
t.Errorf("WinEventLog.shouldExcludeEmptyField() = %v, want %v", gotShould, tt.wantShould)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWinEventLog_shouldProcessField(t *testing.T) {
|
||||
tags := []string{"Source", "Level*"}
|
||||
fields := []string{"EventID", "Message*"}
|
||||
excluded := []string{"Message*"}
|
||||
type args struct {
|
||||
field string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
w *WinEventLog
|
||||
args args
|
||||
wantShould bool
|
||||
wantList string
|
||||
}{
|
||||
{
|
||||
name: "Not in tags",
|
||||
args: args{field: "test"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Tags",
|
||||
args: args{field: "LevelText"},
|
||||
wantShould: true,
|
||||
wantList: "tags",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "Not in Fields",
|
||||
args: args{field: "EventId"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Fields",
|
||||
args: args{field: "EventID"},
|
||||
wantShould: true,
|
||||
wantList: "fields",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Fields and Excluded",
|
||||
args: args{field: "Messages"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotShould, gotList := tt.w.shouldProcessField(tt.args.field)
|
||||
if gotShould != tt.wantShould {
|
||||
t.Errorf("WinEventLog.shouldProcessField() gotShould = %v, want %v", gotShould, tt.wantShould)
|
||||
}
|
||||
if gotList != tt.wantList {
|
||||
t.Errorf("WinEventLog.shouldProcessField() gotList = %v, want %v", gotList, tt.wantList)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
229
systemdns/dnsevtlog/win_eventlog/zsyscall_windows.go
Normal file
229
systemdns/dnsevtlog/win_eventlog/zsyscall_windows.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015-2020 InfluxData Inc.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
//
|
||||
//revive:disable-next-line:var-naming
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// EvtHandle uintptr
|
||||
type EvtHandle uintptr
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
//revive:disable-next-line:var-naming
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
//revive:disable-next-line:var-naming
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
)
|
||||
|
||||
// EvtFormatMessageFlag defines the values that specify the message string from
|
||||
// the event to format.
|
||||
type EvtFormatMessageFlag uint32
|
||||
|
||||
// EVT_FORMAT_MESSAGE_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385525(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
// Format the event's message string.
|
||||
EvtFormatMessageEvent EvtFormatMessageFlag = iota + 1
|
||||
// Format the message string of the level specified in the event.
|
||||
EvtFormatMessageLevel
|
||||
// Format the message string of the task specified in the event.
|
||||
EvtFormatMessageTask
|
||||
// Format the message string of the task specified in the event.
|
||||
EvtFormatMessageOpcode
|
||||
// Format the message string of the keywords specified in the event. If the
|
||||
// event specifies multiple keywords, the formatted string is a list of
|
||||
// null-terminated strings. Increment through the strings until your pointer
|
||||
// points past the end of the used buffer.
|
||||
EvtFormatMessageKeyword
|
||||
//revive:enable:var-naming
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return nil
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modwevtapi = windows.NewLazySystemDLL("wevtapi.dll")
|
||||
|
||||
procEvtSubscribe = modwevtapi.NewProc("EvtSubscribe")
|
||||
procEvtRender = modwevtapi.NewProc("EvtRender")
|
||||
procEvtClose = modwevtapi.NewProc("EvtClose")
|
||||
procEvtNext = modwevtapi.NewProc("EvtNext")
|
||||
procEvtFormatMessage = modwevtapi.NewProc("EvtFormatMessage")
|
||||
procEvtOpenPublisherMetadata = modwevtapi.NewProc("EvtOpenPublisherMetadata")
|
||||
procEvtCreateBookmark = modwevtapi.NewProc("EvtCreateBookmark")
|
||||
procEvtUpdateBookmark = modwevtapi.NewProc("EvtUpdateBookmark")
|
||||
)
|
||||
|
||||
func _EvtSubscribe(session EvtHandle, signalEvent uintptr, channelPath *uint16, query *uint16, bookmark EvtHandle, context uintptr, callback syscall.Handle, flags EvtSubscribeFlag) (handle EvtHandle, err error) {
|
||||
r0, _, e1 := syscall.Syscall9(procEvtSubscribe.Addr(), 8, uintptr(session), uintptr(signalEvent), uintptr(unsafe.Pointer(channelPath)), uintptr(unsafe.Pointer(query)), uintptr(bookmark), uintptr(context), uintptr(callback), uintptr(flags), 0)
|
||||
handle = EvtHandle(r0)
|
||||
if handle == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtRender(context EvtHandle, fragment EvtHandle, flags EvtRenderFlag, bufferSize uint32, buffer *byte, bufferUsed *uint32, propertyCount *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procEvtRender.Addr(), 7, uintptr(context), uintptr(fragment), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferUsed)), uintptr(unsafe.Pointer(propertyCount)), 0, 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtClose(object EvtHandle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procEvtClose.Addr(), 1, uintptr(object), 0, 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtNext(resultSet EvtHandle, eventArraySize uint32, eventArray *EvtHandle, timeout uint32, flags uint32, numReturned *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procEvtNext.Addr(), 6, uintptr(resultSet), uintptr(eventArraySize), uintptr(unsafe.Pointer(eventArray)), uintptr(timeout), uintptr(flags), uintptr(unsafe.Pointer(numReturned)))
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtFormatMessage(publisherMetadata EvtHandle, event EvtHandle, messageID uint32, valueCount uint32, values uintptr, flags EvtFormatMessageFlag, bufferSize uint32, buffer *byte, bufferUsed *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procEvtFormatMessage.Addr(), 9, uintptr(publisherMetadata), uintptr(event), uintptr(messageID), uintptr(valueCount), uintptr(values), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferUsed)))
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtOpenPublisherMetadata(session EvtHandle, publisherIdentity *uint16, logFilePath *uint16, locale uint32, flags uint32) (handle EvtHandle, err error) {
|
||||
r0, _, e1 := syscall.Syscall6(procEvtOpenPublisherMetadata.Addr(), 5, uintptr(session), uintptr(unsafe.Pointer(publisherIdentity)), uintptr(unsafe.Pointer(logFilePath)), uintptr(locale), uintptr(flags), 0)
|
||||
handle = EvtHandle(r0)
|
||||
if handle == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateBookmark create a new bookmark from a string, if the string is empty a new bookrmak is created.
|
||||
// Use update the bookmark with an event to save the position, or render to get the bookmark data to store.
|
||||
func CreateBookmark(bookmark string) (EvtHandle, error) {
|
||||
bookmarkPtr, err := stringPointer(bookmark)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r0, _, e1 := syscall.Syscall(procEvtCreateBookmark.Addr(), 1, bookmarkPtr, 0, 0)
|
||||
handle := EvtHandle(r0)
|
||||
if handle == 0 {
|
||||
if e1 != 0 {
|
||||
return 0, errnoErr(e1)
|
||||
}
|
||||
return 0, syscall.EINVAL
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func UpdateBookmark(bookmark, event EvtHandle, buf []byte) (string, error) {
|
||||
r1, _, e1 := syscall.Syscall(procEvtUpdateBookmark.Addr(), 2, uintptr(bookmark), uintptr(event), 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
return "", errnoErr(e1)
|
||||
} else {
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
}
|
||||
var bufferUsed, propertyCount uint32
|
||||
err := _EvtRender(0, bookmark, EvtRenderBookmark, uint32(len(buf)), &buf[0], &bufferUsed, &propertyCount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bookMarkData, err := DecodeUTF16(buf[:bufferUsed])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bookMarkData), nil
|
||||
}
|
||||
|
||||
func stringPointer(s string) (uintptr, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
// last character is always nil and causes issue for decoding.
|
||||
ptr, err := syscall.UTF16PtrFromString(s[:len(s)-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uintptr(unsafe.Pointer(ptr)), nil
|
||||
|
||||
}
|
1
systemdns/module.go
Normal file
1
systemdns/module.go
Normal file
|
@ -0,0 +1 @@
|
|||
package systemdns
|
96
systemdns/winlogdns/winlog.go
Normal file
96
systemdns/winlogdns/winlog.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package winlogdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||
|
||||
procOpenEventLog = advapi32.NewProc("OpenEventLogW")
|
||||
procCloseEventLog = advapi32.NewProc("CloseEventLog")
|
||||
procReadEventLog = advapi32.NewProc("ReadEventLogW")
|
||||
)
|
||||
|
||||
type DNSLogReader struct {
|
||||
handle uintptr
|
||||
}
|
||||
|
||||
type Win32_NTLogEvent struct {
|
||||
Logfile string
|
||||
RecordID uint32
|
||||
EventCode uint16
|
||||
ProcessName string // Process name that generated the event
|
||||
ProcessID uint32 // Process ID (PID) of the generating process
|
||||
Message string // Event message containing DNS query and response details
|
||||
}
|
||||
|
||||
func NewDNSLogListener() (*DNSLogReader, error) {
|
||||
// Open event log.
|
||||
lr := new(DNSLogReader)
|
||||
if err := lr.openEventLog(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lr, nil
|
||||
}
|
||||
|
||||
func (lr *DNSLogReader) openEventLog() error {
|
||||
// Convert strings.
|
||||
host, err := syscall.UTF16PtrFromString("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := syscall.UTF16PtrFromString("DNS Client Events")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open event log for DNS client events.
|
||||
handle, _, err := procOpenEventLog.Call(
|
||||
uintptr(unsafe.Pointer(host)),
|
||||
uintptr(unsafe.Pointer(source)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set handle and return
|
||||
lr.handle = handle
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lr *DNSLogReader) readEventLog(readflags, recordoffset uint32, buffer []byte, numberofbytestoread uint32, bytesread, minnumberofbytesneeded *uint32) (*Win32_NTLogEvent, error) {
|
||||
ret, _, err := procReadEventLog.Call(
|
||||
uintptr(lr.handle),
|
||||
uintptr(readflags),
|
||||
uintptr(recordoffset),
|
||||
uintptr(unsafe.Pointer(&buffer[0])),
|
||||
uintptr(numberofbytestoread),
|
||||
uintptr(unsafe.Pointer(bytesread)),
|
||||
uintptr(unsafe.Pointer(minnumberofbytesneeded)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ret != 0 {
|
||||
return nil, fmt.Errorf("failed with return code %d", ret)
|
||||
}
|
||||
|
||||
// What do I do here?
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (lr *DNSLogReader) Close() error {
|
||||
ret, _, err := procCloseEventLog.Call(lr.handle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return fmt.Errorf("failed with return code %d", ret)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Add table
Reference in a new issue