Skip to content

Commit 2bc7f0e

Browse files
author
Christoph Wurm
committed
[Auditbeat] Login: Fix re-read of utmp files (#12028)
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 a9ae66e commit 2bc7f0e

File tree

7 files changed

+116
-12
lines changed

7 files changed

+116
-12
lines changed

CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ https://github.com/elastic/beats/compare/v7.0.0...7.1[Check the HEAD diff]
3636

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

4041
*Filebeat*
4142

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 @@ beta[]
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: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ package login
88

99
import (
1010
"encoding/binary"
11+
"io"
12+
"io/ioutil"
1113
"net"
14+
"os"
15+
"path/filepath"
1216
"testing"
1317
"time"
1418

@@ -28,7 +32,7 @@ func TestData(t *testing.T) {
2832
defer abtest.SetupDataDir(t)()
2933

3034
config := getBaseConfig()
31-
config["login.wtmp_file_pattern"] = "../../../tests/files/wtmp"
35+
config["login.wtmp_file_pattern"] = "./testdata/wtmp"
3236
config["login.btmp_file_pattern"] = ""
3337
f := mbtest.NewReportingMetricSetV2(t, config)
3438
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
@@ -56,8 +60,13 @@ func TestWtmp(t *testing.T) {
5660

5761
defer abtest.SetupDataDir(t)()
5862

63+
dir := setupTestDir(t)
64+
defer os.RemoveAll(dir)
65+
66+
wtmpFilepath := filepath.Join(dir, "wtmp")
67+
5968
config := getBaseConfig()
60-
config["login.wtmp_file_pattern"] = "../../../tests/files/wtmp"
69+
config["login.wtmp_file_pattern"] = wtmpFilepath
6170
config["login.btmp_file_pattern"] = ""
6271
f := mbtest.NewReportingMetricSetV2(t, config)
6372
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
@@ -85,6 +94,40 @@ func TestWtmp(t *testing.T) {
8594
checkFieldValue(t, events[0].RootFields, "user.terminal", "pts/2")
8695
assert.True(t, events[0].Timestamp.Equal(time.Date(2019, 1, 24, 9, 51, 51, 367964000, time.UTC)),
8796
"Timestamp is not equal: %+v", events[0].Timestamp)
97+
98+
// Append logout event to wtmp file and check that it's read
99+
wtmpFile, err := os.OpenFile(wtmpFilepath, os.O_APPEND|os.O_WRONLY, 0644)
100+
if err != nil {
101+
t.Fatalf("error opening %v: %v", wtmpFilepath, err)
102+
}
103+
104+
loginUtmp := utmpC{
105+
Type: DEAD_PROCESS,
106+
}
107+
copy(loginUtmp.Device[:], "pts/2")
108+
109+
err = binary.Write(wtmpFile, byteOrder, loginUtmp)
110+
if err != nil {
111+
t.Fatalf("error writing to %v: %v", wtmpFilepath, err)
112+
}
113+
114+
events, errs = mbtest.ReportingFetchV2(f)
115+
if len(errs) > 0 {
116+
t.Fatalf("received error: %+v", errs[0])
117+
}
118+
119+
if len(events) == 0 {
120+
t.Fatal("no events were generated")
121+
} else if len(events) != 1 {
122+
t.Fatalf("only one event expected, got %d: %v", len(events), events)
123+
}
124+
125+
checkFieldValue(t, events[0].RootFields, "event.kind", "event")
126+
checkFieldValue(t, events[0].RootFields, "event.action", "user_logout")
127+
checkFieldValue(t, events[0].RootFields, "process.pid", 14962)
128+
checkFieldValue(t, events[0].RootFields, "source.ip", "10.0.2.2")
129+
checkFieldValue(t, events[0].RootFields, "user.name", "vagrant")
130+
checkFieldValue(t, events[0].RootFields, "user.terminal", "pts/2")
88131
}
89132

90133
func TestBtmp(t *testing.T) {
@@ -96,7 +139,7 @@ func TestBtmp(t *testing.T) {
96139

97140
config := getBaseConfig()
98141
config["login.wtmp_file_pattern"] = ""
99-
config["login.btmp_file_pattern"] = "../../../tests/files/btmp_ubuntu1804"
142+
config["login.btmp_file_pattern"] = "./testdata/btmp*"
100143
f := mbtest.NewReportingMetricSetV2(t, config)
101144
defer f.(*MetricSet).utmpReader.bucket.DeleteBucket()
102145

@@ -190,3 +233,48 @@ func getBaseConfig() map[string]interface{} {
190233
"datasets": []string{"login"},
191234
}
192235
}
236+
237+
// setupTestDir creates a temporary directory, copies the test files into it,
238+
// and returns the path.
239+
func setupTestDir(t *testing.T) string {
240+
tmp, err := ioutil.TempDir("", "auditbeat-login-test-dir")
241+
if err != nil {
242+
t.Fatal("failed to create temp dir")
243+
}
244+
245+
copyDir(t, "./testdata", tmp)
246+
247+
return tmp
248+
}
249+
250+
func copyDir(t *testing.T, src, dst string) {
251+
files, err := ioutil.ReadDir(src)
252+
if err != nil {
253+
t.Fatalf("failed to read %v", src)
254+
}
255+
256+
for _, file := range files {
257+
srcFile := filepath.Join(src, file.Name())
258+
dstFile := filepath.Join(dst, file.Name())
259+
copyFile(t, srcFile, dstFile)
260+
}
261+
}
262+
263+
func copyFile(t *testing.T, src, dst string) {
264+
in, err := os.Open(src)
265+
if err != nil {
266+
t.Fatalf("failed to open %v", src)
267+
}
268+
defer in.Close()
269+
270+
out, err := os.Create(dst)
271+
if err != nil {
272+
t.Fatalf("failed to open %v", dst)
273+
}
274+
defer out.Close()
275+
276+
_, err = io.Copy(out, in)
277+
if err != nil {
278+
t.Fatalf("failed to copy %v to %v", src, dst)
279+
}
280+
}

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 beta and that generates a warning, TODO: remove later

0 commit comments

Comments
 (0)