Skip to content

Commit 1fa89cc

Browse files
author
Christoph Wurm
authored
[Auditbeat] Cherry-pick #12028 to 6.8: Login: Fix re-read of utmp files (#12108)
The `login` dataset is not using the previous file offset when reading new entries in a utmp file. As a result, whenever a new login event occurs, all records are re-read. Also expands the documentation, moves test files to testdata/, and adds a test case that adds a utmp record to the test file and re-reads it to make sure this bug does not happen again. (cherry picked from commit 683f4f7)
1 parent 44077a0 commit 1fa89cc

File tree

7 files changed

+152
-23
lines changed

7 files changed

+152
-23
lines changed

CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ https://github.com/elastic/beats/compare/v6.7.2...6.8[Check the HEAD diff]
3939

4040
- Package dataset: Log error when Homebrew is not installed. {pull}11667[11667]
4141
- Process dataset: Fixed a memory leak under Windows. {pull}12100[12100]
42+
- Login dataset: Fix re-read of utmp files. {pull}12028[12028]
4243

4344
*Filebeat*
4445

x-pack/auditbeat/module/system/login/_meta/docs.asciidoc

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@ experimental[]
44

55
This is the `login` dataset of the system module.
66

7-
It is implemented for Linux only.
7+
[float]
8+
=== Implementation
9+
10+
The `login` dataset is implemented for Linux only.
11+
12+
On Linux, the dataset reads the https://en.wikipedia.org/wiki/Utmp[utmp] files
13+
that keep track of logins and logouts to the system. They are usually located
14+
at `/var/log/wtmp` (successful logins) and `/var/log/btmp` (failed logins).
15+
16+
The file patterns used to locate the files can be configured using
17+
`login.wtmp_file_pattern` and `login.btmp_file_pattern`. By default,
18+
both the current files and any rotated files (e.g. `wtmp.1`, `wtmp.2`)
19+
are read.
20+
21+
utmp files are binary, but you can display their contents using the
22+
`utmpdump` utility.
823

924
[float]
1025
==== Example dashboard

x-pack/auditbeat/module/system/login/login_test.go

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ package login
88

99
import (
1010
"encoding/binary"
11+
"io"
1112
"io/ioutil"
1213
"net"
1314
"os"
15+
"path/filepath"
1416
"testing"
1517
"time"
1618

1719
"github.com/stretchr/testify/assert"
1820

1921
"github.com/elastic/beats/auditbeat/core"
22+
abtest "github.com/elastic/beats/auditbeat/testing"
2023
"github.com/elastic/beats/libbeat/common"
21-
"github.com/elastic/beats/libbeat/paths"
2224
mbtest "github.com/elastic/beats/metricbeat/mb/testing"
2325
)
2426

@@ -27,12 +29,13 @@ func TestData(t *testing.T) {
2729
t.Skip("Test only works on little-endian systems - skipping.")
2830
}
2931

30-
defer setup(t)()
32+
defer abtest.SetupDataDir(t)()
3133

3234
config := getBaseConfig()
33-
config["login.wtmp_file_pattern"] = "../../../tests/files/wtmp"
35+
config["login.wtmp_file_pattern"] = "./testdata/wtmp"
3436
config["login.btmp_file_pattern"] = ""
3537
f := mbtest.NewReportingMetricSetV2(t, config)
38+
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
3639

3740
events, errs := mbtest.ReportingFetchV2(f)
3841
if len(errs) > 0 {
@@ -45,21 +48,98 @@ func TestData(t *testing.T) {
4548
t.Fatalf("only one event expected, got %d", len(events))
4649
}
4750

51+
events[0].RootFields.Put("event.origin", "/var/log/wtmp")
4852
fullEvent := mbtest.StandardizeEvent(f, events[0], core.AddDatasetToEvent)
4953
mbtest.WriteEventToDataJSON(t, fullEvent, "")
5054
}
5155

