[bug-hunter] auditd coalescing misattributes node on interleaved events
- Dominant language
- Go
- Stars
- 12.7k
- Forks
- 5k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 364
Description
## Impact
When audit logs from multiple nodes are interleaved (common with forwarded/aggregated streams), the coalescing parser can assign the wrong `auditd.data.node` to an event. This misattributes events to the wrong host, which can break host-level detection, triage, and auditing workflows.
## Reproduction Steps
1. Add this test to `libbeat/reader/auditd/coalesce_test.go` (new test function):
```go
func TestCoalescingInterleavedNodeIsolation(t *testing.T) {
lines := [][]byte{
[]byte(`node=host-a type=SYSCALL msg=audit(1626700000.000:610): arch=c000003e syscall=59 success=yes exit=0 a0=1 a1=2 a2=3 a3=4 items=0 ppid=100 pid=611 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=1 comm="a" exe="/usr/bin/a" key=(null)`),
[]byte(`node=host-b type=SYSCALL msg=audit(1626700000.001:611): arch=c000003e syscall=59 success=yes exit=0 a0=1 a1=2 a2=3 a3=4 items=0 ppid=100 pid=612 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=1 comm="b" exe="/usr/bin/b" key=(null)`),
[]byte(`type=EOE msg=audit(1626700000.000:610):`),
[]byte(`type=EOE msg=audit(1626700000.001:611):`),
}
r := &testReader{messages: lines}
p := NewParser(r, coalesceConfig(), logptest.NewTestingLogger(t, t.Name()))
got := map[uint32]string{}
for i := 0; i < 2; i++ {
msg, err := p.Next()
if err != nil {
t.Fatalf("Next() returned error: %v", err)
}
seqValue, _ := msg.Fields.GetValue("auditd.sequence")
seq, ok := seqValue.(uint32)
if !ok {
t.Fatalf("auditd.sequence = %v (%T); want uint32", seqValue, seqValue)
}
nodeValue, _ := msg.Fields.GetValue("auditd.data.node")
node, ok := nodeValue.(string)
if !ok {
t.Fatalf("auditd.data.node = %v (%T); want string", nodeValue, nodeValue)
}
got[seq] = node
}
if got[610] != "host-a" {
t.Errorf("sequence 610 node = %q; want %q", got[610], "host-a")
}
if got[611] != "host-b" {
t.Errorf("sequence 611 node = %q; want %q", got[611], "host-b")
}
_, err := p.Next()
if !errors.Is(err, io.EOF) {
t.Errorf("third Next() = %v; want io.EOF", err)
}
}
```
2. Run:
```bash
go test ./libbeat/reader/auditd -run TestCoalescingInterleavedNodeIsolation -count=1
```
## Expected vs Actual
**Expected:** Sequence `610` keeps `auditd.data.node="host-a"` and sequence `611` keeps `auditd.data.node="host-b"`.
**Actual:** Sequence `610` receives `"host-b"` (the most recently seen node), proving node leakage across in-flight sequences.
Command output:
```text
--- FAIL: TestCoalescingInterleavedNodeIsolation (0.00s)
coalesce_test.go:340: sequence 610 node = "host-b"; want "host-a"
FAIL
FAIL github.com/elastic/beats/v7/libbeat/reader/auditd 0.008s
FAIL
```
## Failing Test
The full failing test is included above in **Reproduction Steps**.
## Evidence
- Global parser state stores the most recently seen node, not per-sequence node:
- `libbeat/reader/auditd/coalesce.go:47` — `node string // most recently seen node= value`
- Node value is overwritten while reading lines, regardless of sequence:
- `libbeat/reader/auditd/coalesce.go:165-168`
- Emitted event uses global node field:
- `libbeat/reader/auditd/coalesce.go:230-236`
- `git blame` shows this logic was introduced in commit `f4497618bcf` (`libbeat/reader/auditd: add multi-record coalescing mode`).
---
[What is this?](https://ela.st/github-ai-tools) | [From workflow: Bug Hunter](https://github.com/elastic/beats/actions/runs/34592438640)
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
> - [x] expires on Sep 18, 2026, 11:21 AM UTC
Contributor guide
Research direction
Start with libbeat/reader/auditd/coalesce.go, especially the node state at line 47, node handling at lines 165-168, and event emission at lines 230-236. Add the supplied regression test to libbeat/reader/auditd/coalesce_test.go and run go test ./libbeat/reader/auditd -run TestCoalescingInterleavedNodeIsolation -count=1. Done means sequences 610 and 611 retain host-a and host-b respectively, and the following call returns io.EOF.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100