Skip to content

Commit ae53ab4

Browse files
committed
Add integration test for events generated in containers
Because capturing information for a process running in a container is not fully possible from `/proc`, some information is passed to the Process constructor directly to overwrite it. This should be fine though, because we are using a specific container that should not have major changes on how it executes the command we are expecting.
1 parent 8c3fb0c commit ae53ab4

File tree

7 files changed

+99
-34
lines changed

7 files changed

+99
-34
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ log = { version = "0.4.22", default-features = false }
2424
prometheus-client = { version = "0.24.0", default-features = false }
2525
prost = "0.13.5"
2626
prost-types = "0.13.5"
27-
serde = { version = "1.0.219", features = ["derive"] }
27+
serde = { version = "1.0.219", features = ["derive", "rc"] }
2828
serde_json = "1.0.142"
2929
tokio = { version = "1.40.0", default-features = false, features = [
3030
"macros",

fact/src/cgroup.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::host_info::get_cgroup_paths;
1717

1818
#[derive(Debug)]
1919
struct ContainerIdEntry {
20-
container_id: Option<String>,
20+
container_id: Option<Arc<String>>,
2121
pub last_seen: SystemTime,
2222
}
2323

@@ -51,7 +51,7 @@ impl ContainerIdCache {
5151
})
5252
}
5353