52-
func TestFailedLogins(t *testing.T) {
56+
func TestWtmp(t *testing.T) {
5357
if byteOrder != binary.LittleEndian {
5458
t.Skip("Test only works on little-endian systems - skipping.")
5559
}
5660

57-
defer setup(t)()
61+
defer abtest.SetupDataDir(t)()
62+
63+
dir := setupTestDir(t)
64+
defer os.RemoveAll(dir)
65+
66+
wtmpFilepath := filepath.Join(dir, "wtmp")
67+
68+
config := getBaseConfig()
69+
config["login.wtmp_file_pattern"] = wtmpFilepath
70+
config["login.btmp_file_pattern"] = ""
71+
f := mbtest.NewReportingMetricSetV2(t, config)
72+
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
73+
74+
events, errs := mbtest.ReportingFetchV2(f)
75+
if len(errs) > 0 {
76+
t.Fatalf("received error: %+v", errs[0])
77+
}
78+
79+
if len(events) == 0 {
80+
t.Fatal("no events were generated")
81+
} else if len(events) != 1 {
82+
t.Fatalf("only one event expected, got %d", len(events))
83+
}
84+
85+
// utmpdump: [7] [14962] [ts/2] [vagrant ] [pts/2 ] [10.0.2.2 ] [10.0.2.2 ] [2019-01-24T09:51:51,367964+00:00]
86+
checkFieldValue(t, events[0].RootFields, "event.kind", "event")
87+
checkFieldValue(t, events[0].RootFields, "event.action", "user_login")
88+
checkFieldValue(t, events[0].RootFields, "event.outcome", "success")
89+
checkFieldValue(t, events[0].RootFields, "process.pid", 14962)
90+
checkFieldValue(t, events[0].RootFields, "source.ip", "10.0.2.2")
91+
checkFieldValue(t, events[0].RootFields, "user.name", "vagrant")
92+
checkFieldValue(t, events[0].RootFields, "user.terminal", "pts/2")
93+
assert.True(t, events[0].Timestamp.Equal(time.Date(2019, 1, 24, 9, 51, 51, 367964000, time.UTC)),
94+
"Timestamp is not equal: %+v", events[0].Timestamp)
95+
96+
// Append logout event to wtmp file and check that it's read
97+
wtmpFile, err := os.OpenFile(wtmpFilepath, os.O_APPEND|os.O_WRONLY, 0644)
98+
if err != nil {
99+
t.Fatalf("error opening %v: %v", wtmpFilepath, err)
100+
}
101+
102+
loginUtmp := utmpC{
103+
Type: DEAD_PROCESS,
104+
}
105+
copy(loginUtmp.Device[:], "pts/2")
106+
107+
err = binary.Write(wtmpFile, byteOrder, loginUtmp)
108+
if err != nil {
109+
t.Fatalf("error writing to %v: %v", wtmpFilepath, err)
110+
}
111+
112+
events, errs = mbtest.ReportingFetchV2(f)
113+
if len(errs) > 0 {
114+
t.Fatalf("received error: %+v", errs[0])
115+
}
116+
117+
if len(events) == 0 {
118+
t.Fatal("no events were generated")
119+
} else if len(events) != 1 {
120+
t.Fatalf("only one event expected, got %d: %v", len(events), events)
121+
}
122+
123+
checkFieldValue(t, events[0].RootFields, "event.kind", "event")
124+
checkFieldValue(t, events[0].RootFields, "event.action", "user_logout")
125+
checkFieldValue(t, events[0].RootFields, "process.pid", 14962)
126+
checkFieldValue(t, events[0].RootFields, "source.ip", "10.0.2.2")
127+
checkFieldValue(t, events[0].RootFields, "user.name", "vagrant")
128+
checkFieldValue(t, events[0].RootFields, "user.terminal", "pts/2")
129+
}
130+
131+
func TestBtmp(t *testing.T) {
132+
if byteOrder != binary.LittleEndian {
133+
t.Skip("Test only works on little-endian systems - skipping.")
134+
}
135+
136+
defer abtest.SetupDataDir(t)()
58137

59138
config := getBaseConfig()
60139
config["login.wtmp_file_pattern"] = ""
61-
config["login.btmp_file_pattern"] = "../../../tests/files/btmp_ubuntu1804"
140+
config["login.btmp_file_pattern"] = "./testdata/btmp*"
62141
f := mbtest.NewReportingMetricSetV2(t, config)
142+
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
63143

64144
events, errs := mbtest.ReportingFetchV2(f)
65145
if len(errs) > 0 {
@@ -144,14 +224,47 @@ func getBaseConfig() map[string]interface{} {
144224
}
145225
}
146226

147-
// setup is copied from file_integrity/metricset_test.go.
148-
// TODO: Move to shared location and use in all unit tests.
149-
func setup(t testing.TB) func() {
150-
// path.data should be set so that the DB is written to a predictable location.
151-
var err error
152-
paths.Paths.Data, err = ioutil.TempDir("", "beat-data-dir")
227+
// setupTestDir creates a temporary directory, copies the test files into it,
228+
// and returns the path.
229+
func setupTestDir(t *testing.T) string {
230+
tmp, err := ioutil.TempDir("", "auditbeat-login-test-dir")
231+
if err != nil {
232+
t.Fatal("failed to create temp dir")
233+
}
234+
235+
copyDir(t, "./testdata", tmp)
236+
237+
return tmp
238+
}
239+
240+
func copyDir(t *testing.T, src, dst string) {
241+
files, err := ioutil.ReadDir(src)
242+
if err != nil {
243+
t.Fatalf("failed to read %v", src)
244+
}
245+
246+
for _, file := range files {
247+
srcFile := filepath.Join(src, file.Name())
248+
dstFile := filepath.Join(dst, file.Name())
249+
copyFile(t, srcFile, dstFile)
250+
}
251+
}
252+
253+
func copyFile(t *testing.T, src, dst string) {
254+
in, err := os.Open(src)
255+
if err != nil {
256+
t.Fatalf("failed to open %v", src)
257+
}
258+
defer in.Close()
259+
260+
out, err := os.Create(dst)
261+
if err != nil {
262+
t.Fatalf("failed to open %v", dst)
263+
}
264+
defer out.Close()
265+
266+
_, err = io.Copy(out, in)
153267
if err != nil {
154-
t.Fatal()
268+
t.Fatalf("failed to copy %v to %v", src, dst)
155269
}
156-
return func() { os.RemoveAll(paths.Paths.Data) }
157270
}

x-pack/auditbeat/module/system/login/utmp.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,10 @@ func (r *UtmpFileReader) findFiles(filePattern string, utmpType UtmpType) ([]Utm
146146
}
147147

148148
utmpFiles = append(utmpFiles, UtmpFile{
149-
Inode: Inode(fileInfo.Sys().(*syscall.Stat_t).Ino),
150-
Path: path,
151-
Size: fileInfo.Size(),
152-
Offset: 0,
153-
Type: utmpType,
149+
Inode: Inode(fileInfo.Sys().(*syscall.Stat_t).Ino),
150+
Path: path,
151+
Size: fileInfo.Size(),
152+
Type: utmpType,
154153
})
155154
}
156155

@@ -178,6 +177,7 @@ func (r *UtmpFileReader) readNewInFile(loginRecordC chan<- LoginRecord, errorC c
178177
if !isKnownFile {
179178
r.log.Debugf("Found new file: %v (utmpFile=%+v)", utmpFile.Path, utmpFile)
180179
}
180+
utmpFile.Offset = savedUtmpFile.Offset
181181

182182
size := utmpFile.Size
183183
oldSize := savedUtmpFile.Size
@@ -211,7 +211,7 @@ func (r *UtmpFileReader) readNewInFile(loginRecordC chan<- LoginRecord, errorC c
211211
defer func() {
212212
// Once we start reading a file, we update the file record even if something fails -
213213
// otherwise we will just keep trying to re-read very frequently forever.
214-
r.updateSavedUtmpFile(utmpFile, f)
214+
err := r.updateSavedUtmpFile(utmpFile, f)
215215
if err != nil {
216216
errorC <- errors.Wrapf(err, "error updating file record for file %v", utmpFile.Path)
217217
}

x-pack/auditbeat/tests/system/test_metricsets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def test_metricset_login(self):
3434
"user.name", "user.terminal"]
3535

3636
config = {
37-
"login.wtmp_file_pattern": os.path.abspath(os.path.join(self.beat_path, "tests/files/wtmp")),
38-
"login.btmp_file_pattern": "-1"
37+
"login.wtmp_file_pattern": os.path.abspath(os.path.join(self.beat_path, "module/system/login/testdata/wtmp*")),
38+
"login.btmp_file_pattern": os.path.abspath(os.path.join(self.beat_path, "module/system/login/testdata/btmp*")),
3939
}
4040

4141
# Metricset is experimental and that generates a warning, TODO: remove later

0 commit comments

Comments
 (0)