diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 185d5a5..f884369 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,6 +23,7 @@ jobs: runs-on: ubuntu-latest container: image: registry.fedoraproject.org/fedora:38 + options: --privileged steps: - uses: actions/checkout@v3 - name: Install dependencies @@ -32,6 +33,14 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: | + cargo test --verbose --workspace --exclude crypto-auditing-agent + - name: Run integration tests + run: | + cd agent + cargo test --verbose -- --skip test_probe_no_coalesce --skip test_probe_coalesce + cargo test --verbose test_probe_no_coalesce + cargo test --verbose test_probe_coalesce + cd - - name: Run clippy run: cargo clippy diff --git a/Cargo.lock b/Cargo.lock index f8fa03e..e9d1af1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "agenttest" +version = "0.1.0" +dependencies = [ + "anyhow", + "libbpf-cargo", + "libbpf-rs", + "libc", + "nix", + "plain", + "tempfile", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -408,6 +421,7 @@ dependencies = [ name = "crypto-auditing-agent" version = "0.1.0" dependencies = [ + "agenttest", "anyhow", "bytes", "clap 4.4.2", @@ -419,8 +433,11 @@ dependencies = [ "nix", "openssl", "page_size", + "plain", + "probe", "serde", "serde_cbor", + "tempfile", "time", "tokio", "tokio-uring", @@ -1356,12 +1373,24 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "probe" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e81fcf486280f0b8b18ca43ceafd32739eb0eb703eb024a8d00814eeba556" + [[package]] name = "proc-macro-crate" version = "1.3.1" diff --git a/agent/.cargo/config.toml b/agent/.cargo/config.toml new file mode 100644 index 0000000..2967cf3 --- /dev/null +++ b/agent/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = "sudo -E" diff --git a/agent/Cargo.toml b/agent/Cargo.toml index d34753d..25edb89 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -32,3 +32,8 @@ probe = "0.3" [build-dependencies] libbpf-cargo = { version = "0.20", features = ["novendor"] } + +[dev-dependencies] +tempfile = "3" +plain = "0.2" +agenttest = { path = "tests/agenttest" } diff --git a/agent/fixtures/agent.conf b/agent/fixtures/agent.conf new file mode 100644 index 0000000..44ae95c --- /dev/null +++ b/agent/fixtures/agent.conf @@ -0,0 +1,7 @@ +# library = ["/usr/lib64/libgnutls.so.30", "/usr/lib64/libssl.so.3"] +# log_file = "/var/log/crypto-auditing/audit.cborseq" +# user = "crypto-auditing:crypto-auditing" +# coalesce_window = 100 +# max_events = 1000000 + +coalesce_window = 0 diff --git a/agent/tests/agenttest/Cargo.toml b/agent/tests/agenttest/Cargo.toml new file mode 100644 index 0000000..15b6494 --- /dev/null +++ b/agent/tests/agenttest/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "agenttest" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-or-later" +authors = ["The crypto-auditing developers"] + +[dependencies] +anyhow = "1.0" +libbpf-rs = { version = "0.20", features = ["novendor"] } +libc = "0.2" +nix = "0.26" +tempfile = "3" +plain = "0.2" + +[build-dependencies] +libbpf-cargo = { version = "0.20", features = ["novendor"] } diff --git a/agent/tests/agenttest/build.rs b/agent/tests/agenttest/build.rs new file mode 100644 index 0000000..ffbaf23 --- /dev/null +++ b/agent/tests/agenttest/build.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0 + +use libbpf_cargo::SkeletonBuilder; +use std::{env, path::PathBuf}; + +const SRC: &str = "src/bpf/agent.bpf.c"; + +fn main() { + let mut out = + PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set in build script")); + out.push("agent.skel.rs"); + SkeletonBuilder::new() + .source(SRC) + .build_and_generate(&out) + .unwrap(); + println!("cargo:rerun-if-changed={}", SRC); +} diff --git a/agent/tests/agenttest/src/bpf/agent.bpf.c b/agent/tests/agenttest/src/bpf/agent.bpf.c new file mode 100644 index 0000000..23580e0 --- /dev/null +++ b/agent/tests/agenttest/src/bpf/agent.bpf.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#include "vmlinux.h" +#include + +#define MAX_DATA_SIZE 512 + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 4096 /* one page */); +} ringbuf SEC(".maps"); + +SEC("usdt") +int +BPF_USDT(event_group, long count) +{ + long *value; + long err; + + value = bpf_ringbuf_reserve (&ringbuf, sizeof(*value), 0); + if (value) + { + *value = count; + bpf_ringbuf_submit (value, 0); + } + + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/agent/tests/agenttest/src/bpf/vmlinux.h b/agent/tests/agenttest/src/bpf/vmlinux.h new file mode 120000 index 0000000..71c5aea --- /dev/null +++ b/agent/tests/agenttest/src/bpf/vmlinux.h @@ -0,0 +1 @@ +../../../../src/bpf/vmlinux.h \ No newline at end of file diff --git a/agent/tests/agenttest/src/lib.rs b/agent/tests/agenttest/src/lib.rs new file mode 100644 index 0000000..e2223a7 --- /dev/null +++ b/agent/tests/agenttest/src/lib.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023 The crypto-auditing developers. + +use anyhow::{bail, Result}; +use libbpf_rs::{Link, Map, Object, RingBufferBuilder}; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Child; +use std::time::Duration; + +mod skel { + include!(concat!(env!("OUT_DIR"), "/agent.skel.rs")); +} +use skel::*; + +pub fn target_dir() -> PathBuf { + env::current_exe() + .ok() + .map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + .unwrap() +} + +pub fn agent_path() -> PathBuf { + target_dir().join("crypto-auditing-agent") +} + +pub fn bump_memlock_rlimit() -> Result<()> { + let rlimit = libc::rlimit { + rlim_cur: 128 << 20, + rlim_max: 128 << 20, + }; + + if unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlimit) } != 0 { + bail!("Failed to increase rlimit"); + } + + Ok(()) +} + +pub fn attach_bpf(process: &Child, path: impl AsRef) -> Result<(Link, Object)> { + let skel_builder = AgentSkelBuilder::default(); + let open_skel = skel_builder.open()?; + let mut skel = open_skel.load()?; + + let mut progs = skel.progs_mut(); + let prog = progs.event_group(); + + let link = prog + .attach_usdt( + process.id() as i32, + path.as_ref(), + "crypto_auditing_internal_agent", + "event_group", + ) + .expect("unable to attach prog"); + + Ok((link, skel.obj)) +} + +// Copied from libbpf-rs/libbpf-rs/tests/test.rs +pub fn with_ringbuffer(map: &Map, action: F, timeout: Duration) -> Result +where + F: FnOnce(), +{ + let mut value = 0i64; + { + let callback = |data: &[u8]| { + plain::copy_from_bytes(&mut value, data).expect("Wrong size"); + 0 + }; + + let mut builder = RingBufferBuilder::new(); + builder.add(map, callback)?; + let mgr = builder.build()?; + + action(); + mgr.poll(timeout)?; + mgr.consume()?; + } + + Ok(value) +} diff --git a/agent/tests/coalesce.rs b/agent/tests/coalesce.rs new file mode 100644 index 0000000..2229d22 --- /dev/null +++ b/agent/tests/coalesce.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023 The crypto-auditing developers. + +extern crate agenttest; +use agenttest::*; + +use crypto_auditing::types::EventGroup; +use probe::probe; +use serde_cbor::de::Deserializer; +use std::env; +use std::panic; +use std::path::PathBuf; +use std::process::Command; +use std::thread; +use std::time::Duration; +use tempfile::tempdir; + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures") +} + +#[test] +fn test_probe_coalesce() { + bump_memlock_rlimit().expect("unable to bump memlock rlimit"); + + let agent_path = agent_path(); + let log_dir = tempdir().expect("unable to create temporary directory"); + let log_path = log_dir.path().join("agent.log"); + let mut process = Command::new(&agent_path) + .arg("-c") + .arg(fixture_dir().join("agent.conf")) + .arg("--log-file") + .arg(&log_path) + .arg("--library") + .arg(&env::current_exe().unwrap()) + .arg("--coalesce-window") + .arg("1000") + .spawn() + .expect("unable to spawn agent"); + + // Wait until the agent process starts up + while !log_path.exists() { + thread::sleep(Duration::from_millis(100)); + } + + let result = panic::catch_unwind(|| { + let foo = String::from("foo\0"); + let bar = String::from("bar\0"); + let baz = String::from("baz\0"); + + let (_link, object) = + attach_bpf(&process, &agent_path).expect("unable to attach agent.bpf.o"); + let map = object.map("ringbuf").expect("unable to get ringbuf map"); + + let timeout = Duration::from_secs(10); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 1, 2); + probe!(crypto_auditing, word_data, 1, foo.as_ptr(), 3); + probe!(crypto_auditing, string_data, 1, bar.as_ptr(), bar.as_ptr()); + probe!( + crypto_auditing, + blob_data, + 1, + baz.as_ptr(), + baz.as_ptr(), + baz.len() + ); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 4); + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 4, 5); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let log_file = std::fs::File::open(&log_path) + .expect(&format!("unable to read file `{}`", log_path.display())); + + let groups: Result, _> = Deserializer::from_reader(&log_file) + .into_iter::() + .collect(); + let groups = groups.expect("error deserializing"); + assert_eq!(groups.len(), 2); + assert_eq!(groups[0].events().len(), 4); + assert_eq!(groups[1].events().len(), 1); + }); + + process.kill().expect("unable to kill agent"); + + assert!(result.is_ok()); +} diff --git a/agent/tests/no_coalesce.rs b/agent/tests/no_coalesce.rs new file mode 100644 index 0000000..3e18440 --- /dev/null +++ b/agent/tests/no_coalesce.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023 The crypto-auditing developers. + +extern crate agenttest; +use agenttest::*; + +use crypto_auditing::types::EventGroup; +use probe::probe; +use serde_cbor::de::Deserializer; +use std::env; +use std::panic; +use std::path::PathBuf; +use std::process::Command; +use std::thread; +use std::time::Duration; +use tempfile::tempdir; + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures") +} + +#[test] +fn test_probe_no_coalesce() { + bump_memlock_rlimit().expect("unable to bump memlock rlimit"); + + let agent_path = agent_path(); + let log_dir = tempdir().expect("unable to create temporary directory"); + let log_path = log_dir.path().join("agent.log"); + let mut process = Command::new(&agent_path) + .arg("-c") + .arg(fixture_dir().join("agent.conf")) + .arg("--log-file") + .arg(&log_path) + .arg("--library") + .arg(&env::current_exe().unwrap()) + .spawn() + .expect("unable to spawn agent"); + + // Wait until the agent starts up + while !log_path.exists() { + thread::sleep(Duration::from_millis(100)); + } + + let result = panic::catch_unwind(|| { + let foo = String::from("foo\0"); + let bar = String::from("bar\0"); + let baz = String::from("baz\0"); + + let (_link, object) = + attach_bpf(&process, &agent_path).expect("unable to attach agent.bpf.o"); + let map = object.map("ringbuf").expect("unable to get ringbuf map"); + + let timeout = Duration::from_secs(10); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 1, 2); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, word_data, 1, foo.as_ptr(), 3); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, string_data, 1, bar.as_ptr(), bar.as_ptr()); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + let result = with_ringbuffer( + map, + || { + probe!( + crypto_auditing, + blob_data, + 1, + baz.as_ptr(), + baz.as_ptr(), + baz.len() + ); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 4, 5); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let log_file = std::fs::File::open(&log_path) + .expect(&format!("unable to read file `{}`", log_path.display())); + + let groups: Result, _> = Deserializer::from_reader(&log_file) + .into_iter::() + .collect(); + let groups = groups.expect("error deserializing"); + assert_eq!(groups.len(), 5); + assert_eq!(groups[0].events().len(), 1); + assert_eq!(groups[1].events().len(), 1); + assert_eq!(groups[2].events().len(), 1); + assert_eq!(groups[3].events().len(), 1); + assert_eq!(groups[4].events().len(), 1); + }); + + process.kill().expect("unable to kill agent"); + + assert!(result.is_ok()); +}