54-
pub async fn get_container_id(&self, cgroup_id: u64) -> Option<String> {
54+
pub async fn get_container_id(&self, cgroup_id: u64) -> Option<Arc<String>> {
5555
let mut map = self.0.lock().await;
5656
match map.get(&cgroup_id) {
5757
Some(entry) => entry.container_id.clone(),
@@ -82,7 +82,7 @@ impl ContainerIdCache {
8282
})
8383
}
8484

85-
fn walk_cgroupfs(path: &PathBuf, map: &mut ContainerIdMap, parent_id: Option<&str>) {
85+
fn walk_cgroupfs(path: &PathBuf, map: &mut ContainerIdMap, parent_id: Option<Arc<String>>) {
8686
for entry in std::fs::read_dir(path).unwrap() {
8787
let entry = match entry {
8888
Ok(entry) => entry,
@@ -109,8 +109,8 @@ impl ContainerIdCache {
109109
.unwrap_or("");
110110
let container_id = match ContainerIdCache::extract_container_id(last_component)
111111
{
112-
Some(cid) => Some(cid),
113-
None => parent_id.map(|f| f.to_owned()),
112+
Some(cid) => Some(Arc::new(cid)),
113+
None => parent_id.clone(),
114114
};
115115
let last_seen = SystemTime::now();
116116
map.insert(
@@ -123,7 +123,7 @@ impl ContainerIdCache {
123123
container_id
124124
}
125125
};
126-
ContainerIdCache::walk_cgroupfs(&p, map, container_id.as_deref());
126+
ContainerIdCache::walk_cgroupfs(&p, map, container_id);
127127
}
128128
}
129129

fact/src/event.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#[cfg(test)]
22
use std::time::{SystemTime, UNIX_EPOCH};
3-
use std::{ffi::CStr, os::raw::c_char, path::PathBuf};
3+
use std::{ffi::CStr, os::raw::c_char, path::PathBuf, sync::Arc};
44

55
use fact_api::FileActivity;
66
use serde::Serialize;
@@ -60,7 +60,7 @@ pub struct Process {
6060
comm: String,
6161
args: Vec<String>,
6262
exe_path: String,
63-
container_id: Option<String>,
63+
container_id: Option<Arc<String>>,
6464
uid: u32,
6565
username: &'static str,
6666
gid: u32,
@@ -126,7 +126,7 @@ impl Process {
126126
.unwrap();
127127
let args = std::env::args().collect::<Vec<_>>();
128128
let cgroup = std::fs::read_to_string("/proc/self/cgroup").expect("Failed to read cgroup");
129-
let container_id = ContainerIdCache::extract_container_id(&cgroup);
129+
let container_id = ContainerIdCache::extract_container_id(&cgroup).map(Arc::new);
130130
let uid = unsafe { libc::getuid() };
131131
let gid = unsafe { libc::getgid() };
132132
let pid = std::process::id();
@@ -182,19 +182,16 @@ impl From<Process> for fact_api::ProcessSignal {
182182
lineage,
183183
} = value;
184184

185-
let container_id = container_id.unwrap_or("".to_string());
186-
187-
let args = args
188-
.into_iter()
189-
.reduce(|acc, i| acc + " " + &i)
190-
.unwrap_or("".to_owned());
185+
let container_id = container_id
186+
.map(Arc::unwrap_or_clone)
187+
.unwrap_or("".to_string());
191188

192189
Self {
193190
id: Uuid::new_v4().to_string(),
194191
container_id,
195192
creation_time: None,
196193
name: comm,
197-
args,
194+
args: args.join(" "),
198195
exec_file_path: exe_path,
199196
pid,
200197
uid,

tests/conftest.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import requests
1010

1111
from server import FileActivityService
12+
from logs import dump_logs
1213

1314

1415
@pytest.fixture
@@ -46,6 +47,18 @@ def docker_client():
4647
return docker.from_env()
4748

4849

50+
@pytest.fixture(scope='session', autouse=True)
51+
def docker_api_client():
52+
"""
53+
Create a docker API client, which is a lower level object and has
54+
access to more methods than the regular client.
55+
56+
Returns:
57+
A docker.APIClient object created with default values.
58+
"""
59+
return docker.APIClient()
60+
61+
4962
@pytest.fixture
5063
def server():
5164
"""
@@ -74,12 +87,6 @@ def get_image(request, docker_client):
7487
docker_client.images.pull(image)
7588

7689

77-
def dump_logs(container, file):
78-
logs = container.logs().decode('utf-8')
79-
with open(file, 'w') as f:
80-
f.write(logs)
81-
82-
8390
@pytest.fixture
8491
def fact(request, docker_client, monitored_dir, server, logs_dir):
8592
"""
@@ -120,6 +127,10 @@ def fact(request, docker_client, monitored_dir, server, logs_dir):
120127
'bind': '/host/usr/lib/os-release',
121128
'mode': 'ro',
122129
},
130+
'/sys/fs/cgroup/': {
131+
'bind': '/host/sys/fs/cgroup',
132+
'mode': 'ro',
133+
}
123134
},
124135
)
125136

tests/event.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ class Process:
3636
Represents a process with its attributes.
3737
"""
3838

39-
def __init__(self, pid: int | None = None):
39+
def __init__(self,
40+
pid: int | None = None,
41+
comm: str | None = None,
42+
exe_path: str | None = None,
43+
args: list[str] | None = None,
44+
):
4045
self._pid: int = pid if pid is not None else os.getpid()
4146
proc_dir = os.path.join('/proc', str(self._pid))
4247

@@ -54,16 +59,23 @@ def get_id(line: str, wanted_id: str) -> int | None:
5459
elif (gid := get_id(line, 'Gid')) is not None:
5560
self._gid: int = gid
5661

57-
self._exe_path: str = os.path.realpath(os.path.join(proc_dir, 'exe'))
58-
59-
with open(os.path.join(proc_dir, 'cmdline'), 'rb') as f:
60-
content = f.read(4096)
61-
args = [arg.decode('utf-8')
62-
for arg in content.split(b'\x00') if arg]
63-
self._args: str = ' '.join(args)
64-
65-
with open(os.path.join(proc_dir, 'comm'), 'r') as f:
66-
self._name: str = f.read().strip()
62+
self._exe_path: str = os.path.realpath(os.path.join(
63+
proc_dir, 'exe')) if exe_path is None else exe_path
64+
65+
if args is None:
66+
with open(os.path.join(proc_dir, 'cmdline'), 'rb') as f:
67+
content = f.read(4096)
68+
args = [arg.decode('utf-8')
69+
for arg in content.split(b'\x00') if arg]
70+
self._args: str = ' '.join(args)
71+
else:
72+
self._args = ' '.join(args)
73+
74+
if comm is None:
75+
with open(os.path.join(proc_dir, 'comm'), 'r') as f:
76+
self._name: str = f.read().strip()
77+
else:
78+
self._name = comm
6779

6880
with open(os.path.join(proc_dir, 'cgroup'), 'r') as f:
6981
self._container_id: str = extract_container_id(f.read())

tests/logs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def dump_logs(container, file):
2+
logs = container.logs().decode('utf-8')
3+
with open(file, 'w') as f:
4+
f.write(logs)

tests/test_file_open.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import json
12
import multiprocessing as mp
23
import os
34
import subprocess
45

6+
import pytest
7+
58
from event import Event, EventType, Process
9+
from logs import dump_logs
610

711

812
def test_open(fact, monitored_dir, server):
@@ -145,3 +149,40 @@ def do_test(fut: str, stop_event: mp.Event):
145149
finally:
146150
stop_event.set()
147151
proc.join(1)
152+
153+
154+
CONTAINER_CMD = 'mkdir -p {monitored_dir}; echo "Some content" > {monitored_dir}/test.txt ; sleep 5'
155+
156+
157+
@pytest.fixture(scope='function')
158+
def test_container(fact, docker_client, monitored_dir, logs_dir):
159+
image = 'fedora:42'
160+
command = f"bash -c '{CONTAINER_CMD.format(monitored_dir=monitored_dir)}'"
161+
container_log = os.path.join(logs_dir, 'fedora.log')
162+
container = docker_client.containers.run(
163+
image,
164+
detach=True,
165+
command=command,
166+
)
167+
yield container
168+
container.stop(timeout=1)
169+
container.wait(timeout=1)
170+
dump_logs(container, container_log)
171+
container.remove()
172+
173+
174+
def test_container_event(fact, monitored_dir, server, test_container, docker_api_client):
175+
fut = os.path.join(monitored_dir, 'test.txt')
176+
177+
inspect = docker_api_client.inspect_container(test_container.id)
178+
p = Process(pid=inspect['State']['Pid'],
179+
comm='bash',
180+
exe_path='/usr/bin/bash',
181+
args=['bash', '-c',
182+
CONTAINER_CMD.format(monitored_dir=monitored_dir)]
183+
)
184+
185+
creation = Event(process=p, event_type=EventType.CREATION, file=fut)
186+
print(f'Waiting for event: {creation}')
187+
188+
server.wait_events([creation])

0 commit comments

Comments
 (0)