├── requirements.txt ├── .gitignore ├── docker ├── bpf-lsm │ └── Dockerfile ├── rust │ └── Dockerfile ├── libbpf │ ├── patches │ │ ├── types.patch │ │ └── makefile.patch │ └── Dockerfile ├── README.md └── llvm │ ├── patches │ ├── LLVMConfig.patch │ └── strtoll.patch │ └── Dockerfile ├── probe-sys ├── src │ ├── constants.rs │ ├── lib.rs │ ├── traits.rs │ ├── errors.rs │ ├── helpers.rs │ ├── transform_generated.rs │ ├── query_writer.rs │ ├── probe_generated.rs │ ├── ffi_generated.rs │ ├── struct.proto │ └── compiler_generated.rs ├── Cargo.toml ├── build.rs └── templates │ ├── struct.proto.j2 │ ├── transform_generated.rs.j2 │ ├── ffi_generated.rs.j2 │ ├── serial_generated.rs.j2 │ ├── probe_generated.rs.j2 │ └── compiler_generated.rs.j2 ├── rule-compiler └── Cargo.toml ├── Vagrantfile ├── src ├── errors.rs ├── logging.rs ├── tests.rs ├── globals.rs ├── handler.rs ├── batcher.rs ├── main.rs └── client.rs ├── Cargo.toml ├── vm ├── README.md └── image.json ├── libprobe ├── src │ ├── include │ │ ├── probe.h │ │ ├── probe_common.h │ │ ├── probe_bpf.h │ │ ├── probe_macros.h │ │ └── probe.generated.h │ ├── probe.bpf.c │ └── probe.c ├── Makefile └── templates │ └── include │ └── probe.generated.h.j2 ├── Makefile ├── scripts ├── generate-template └── generate-structures ├── README.md ├── elasticsearch ├── bprm_check_security.json └── inode_unlink.json └── schemas ├── bprm_check_security.yml └── inode_unlink.yml /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.11.2 2 | MarkupSafe==1.1.1 3 | PyYAML==5.4.1 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .output 2 | src/probe 3 | .vagrant 4 | target 5 | probe 6 | rpms 7 | .cargo 8 | venv 9 | .vscode 10 | libprobe.a 11 | -------------------------------------------------------------------------------- /docker/bpf-lsm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM andrewstucki/libbpf-rust-builder:0.3 2 | 3 | RUN apk add --no-cache python3 && rustup default nightly 4 | -------------------------------------------------------------------------------- /docker/rust/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM andrewstucki/libbpf-builder:0.3 2 | 3 | ENV RUSTUP_HOME=/rust CARGO_HOME=/cargo PATH=/cargo/bin:/rust/bin:$PATH 4 | 5 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 6 | -------------------------------------------------------------------------------- /probe-sys/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const UNSET_OPERATOR: u8 = 0; 2 | pub(crate) const EQUAL_OPERATOR: u8 = 1; 3 | pub(crate) const NOT_EQUAL_OPERATOR: u8 = 2; 4 | pub(crate) const TRUE_ABSOLUTE: u8 = 1; 5 | pub(crate) const FALSE_ABSOLUTE: u8 = 2; 6 | -------------------------------------------------------------------------------- /rule-compiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rule-compiler" 3 | version = "0.0.1" 4 | authors = ["Andrew Stucki "] 5 | license = "GPL" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["rlib"] 10 | 11 | [dependencies] 12 | nom = "6.1.0" 13 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "andrewstucki/bpf" 6 | config.vm.box_version = "5.11-rc6-1612309303" 7 | config.vm.provider "virtualbox" do |v| 8 | v.memory = 2048 9 | v.cpus = 2 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /docker/libbpf/patches/types.patch: -------------------------------------------------------------------------------- 1 | diff --git a/include/linux/types.h b/include/linux/types.h 2 | index bae1ed8..b15252a 100644 3 | --- a/include/linux/types.h 4 | +++ b/include/linux/types.h 5 | @@ -7,6 +7,8 @@ 6 | #include 7 | #include 8 | 9 | +#include 10 | + 11 | #include 12 | #include 13 | 14 | -------------------------------------------------------------------------------- /docker/libbpf/patches/makefile.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/Makefile b/src/Makefile 2 | index bc25aba..8b2e140 100644 3 | --- a/src/Makefile 4 | +++ b/src/Makefile 5 | @@ -57,11 +57,7 @@ INSTALL = install 6 | 7 | DESTDIR ?= 8 | 9 | -ifeq ($(shell uname -m),x86_64) 10 | - LIBSUBDIR := lib64 11 | -else 12 | - LIBSUBDIR := lib 13 | -endif 14 | +LIBSUBDIR := lib 15 | 16 | PREFIX ?= /usr 17 | LIBDIR ?= $(PREFIX)/$(LIBSUBDIR) 18 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | EnqueuingError(String), 4 | } 5 | 6 | impl std::fmt::Display for Error { 7 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 8 | match *self { 9 | Error::EnqueuingError(ref e) => write!(f, "could not enqueue data: {}", e), 10 | } 11 | } 12 | } 13 | 14 | impl std::error::Error for Error { 15 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 16 | match *self { 17 | Error::EnqueuingError(..) => None, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker toolchain 2 | 3 | This directory contains the docker configuration for the main toolchain image used in compiling this project. Included alongside the Dockerfiles are LLVM and libbpf patches to get them to build against musl. Many of them were copied/modified from the latest Alpine clang package configurations I could find when I initially built this. 4 | 5 | Because building LLVM can be _painfully_ slow, the main tip to speed it up is to provision a 96-core latest Ubuntu box on AWS and run the `docker build` commands on it. This will likely cut down the build time for these images from a few hours to ~10 minutes. Once you're done, push the images to a docker hub repo and decommission the EC2 instance. 6 | -------------------------------------------------------------------------------- /probe-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "probe-sys" 3 | version = "0.0.1" 4 | authors = ["Andrew Stucki "] 5 | license = "GPL" 6 | edition = "2018" 7 | build = "build.rs" 8 | 9 | [lib] 10 | crate-type = ["rlib"] 11 | 12 | [dependencies] 13 | rule-compiler = { path = "../rule-compiler" } 14 | log = "0.4" 15 | once_cell = "1.5.2" 16 | users = "0.11.0" 17 | machine-uid = "0.2.0" 18 | sha2 = "0.9.3" 19 | pnet = "0.27.2" 20 | sysinfo = { version = "0.16.0", default-features = false } 21 | protobuf = { git = "https://github.com/stepancheg/rust-protobuf", rev="5f3ed259", features = ["with-bytes"] } 22 | 23 | [build-dependencies] 24 | protobuf-codegen-pure = { git = "https://github.com/stepancheg/rust-protobuf", rev="5f3ed259" } 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "probe" 3 | version = "0.0.1" 4 | authors = ["Andrew Stucki "] 5 | license = "GPL" 6 | edition = "2018" 7 | description = "Example BPF LSM in C/Rust" 8 | 9 | [dependencies] 10 | probe-sys = { path = "./probe-sys" } 11 | rule-compiler = { path = "./rule-compiler" } 12 | seahorse = "1.1" 13 | ureq = "2.0.1" 14 | ajson = "0.2" 15 | flate2 = "1.0.20" 16 | chrono = "0.4.19" 17 | log = "0.4" 18 | fern = "0.5" 19 | once_cell = "1.5.2" 20 | num_cpus = "1.13.0" 21 | webpki = "0.21.4" 22 | backoff = "0.3.0" 23 | instant = "0.1.9" 24 | base64 = "0.13.0" 25 | rustls = { version = "0.19.0", features = ["dangerous_configuration"] } 26 | uuid = { version = "0.8.2", features = ["v4"] } 27 | spmc = { git = "https://github.com/andrewstucki/spmc", rev = "5b7c142" } 28 | # For use as durable storage 29 | sled = { git = "https://github.com/spacejam/sled", rev = "d92e56d" } 30 | 31 | [workspace] 32 | members = [ "probe-sys" ] 33 | -------------------------------------------------------------------------------- /probe-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | extern crate protobuf_codegen_pure; 4 | 5 | fn main() { 6 | protobuf_codegen_pure::Codegen::new() 7 | .out_dir("src") 8 | .input("src/struct.proto") 9 | .include("src") 10 | .run() 11 | .expect("protoc"); 12 | 13 | if cfg!(target_os = "linux") { 14 | let src_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); 15 | 16 | println!( 17 | "cargo:rustc-link-search=native={}", 18 | src_dir.join("..").join("libprobe").to_str().unwrap() 19 | ); 20 | println!("cargo:rustc-link-search=native=/usr/lib"); 21 | println!("cargo:rustc-link-search=native=/lib"); 22 | println!("cargo:rustc-link-lib=static=probe"); 23 | println!("cargo:rustc-link-lib=static=bpf"); 24 | println!("cargo:rustc-link-lib=static=elf"); 25 | println!("cargo:rustc-link-lib=static=z"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use fern::Dispatch; 2 | use log::LevelFilter; 3 | 4 | pub fn setup_logger(level: LevelFilter) { 5 | let mut dispatch = Dispatch::new() 6 | .format(|out, message, record| { 7 | out.finish(format_args!( 8 | "{}[{}][{}] {}", 9 | chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), 10 | record.target(), 11 | record.level(), 12 | message 13 | )) 14 | }) 15 | .level(level); 16 | if level != LevelFilter::Debug { 17 | // ureq logs at too high of a log level by default 18 | dispatch = dispatch.level_for("ureq", LevelFilter::Warn); 19 | } 20 | let result = dispatch.chain(std::io::stderr()).apply(); 21 | if result.is_err() { 22 | eprintln!( 23 | "Error initializing logging: {:}", 24 | result.unwrap_err().to_string() 25 | ); 26 | std::process::exit(1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /probe-sys/templates/struct.proto.j2: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package probe.protobuf; 4 | 5 | option (rustproto.carllerche_bytes_for_bytes_all) = true; 6 | option (rustproto.carllerche_bytes_for_string_all) = true; 7 | option optimize_for = SPEED; 8 | 9 | {% for module in modules %}{% for structure in module.structures %} 10 | message {{structure.final}} { 11 | {% for field in structure.fields %} 12 | {% if not field.type.proto.startswith('repeated ') %}optional {% endif %}{{field.type.proto}} {{field.final}} = {{loop.index}} [json_name="{{field.original}}"]; 13 | {% endfor %} 14 | } 15 | {% endfor %}{% endfor %} 16 | 17 | message Event { 18 | enum EventType { 19 | {% for module in modules %} 20 | {% set entry_point = module.structures | last %}{{ entry_point.final | upper }} = {{loop.index0}}; 21 | {% endfor %} 22 | } 23 | required EventType event_type = 1; 24 | {% for module in modules %} 25 | {% set entry_point = module.structures | last %}optional {{ entry_point.final }} {{ entry_point.name }} = {{loop.index + 1}}; 26 | {% endfor %} 27 | } 28 | -------------------------------------------------------------------------------- /vm/README.md: -------------------------------------------------------------------------------- 1 | # VM generation 2 | 3 | This directory contains the kernel config for the Virtualbox image referenced in the Vagrantfile. Additionally, it has a packer configuration for generating a new Vagrant Cloud-hosted Vagrant box. 4 | 5 | For now, rebuilding the kernel deb and the Vagrant image is a fairly manual. Some tips to speed it up: 6 | 7 | 1. Provision a 96-core latest Ubuntu box on AWS. 8 | 2. Most of the instructions and dependencies for building a debian kernel package can be found [here](https://wiki.ubuntu.com/KernelTeam/GitKernelBuild). 9 | 3. Copy/upload the kernel config file in this directory. 10 | 4. You'll likely need a newer version of `pahole` than ships with whatever Ubuntu version comes in the EC2 box in order to handle the latest BPF/BTF changes. You can manually download/install the debian packages from [launchpad](https://launchpad.net/ubuntu/+source/dwarves-dfsg). 11 | 5. Once the kernel is done building, upload the package artifacts some place that can be pulled down on the public internet. Decommission the VM to avoid $$$. Run packer. 12 | -------------------------------------------------------------------------------- /probe-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::sync::Mutex; 3 | use users::UsersCache; 4 | 5 | mod constants; 6 | mod errors; 7 | mod helpers; 8 | mod query_writer; 9 | mod traits; 10 | 11 | // import all of the generated modules 12 | mod struct_pb; 13 | #[allow(non_camel_case_types)] 14 | #[allow(non_snake_case)] 15 | #[rustfmt::skip] 16 | mod compiler_generated; 17 | #[rustfmt::skip] 18 | #[allow(non_camel_case_types)] 19 | #[allow(non_snake_case)] 20 | mod ffi_generated; 21 | #[rustfmt::skip] 22 | mod probe_generated; 23 | #[rustfmt::skip] 24 | mod serial_generated; 25 | #[rustfmt::skip] 26 | mod transform_generated; 27 | 28 | pub use errors::{Error, SerializableResult, SerializationError}; 29 | pub use probe_generated::Probe; 30 | pub use serial_generated::*; 31 | pub use struct_pb::*; 32 | pub use traits::{ProbeHandler, SerializableEvent}; 33 | pub use transform_generated::{TransformationHandler, Transformer}; 34 | // for tests 35 | pub use query_writer::BpfQueryWriterFactory; 36 | 37 | static USERS_CACHE: Lazy> = Lazy::new(|| Mutex::new(UsersCache::new())); 38 | -------------------------------------------------------------------------------- /vm/image.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "cloud_token": "{{ env `VAGRANT_CLOUD_TOKEN` }}", 4 | "version": "5.11-rc6-{{timestamp}}" 5 | }, 6 | "builders": [ 7 | { 8 | "communicator": "ssh", 9 | "source_path": "ubuntu/hirsute64", 10 | "provider": "virtualbox", 11 | "add_force": true, 12 | "type": "vagrant" 13 | } 14 | ], 15 | "provisioners": [ 16 | { 17 | "type": "shell", 18 | "inline": [ 19 | "curl -s -o linux.deb https://andrewstucki-bpf-lsm-deb.s3.amazonaws.com/linux-image-5.11.0-rc6-bpf-lsm_5.11.0-rc6-bpf-lsm-1_amd64.deb", 20 | "sudo dpkg -i linux.deb", 21 | "rm linux.deb", 22 | "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sudo sh -s -- -y", 23 | "sudo apt-get update", 24 | "sudo apt-get install -y lldb", 25 | "sudo rm -rf /var/lib/apt/lists/*" 26 | ] 27 | } 28 | ], 29 | "post-processors": [ 30 | { 31 | "type": "vagrant-cloud", 32 | "box_tag": "andrewstucki/bpf", 33 | "access_token": "{{user `cloud_token`}}", 34 | "version": "{{user `version`}}" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /docker/llvm/patches/LLVMConfig.patch: -------------------------------------------------------------------------------- 1 | Fix LLVMConfig.cmake being generated to use correct LLVM_INSTALL_PREFIX 2 | (e.g. /usr/lib/llvm5). 3 | 4 | This is needed e.g. for building lldb. 5 | 6 | --- a/cmake/modules/CMakeLists.txt 7 | +++ b/cmake/modules/CMakeLists.txt 8 | @@ -87,15 +87,7 @@ 9 | 10 | # Generate LLVMConfig.cmake for the install tree. 11 | set(LLVM_CONFIG_CODE " 12 | -# Compute the installation prefix from this LLVMConfig.cmake file location. 13 | -get_filename_component(LLVM_INSTALL_PREFIX \"\${CMAKE_CURRENT_LIST_FILE}\" PATH)") 14 | -# Construct the proper number of get_filename_component(... PATH) 15 | -# calls to compute the installation prefix. 16 | -string(REGEX REPLACE "/" ";" _count "${LLVM_INSTALL_PACKAGE_DIR}") 17 | -foreach(p ${_count}) 18 | - set(LLVM_CONFIG_CODE "${LLVM_CONFIG_CODE} 19 | -get_filename_component(LLVM_INSTALL_PREFIX \"\${LLVM_INSTALL_PREFIX}\" PATH)") 20 | -endforeach(p) 21 | +set(LLVM_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\")") 22 | set(LLVM_CONFIG_INCLUDE_DIRS "\${LLVM_INSTALL_PREFIX}/include") 23 | set(LLVM_CONFIG_INCLUDE_DIR "${LLVM_CONFIG_INCLUDE_DIRS}") 24 | set(LLVM_CONFIG_MAIN_INCLUDE_DIR "${LLVM_CONFIG_INCLUDE_DIRS}") 25 | -------------------------------------------------------------------------------- /libprobe/src/include/probe.h: -------------------------------------------------------------------------------- 1 | #ifndef __PROBE_H 2 | #define __PROBE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "probe.generated.h" 13 | #include "probe.skel.h" 14 | #include "probe_macros.h" 15 | 16 | struct handle_event_wrapper { 17 | void *ctx; 18 | void *handler; 19 | }; 20 | 21 | DECLARE_HANDLERS(EVENT_HOOKS); 22 | struct handlers { 23 | DECLARE_HANDLER_WRAPPERS(EVENT_HOOKS); 24 | }; 25 | 26 | struct state_configuration { 27 | unsigned char debug; 28 | DECLARE_HANDLER_CONFIGURATIONS(EVENT_HOOKS); 29 | }; 30 | 31 | struct state { 32 | struct probe_bpf *obj; 33 | struct ring_buffer *rb; 34 | struct handlers *handlers; 35 | DECLARE_HOOKS(ALL_HOOKS); 36 | struct bpf_link *creds_hook; 37 | }; 38 | 39 | struct state *new_state(struct state_configuration config); 40 | void poll_state(struct state *s, int timeout); 41 | void cache_process(struct state *s, pid_t pid, 42 | const struct cached_process *process); 43 | void destroy_state(struct state *self); 44 | 45 | #endif // __PROBE_H 46 | -------------------------------------------------------------------------------- /probe-sys/src/traits.rs: -------------------------------------------------------------------------------- 1 | use rule_compiler::{Operation, Operator}; 2 | 3 | use crate::errors::SerializableResult; 4 | 5 | pub trait ProbeHandler { 6 | fn enqueue(&self, event: &mut T) -> Result<(), U> 7 | where 8 | T: SerializableEvent + std::fmt::Debug; 9 | } 10 | 11 | pub trait SerializableEvent { 12 | fn to_json(&self) -> SerializableResult; 13 | fn to_bytes(&self) -> SerializableResult>; 14 | fn enrich_common(&mut self) -> SerializableResult<&mut Self>; 15 | fn update_id(&mut self, id: &mut str); 16 | fn update_sequence(&mut self, seq: u64); 17 | fn suffix(&self) -> &'static str; 18 | } 19 | 20 | pub trait QueryStruct { 21 | fn set_absolute(&mut self, value: u8); 22 | fn set_number(&mut self, path: String, operator: Operator, value: u64) -> Result<(), String>; 23 | fn set_string(&mut self, path: String, operator: Operator, value: String) 24 | -> Result<(), String>; 25 | fn flush<'a>(&mut self, probe: &'a super::Probe<'a>) -> Result<(), String>; 26 | } 27 | 28 | pub(crate) trait QueryFlusher { 29 | fn apply_rule(&self, module: String, operation: Operation, rule: T); 30 | } 31 | -------------------------------------------------------------------------------- /docker/llvm/patches/strtoll.patch: -------------------------------------------------------------------------------- 1 | musl doesn't support strtoll_l(), so replace it with a simple strtoll() call. 2 | 3 | diff -Nru a/libcxx/include/locale b/libcxx/include/locale 4 | --- a/projects/libcxx/include/locale 5 | +++ b/projects/libcxx/include/locale 6 | @@ -695,7 +695,7 @@ 7 | typename remove_reference::type __save_errno = errno; 8 | errno = 0; 9 | char *__p2; 10 | - long long __ll = strtoll_l(__a, &__p2, __base, _LIBCPP_GET_C_LOCALE); 11 | + long long __ll = strtoll(__a, &__p2, __base); 12 | typename remove_reference::type __current_errno = errno; 13 | if (__current_errno == 0) 14 | errno = __save_errno; 15 | @@ -735,7 +735,7 @@ 16 | typename remove_reference::type __save_errno = errno; 17 | errno = 0; 18 | char *__p2; 19 | - unsigned long long __ll = strtoull_l(__a, &__p2, __base, _LIBCPP_GET_C_LOCALE); 20 | + unsigned long long __ll = strtoull(__a, &__p2, __base); 21 | typename remove_reference::type __current_errno = errno; 22 | if (__current_errno == 0) 23 | errno = __save_errno; 24 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | mod compiler { 2 | use probe_sys::BpfQueryWriterFactory; 3 | use rule_compiler::compile; 4 | 5 | #[test] 6 | fn test_error_missing_fields() { 7 | let rule = compile(r#"REJECT bprm_check_security WHEN x==1 and y==2 AND z==3 OR x==1 AND y==2 OR x==1 and y==2 and z==3 and a=="2""#).unwrap(); 8 | assert!(rule.encode(&BpfQueryWriterFactory::empty()).is_err()); 9 | } 10 | 11 | #[test] 12 | fn test_error_type_mismatch() { 13 | let rule = compile(r#"REJECT bprm_check_security WHEN user.id == "test""#).unwrap(); 14 | assert!(rule.encode(&BpfQueryWriterFactory::empty()).is_err()); 15 | } 16 | 17 | #[test] 18 | fn test_error_repeated() { 19 | let rule = compile(r#"REJECT bprm_check_security WHEN user.id == "a" and user.id != "b""#) 20 | .unwrap(); 21 | assert!(rule.encode(&BpfQueryWriterFactory::empty()).is_err()); 22 | } 23 | 24 | #[test] 25 | fn test_ok() { 26 | let rule = 27 | compile(r#"REJECT bprm_check_security WHEN process.executable == "/usr/bin/ls" and user.id == 1"#) 28 | .unwrap(); 29 | assert!(rule.encode(&BpfQueryWriterFactory::empty()).is_ok()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/globals.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::format; 3 | use std::sync::Mutex; 4 | 5 | use once_cell::sync::{Lazy, OnceCell}; 6 | use sled::Db; 7 | 8 | static DB_INSTANCE: OnceCell = OnceCell::new(); 9 | 10 | pub fn global_database() -> &'static Db { 11 | DB_INSTANCE.get().expect("database is not initialized") 12 | } 13 | 14 | pub fn initialize_global_database(db: Db) { 15 | DB_INSTANCE 16 | .set(db) 17 | .expect("database could not be initialized"); 18 | } 19 | 20 | static TEMPLATES: Lazy>> = Lazy::new(|| { 21 | let mut m = HashMap::new(); 22 | let bprm_check_security_data = include_bytes!("../elasticsearch/bprm_check_security.json"); 23 | m.insert("bprm_check_security", &bprm_check_security_data[..]); 24 | let inode_unlink_data = include_bytes!("../elasticsearch/inode_unlink.json"); 25 | m.insert("inode_unlink", &inode_unlink_data[..]); 26 | Mutex::new(m) 27 | }); 28 | 29 | pub fn get_template(name: &str) -> Result<&'static [u8], String> { 30 | let registry = TEMPLATES.lock().unwrap(); 31 | match registry.get(name) { 32 | Some(data) => Ok(data), 33 | None => Err(format!("invalid template name {}", name)), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libprobe/Makefile: -------------------------------------------------------------------------------- 1 | OUTPUT := .output 2 | CLANG ?= clang 3 | LLVM_STRIP ?= llvm-strip 4 | BPFTOOL ?= /usr/bin/bpftool 5 | INCLUDES := -I$(OUTPUT) -Isrc/include 6 | CFLAGS := -O3 -Os -fdata-sections -ffunction-sections -fvisibility=hidden 7 | BPFFLAGS := -g -O2 -D__TARGET_ARCH_x86 -D__KERNEL__ -target bpf 8 | LIB = libprobe.a 9 | SOURCES = src/probe.c 10 | BPF_SOURCES = src/probe.bpf.c 11 | HEADERS = src/include/vmlinux.h src/include/probe_common.h src/include/probe_bpf.h src/include/probe_macros.h src/include/probe.h src/include/probe.generated.h 12 | OBJECTS = $(SOURCES:src/%.c=$(OUTPUT)/%.o) 13 | BPF_OBJECTS = $(BPF_SOURCES:src/%.bpf.c=$(OUTPUT)/%.bpf.o) 14 | BPF_HEADERS = $(BPF_SOURCES:src/%.bpf.c=$(OUTPUT)/%.skel.h) 15 | 16 | .DEFAULT_GOAL := $(LIB) 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf $(OUTPUT) 21 | 22 | $(OUTPUT): 23 | mkdir -p $@ 24 | 25 | $(LIB): $(OBJECTS) $(BPF_OBJECTS) 26 | $(AR) rcs $@ $(OBJECTS) $(BPF_OBJECTS) 27 | 28 | $(OUTPUT)/%.o: src/%.c | $(BPF_HEADERS) 29 | $(CLANG) $(CFLAGS) $(INCLUDES) -c $< -o $@ 30 | 31 | $(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(BPF_OBJECTS) 32 | $(BPFTOOL) gen skeleton $< > $@ 33 | 34 | $(OUTPUT)/%.bpf.o: src/%.bpf.c | $(OUTPUT) $(HEADERS) 35 | $(CLANG) $(BPFFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ && \ 36 | $(LLVM_STRIP) -g $@ 37 | -------------------------------------------------------------------------------- /probe-sys/templates/transform_generated.rs.j2: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use protobuf::Message; 4 | 5 | use crate::errors::SerializableResult; 6 | use crate::struct_pb::*; 7 | use crate::traits::SerializableEvent; 8 | 9 | pub trait TransformationHandler { 10 | {% for module in modules %}{% set entry_point = module.structures | last %} 11 | fn enrich_{{module.name}}<'a>(&self, e: &'a mut {{entry_point.final}}) -> SerializableResult<&'a mut {{entry_point.final}}>; 12 | {% endfor %} 13 | } 14 | 15 | pub struct Transformer { 16 | handler: T, 17 | } 18 | 19 | impl Transformer { 20 | pub fn new(handler: T) -> Self { 21 | Self { handler: handler } 22 | } 23 | 24 | pub fn transform(&self, data: Vec) -> SerializableResult<(String, String)> { 25 | let e = Event::parse_from_bytes(&data).unwrap(); 26 | match e.get_event_type() { 27 | {% for module in modules %}{% set entry_point = module.structures | last %} 28 | event::EventType::{{entry_point.final | upper}} => { 29 | let json = self.handler.enrich_{{module.name}}((&mut e.{{entry_point.name}}.unwrap()).enrich_common()?)?.to_json()?; 30 | Ok((String::from("{{module.name}}"), json)) 31 | }, 32 | {% endfor %} 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /probe-sys/src/errors.rs: -------------------------------------------------------------------------------- 1 | use protobuf::json::PrintError; 2 | use protobuf::ProtobufError; 3 | use std::{error, fmt}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum Error { 7 | InitializationError, 8 | } 9 | 10 | impl fmt::Display for Error { 11 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 | match *self { 13 | Error::InitializationError => f.write_str( 14 | "Could not initialize BPF object, ensure you're using Linux kernel >= 4.18", 15 | ), 16 | } 17 | } 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum SerializationError { 22 | Json(PrintError), 23 | Bytes(ProtobufError), 24 | Transform(String), 25 | } 26 | 27 | impl fmt::Display for SerializationError { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::Json(_e) => write!(f, "json serialization failed"), 31 | Self::Bytes(e) => std::fmt::Display::fmt(&e, f), 32 | Self::Transform(e) => std::fmt::Display::fmt(&e, f), 33 | } 34 | } 35 | } 36 | 37 | impl error::Error for SerializationError { 38 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 39 | match self { 40 | Self::Json(_) => None, 41 | Self::Bytes(e) => Some(e), 42 | Self::Transform(_) => None, 43 | } 44 | } 45 | } 46 | 47 | pub type SerializableResult = Result; 48 | -------------------------------------------------------------------------------- /probe-sys/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use rule_compiler::Operator; 2 | use std::ffi::CStr; 3 | use std::os::raw::c_char; 4 | 5 | use crate::constants::{EQUAL_OPERATOR, FALSE_ABSOLUTE, NOT_EQUAL_OPERATOR, TRUE_ABSOLUTE}; 6 | 7 | pub(crate) fn transform_string(val: Vec) -> String { 8 | unsafe { CStr::from_ptr(val.as_ptr()).to_string_lossy().into_owned() } 9 | } 10 | 11 | pub(crate) fn convert_string_array(size: u64, arr: Vec) -> Vec 12 | where 13 | T: Into> + Copy, 14 | { 15 | let max_length = arr.len(); 16 | unsafe { 17 | let mut strings = vec![]; 18 | for x in 0..size { 19 | if size as usize >= max_length { 20 | break 21 | } 22 | let ptr: Vec = arr[x as usize].into(); 23 | let var = CStr::from_ptr(ptr.as_ptr()); 24 | let printable = var.to_string_lossy().into_owned(); 25 | strings.push(printable) 26 | } 27 | strings 28 | } 29 | } 30 | 31 | pub(crate) fn int_to_string(v: u64) -> String { 32 | v.to_string() 33 | } 34 | 35 | pub(crate) fn operator_to_constant(operator: Operator) -> u8 { 36 | match operator { 37 | Operator::Equal => EQUAL_OPERATOR, 38 | Operator::NotEqual => NOT_EQUAL_OPERATOR, 39 | } 40 | } 41 | 42 | pub(crate) fn absolute_to_constant(absolute: bool) -> u8 { 43 | match absolute { 44 | true => TRUE_ABSOLUTE, 45 | false => FALSE_ABSOLUTE, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /probe-sys/src/transform_generated.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use protobuf::Message; 4 | 5 | use crate::errors::SerializableResult; 6 | use crate::struct_pb::*; 7 | use crate::traits::SerializableEvent; 8 | 9 | pub trait TransformationHandler { 10 | fn enrich_bprm_check_security<'a>(&self, e: &'a mut BprmCheckSecurityEvent) -> SerializableResult<&'a mut BprmCheckSecurityEvent>; 11 | fn enrich_inode_unlink<'a>(&self, e: &'a mut InodeUnlinkEvent) -> SerializableResult<&'a mut InodeUnlinkEvent>; 12 | } 13 | 14 | pub struct Transformer { 15 | handler: T, 16 | } 17 | 18 | impl Transformer { 19 | pub fn new(handler: T) -> Self { 20 | Self { handler: handler } 21 | } 22 | 23 | pub fn transform(&self, data: Vec) -> SerializableResult<(String, String)> { 24 | let e = Event::parse_from_bytes(&data).unwrap(); 25 | match e.get_event_type() { 26 | event::EventType::BPRMCHECKSECURITYEVENT => { 27 | let json = self.handler.enrich_bprm_check_security((&mut e.bprm_check_security_event_t.unwrap()).enrich_common()?)?.to_json()?; 28 | Ok((String::from("bprm_check_security"), json)) 29 | }, 30 | event::EventType::INODEUNLINKEVENT => { 31 | let json = self.handler.enrich_inode_unlink((&mut e.inode_unlink_event_t.unwrap()).enrich_common()?)?.to_json()?; 32 | Ok((String::from("inode_unlink"), json)) 33 | }, 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /libprobe/src/include/probe_common.h: -------------------------------------------------------------------------------- 1 | #ifndef __PROBE_COMMON_H 2 | #define __PROBE_COMMON_H 3 | 4 | #ifndef EPERM 5 | #define EPERM 1 6 | #endif 7 | 8 | #define RINGBUFFER_FLAGS 0 9 | 10 | #define INLINE_STATIC __attribute__((always_inline)) static 11 | #define ARR_LENGTH(x) sizeof(x) / sizeof(x[0]) 12 | 13 | #ifndef memset 14 | #define memset(dest, chr, n) __builtin_memset((dest), (chr), (n)) 15 | #endif 16 | 17 | #ifndef memcpy 18 | #define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n)) 19 | #endif 20 | 21 | #ifndef memmove 22 | #define memmove(x, y, n) __builtin_memmove((x), (y), (n)) 23 | #endif 24 | 25 | INLINE_STATIC int ___strncmp(const char *x, const char *y, unsigned int len) { 26 | const char *a = x; 27 | const char *b = y; 28 | for (unsigned int i = 0; i < len; i++) { 29 | if (!*a && !*b) 30 | return 0; // we have a null byte at the same location 31 | if (*a != *b) 32 | return 1; 33 | a++; 34 | b++; 35 | } 36 | return 0; 37 | } 38 | 39 | #define SET_STRING(x, y) memcpy(x, y, ARR_LENGTH(x)) 40 | 41 | // rules checks 42 | #define MAX_RULE_SIZE 8 43 | #define TRUE_ABSOLUTE 1 44 | #define FALSE_ABSOLUTE 2 45 | #define EQUAL_OPERATOR 1 46 | #define NOT_EQUAL_OPERATOR 2 47 | #define NUMBER_EQUALITY(x, y) x == y; 48 | #define NUMBER_INEQUALITY(x, y) x != y; 49 | #define STRING_EQUALITY(x, y) ___strncmp(x, y, ARR_LENGTH(x)) == 0 50 | #define STRING_INEQUALITY(x, y) ___strncmp(x, y, ARR_LENGTH(x)) != 0 51 | 52 | #define MAX_PATH_SIZE 256 53 | #define MAX_ARGS 64 54 | #define ARGSIZE 128 55 | 56 | struct cached_process { 57 | char name[MAX_PATH_SIZE]; 58 | char executable[MAX_PATH_SIZE]; 59 | char args[MAX_ARGS][ARGSIZE]; 60 | unsigned long args_count; 61 | int truncated; 62 | }; 63 | 64 | struct cached_file { 65 | char path[MAX_PATH_SIZE]; 66 | }; 67 | 68 | #endif // __PROBE_COMMON_H 69 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIRECTORY := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | GENERATOR_SCRIPT = scripts/generate-structures 3 | SCHEMA_SCRIPT = scripts/generate-template 4 | CONTAINER := docker run --rm -v ${DIRECTORY}/.cargo:/cargo/registry -v ${DIRECTORY}/.cargo/git:/cargo/git -v ${DIRECTORY}:/src andrewstucki/bpf-lsm-builder:latest 5 | 6 | .DEFAULT_GOAL := build 7 | 8 | libprobe/libprobe.a: 9 | @$(CONTAINER) /bin/sh -c "make -C libprobe" 10 | 11 | .PHONY: build 12 | build: libprobe/libprobe.a 13 | @echo "Compiling release binary" 14 | @$(CONTAINER) /bin/sh -c "RUSTFLAGS=-Ctarget-feature=+crt-static cargo build --release && cp target/release/probe . && strip probe" 15 | 16 | .PHONY: debug 17 | debug: libprobe/libprobe.a 18 | @echo "Compiling debug binary" 19 | @$(CONTAINER) /bin/sh -c "RUSTFLAGS=-Ctarget-feature=+crt-static cargo build && cp target/debug/probe ." 20 | 21 | .PHONY: test 22 | test: libprobe/libprobe.a 23 | @echo "Running tests" 24 | @$(CONTAINER) /bin/sh -c "RUSTFLAGS=-Ctarget-feature=+crt-static cargo test" 25 | 26 | .PHONY: lint 27 | lint: 28 | @echo "Running lint" 29 | @$(CONTAINER) /bin/sh -c "RUSTFLAGS=-Ctarget-feature=+crt-static cargo clippy" 30 | 31 | .PHONY: test-rule-compiler 32 | test-rule-compiler: 33 | @echo "Running rule-compiler tests" 34 | @$(CONTAINER) /bin/sh -c "cd rule-compiler && RUSTFLAGS=-Ctarget-feature=+crt-static cargo test" 35 | 36 | venv: 37 | @echo "Setting up virtualenv" 38 | @$(CONTAINER) /bin/sh -c "python3 -m venv ./venv && source ./venv/bin/activate && pip install -r requirements.txt" 39 | 40 | .PHONY: generate 41 | generate: venv 42 | @echo "Generating files" 43 | @rm -rf libprobe/libprobe.a libprobe/.output target/*/build/probe-sys* 44 | @$(CONTAINER) /bin/sh -c "source ./venv/bin/activate && $(GENERATOR_SCRIPT)" 45 | 46 | .PHONY: schema 47 | schema: venv 48 | @echo "Generating Elasticsearch schema" 49 | @$(CONTAINER) /bin/sh -c "source ./venv/bin/activate && $(SCHEMA_SCRIPT)" 50 | -------------------------------------------------------------------------------- /docker/libbpf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim as bpftool-build 2 | ARG KERNEL_REPO=git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git 3 | ARG KERNEL_REF=master 4 | RUN apt-get update && \ 5 | apt-get upgrade -y && \ 6 | apt-get install -y --no-install-recommends \ 7 | gpg gpg-agent libelf-dev libmnl-dev libc-dev iptables libgcc-8-dev \ 8 | bash-completion binutils binutils-dev ca-certificates make git curl \ 9 | xz-utils gcc pkg-config bison flex build-essential python3 && \ 10 | apt-get purge --auto-remove && \ 11 | apt-get clean 12 | 13 | WORKDIR /tmp 14 | 15 | RUN \ 16 | git clone --depth 1 -b $KERNEL_REF $KERNEL_REPO && \ 17 | cd linux/tools/bpf/bpftool/ && \ 18 | sed -i '/CFLAGS += -O2/a CFLAGS += -static' Makefile && \ 19 | sed -i 's/LIBS = -lelf $(LIBBPF)/LIBS = -lelf -lz $(LIBBPF)/g' Makefile && \ 20 | printf 'feature-libbfd=0\nfeature-zlib=1\nfeature-libelf=1\nfeature-bpf=1\nfeature-libelf-mmap=1' >> FEATURES_DUMP.bpftool && \ 21 | FEATURES_DUMP=`pwd`/FEATURES_DUMP.bpftool make -j `getconf _NPROCESSORS_ONLN` && \ 22 | strip bpftool && \ 23 | ldd bpftool 2>&1 | grep -q -e "Not a valid dynamic program" \ 24 | -e "not a dynamic executable" || \ 25 | ( echo "Error: bpftool is not statically linked"; false ) && \ 26 | mv bpftool /usr/bin && rm -rf /tmp/linux 27 | 28 | FROM andrewstucki/llvm10rc3-musl-toolchain 29 | 30 | COPY --from=bpftool-build /usr/bin/bpftool /usr/bin/bpftool 31 | 32 | ADD patches /patches/ 33 | RUN ln -s /usr/bin/clang /usr/bin/cc && \ 34 | \ 35 | apk add --no-cache \ 36 | elfutils-dev \ 37 | zlib-static \ 38 | linux-headers \ 39 | bcc-static \ 40 | bcc-dev \ 41 | patch \ 42 | curl && \ 43 | \ 44 | mkdir -p /src/libbpf && cd /src/libbpf && \ 45 | curl -L "https://github.com/libbpf/libbpf/archive/v0.3.tar.gz" \ 46 | | tar --extract -xz --strip-components=1 && \ 47 | \ 48 | patch -p1 < /patches/makefile.patch && \ 49 | patch -p1 < /patches/types.patch && \ 50 | \ 51 | cd src && BUILD_STATIC_ONLY=y make install && \ 52 | \ 53 | printf "prefix=/usr\nlibdir=/usr/lib\nincludedir=${prefix}/include\n\nName: libelf\nDescription: ELF library\nVersion: 0.168-r3\nLibs: -L${libdir} -lelf\nCflags: -I${includedir}" > /usr/lib/pkgconfig/libelf.pc 54 | 55 | WORKDIR /src 56 | -------------------------------------------------------------------------------- /probe-sys/templates/ffi_generated.rs.j2: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use super::compiler_generated::*; 4 | use std::os::raw::{c_char, c_int, c_void}; 5 | 6 | #[repr(C)] 7 | #[derive(Copy, Clone)] 8 | pub struct cached_process { 9 | pub name: [c_char; 256], 10 | pub executable: [c_char; 256], 11 | pub args: [[c_char; 128]; 64], 12 | pub args_count: u64, 13 | pub truncated: i32, 14 | } 15 | 16 | impl Default for cached_process { 17 | fn default() -> Self { 18 | unsafe { std::mem::zeroed() } 19 | } 20 | } 21 | 22 | {% for module in modules %}{% set entry_point = module.structures | last %} 23 | {% for structure in module.structures %} 24 | {% if not structure.enrichment %} 25 | #[repr(C)] 26 | #[derive(Copy, Clone)] 27 | pub struct {{structure.name}} { 28 | {% for field in structure.fields %} 29 | {% if not field.enrichment %} 30 | pub {{field.name}}: {% if field.type.size is not none %}[{{field.type.rust}}; {{field.type.size}}]{% else %}{{field.type.rust}}{% endif %}, 31 | {% endif %} 32 | {% endfor %} 33 | } 34 | {% endif %} 35 | {% endfor %} 36 | 37 | pub type {{module.name}}_event_handler = extern "C" fn(ctx: *mut c_void, e: {{entry_point.name}}); 38 | {% endfor %} 39 | 40 | #[repr(C)] 41 | #[derive(Copy, Clone)] 42 | pub struct state_configuration { 43 | pub debug: bool, 44 | {% for module in modules %} 45 | pub {{module.name}}_ctx: *mut c_void, 46 | pub {{module.name}}_handler: {{module.name}}_event_handler, 47 | {% endfor %} 48 | } 49 | pub enum state {} 50 | extern "C" { 51 | pub fn new_state(config: state_configuration) -> *mut state; 52 | pub fn poll_state(_self: *mut state, timeout: c_int); 53 | pub fn destroy_state(_self: *mut state); 54 | pub fn cache_process(_self: *mut state, pid: i32, process: *const cached_process); 55 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 56 | pub fn flush_{{module.name}}_filter_rule(_self: *mut state, rule: query_bpf_{{entry_point.name}}); 57 | pub fn flush_{{module.name}}_rejection_rule(_self: *mut state, rule: query_bpf_{{entry_point.name}}); 58 | {% endif %}{% endfor %} 59 | } 60 | 61 | {% for module in modules %}{% set entry_point = module.structures | last %} 62 | pub unsafe fn unpack_{{module.name}}_closure(closure: &mut F) -> (*mut c_void, {{module.name}}_event_handler) 63 | where 64 | F: FnMut({{entry_point.name}}), 65 | { 66 | extern "C" fn trampoline(data: *mut c_void, e: {{entry_point.name}}) 67 | where 68 | F: FnMut({{entry_point.name}}), 69 | { 70 | let closure: &mut F = unsafe { &mut *(data as *mut F) }; 71 | (*closure)(e); 72 | } 73 | (closure as *mut F as *mut c_void, trampoline::) 74 | } 75 | {% endfor %} 76 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use probe_sys::{ 2 | BprmCheckSecurityEvent, InodeUnlinkEvent, ProbeHandler, SerializableEvent, SerializableResult, 3 | TransformationHandler, 4 | }; 5 | use std::path::Path; 6 | use uuid::Uuid; 7 | 8 | use crate::errors::Error; 9 | use crate::globals::global_database; 10 | 11 | #[derive(Copy, Clone)] 12 | pub struct Handler {} 13 | 14 | impl ProbeHandler for Handler { 15 | fn enqueue(&self, event: &mut T) -> Result<(), Error> 16 | where 17 | T: SerializableEvent + std::fmt::Debug, 18 | { 19 | let db = global_database(); 20 | let uuid = Uuid::new_v4(); 21 | let sequence = db 22 | .generate_id() 23 | .map_err(|e| Error::EnqueuingError(e.to_string()))?; 24 | 25 | let mut buffer = Uuid::encode_buffer(); 26 | let event_id = uuid.to_hyphenated().encode_lower(&mut buffer); 27 | event.update_id(event_id); 28 | event.update_sequence(sequence); 29 | 30 | let data = event 31 | .to_bytes() 32 | .map_err(|e| Error::EnqueuingError(e.to_string()))?; 33 | db.insert( 34 | [&sequence.to_be_bytes()[..], uuid.as_bytes()].concat(), 35 | data, 36 | ) 37 | .map_err(|e| Error::EnqueuingError(e.to_string()))?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl TransformationHandler for Handler { 43 | fn enrich_bprm_check_security<'a>( 44 | &self, 45 | e: &'a mut BprmCheckSecurityEvent, 46 | ) -> SerializableResult<&'a mut BprmCheckSecurityEvent> { 47 | let event = e.event.get_mut_ref(); 48 | event.set_kind("event".to_string()); 49 | event.set_category("process".to_string()); 50 | event.set_field_type("start".to_string()); 51 | event.set_module("bpf-lsm".to_string()); 52 | event.set_provider("bprm-check-security".to_string()); 53 | 54 | let process = e.process.get_mut_ref(); 55 | let command_line = process.args.join(" "); 56 | process.set_command_line(command_line); 57 | 58 | Ok(e) 59 | } 60 | 61 | fn enrich_inode_unlink<'a>( 62 | &self, 63 | e: &'a mut InodeUnlinkEvent, 64 | ) -> SerializableResult<&'a mut InodeUnlinkEvent> { 65 | let event = e.event.get_mut_ref(); 66 | event.set_kind("event".to_string()); 67 | event.set_category("file".to_string()); 68 | event.set_field_type("deletion".to_string()); 69 | event.set_module("bpf-lsm".to_string()); 70 | event.set_provider("inode-unlink".to_string()); 71 | 72 | let process = e.process.get_mut_ref(); 73 | let command_line = process.args.join(" "); 74 | process.set_command_line(command_line); 75 | 76 | let file = e.file.get_mut_ref(); 77 | let file_path = file.get_path(); 78 | let path = Path::new(file_path); 79 | let file_name = path.file_name().map(|f| f.to_string_lossy().to_string()); 80 | let file_parent = path.parent().map(|f| f.to_string_lossy().to_string()); 81 | let file_extension = path.extension().map(|f| f.to_string_lossy().to_string()); 82 | 83 | if let Some(name) = file_name { 84 | file.set_name(name) 85 | } 86 | if let Some(parent) = file_parent { 87 | file.set_directory(parent) 88 | } 89 | if let Some(extension) = file_extension { 90 | file.set_extension(extension) 91 | } 92 | 93 | Ok(e) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /libprobe/src/probe.bpf.c: -------------------------------------------------------------------------------- 1 | #include "probe_bpf.h" 2 | 3 | // Security hooks for program execution operations. 4 | 5 | __attribute__((always_inline)) int fork_trace(void *ctx) { 6 | // this is to handle programs which fork multiple times 7 | // without ever calling exec 8 | struct task_struct *current_task = 9 | (struct task_struct *)bpf_get_current_task(); 10 | struct cached_process *child = get_cached_process(current_task); 11 | struct cached_process *parent = 12 | get_cached_process(BPF_CORE_READ(current_task, real_parent)); 13 | if (parent && !child) { // we are in the child process 14 | // update the current task with the parent information 15 | update_cached_process(current_task, parent); 16 | } 17 | return 0; 18 | } 19 | 20 | TRACEPOINT(syscalls, sys_exit_fork, void *ctx) { return fork_trace(ctx); } 21 | 22 | TRACEPOINT(syscalls, sys_exit_vfork, void *ctx) { return fork_trace(ctx); } 23 | 24 | TRACEPOINT(syscalls, sys_exit_clone, void *ctx) { return fork_trace(ctx); } 25 | 26 | TRACEPOINT(syscalls, sys_exit_clone3, void *ctx) { return fork_trace(ctx); } 27 | 28 | TRACEPOINT(syscalls, sys_enter_execve, struct trace_event_raw_sys_enter *ctx) { 29 | struct task_struct *current_task = 30 | (struct task_struct *)bpf_get_current_task(); 31 | 32 | struct cached_process *cached = get_or_create_cached_process(current_task); 33 | if (!cached) 34 | return 0; 35 | const char *argp; 36 | 37 | const char *executable = (const char *)(ctx->args[0]); 38 | const char **args = (const char **)(ctx->args[1]); 39 | if (executable) { 40 | bpf_probe_read_user_str(&cached->executable, MAX_PATH_SIZE, executable); 41 | set_basename(cached->name, cached->executable, MAX_PATH_SIZE); 42 | } 43 | 44 | unsigned long argc = 0; 45 | 46 | #pragma unroll 47 | for (int i = 0; i < MAX_ARGS; i++) { 48 | // get the args from userspace since we can't retrieve 49 | // them in the lsm hook 50 | bpf_probe_read_user(&argp, sizeof(argp), &args[i]); 51 | if (!argp) 52 | goto done; 53 | bpf_probe_read_user_str(&cached->args[i], ARGSIZE, argp); 54 | argc++; 55 | } 56 | /* try to read one more argument to check if there is one */ 57 | bpf_probe_read_user(&argp, sizeof(argp), &args[MAX_ARGS]); 58 | if (!argp) 59 | goto done; 60 | 61 | /* pointer to max_args+1 isn't null, asume we have more arguments */ 62 | cached->truncated = 1; 63 | 64 | done: 65 | cached->args_count = argc; 66 | return 0; 67 | } 68 | 69 | TRACEPOINT(sched, sched_process_free, void *ctx) { 70 | struct task_struct *current_task = 71 | (struct task_struct *)bpf_get_current_task(); 72 | delete_cached_process(current_task); 73 | return 0; 74 | } 75 | 76 | COMPLETE_LSM_HOOK(bprm_check_security, execution, struct linux_binprm *bprm) 77 | 78 | // this isn't the greatest place to capture paths, but generally there's 79 | // an inode permission check some time prior to unlinking the file 80 | NOEVENT_LSM_HOOK(inode_getattr, const struct path *path) { 81 | struct cached_file *cached = get_or_create_cached_file(path->dentry->d_inode); 82 | if (cached) { 83 | bpf_d_path((struct path *)path, cached->path, MAX_PATH_SIZE); 84 | } 85 | return 0; 86 | } 87 | LSM_HOOK(inode_unlink, unlink, struct inode *dir, struct dentry *victim) { 88 | initialize_event(); 89 | struct cached_file *cached = get_cached_file(victim->d_inode); 90 | if (cached) { 91 | memcpy(event->file.path, cached->path, MAX_PATH_SIZE); 92 | event->file.inode = victim->d_inode->i_ino; 93 | } 94 | return 0; 95 | } 96 | -------------------------------------------------------------------------------- /libprobe/src/probe.c: -------------------------------------------------------------------------------- 1 | #include "probe.h" 2 | 3 | static int handle_event(void *ctx, void *data, unsigned long size) { 4 | struct bpf_event_t *event = data; 5 | struct handlers *handlers = ctx; 6 | switch (event->type) { HANDLER_CASES(EVENT_HOOKS); } 7 | return 0; 8 | } 9 | 10 | int print_libbpf_log(enum libbpf_print_level lvl, const char *fmt, 11 | va_list args) { 12 | return vfprintf(stderr, fmt, args); 13 | } 14 | 15 | int noop_log(enum libbpf_print_level lvl, const char *fmt, va_list args) { 16 | return 0; 17 | } 18 | 19 | INLINE_STATIC unsigned long get_clock_offset() { 20 | unsigned long clock_adjustment; 21 | struct timespec boot; 22 | struct timespec current; 23 | clock_gettime(CLOCK_BOOTTIME, &boot); 24 | clock_gettime(CLOCK_REALTIME, ¤t); 25 | unsigned long current_ns = 1000000000 * current.tv_sec + current.tv_nsec; 26 | unsigned long boot_ns = 1000000000 * boot.tv_sec + boot.tv_nsec; 27 | return current_ns - boot_ns; 28 | } 29 | 30 | INLINE_STATIC void destroy_handlers(struct handlers *h) { 31 | if (h) { 32 | DESTROY_HANDLERS(h, EVENT_HOOKS); 33 | free((void *)h); 34 | } 35 | h = NULL; 36 | } 37 | 38 | INLINE_STATIC struct handlers *new_handlers() { 39 | struct handlers *h = (struct handlers *)malloc(sizeof(struct handlers)); 40 | if (!h) { 41 | goto cleanup; 42 | } 43 | WRAP_HANDLERS_OR(h, cleanup, EVENT_HOOKS); 44 | 45 | goto done; 46 | 47 | cleanup: 48 | destroy_handlers(h); 49 | 50 | done: 51 | return h; 52 | } 53 | 54 | struct state *new_state(struct state_configuration config) { 55 | if (config.debug) { 56 | libbpf_set_print(print_libbpf_log); 57 | } else { 58 | libbpf_set_print(noop_log); 59 | } 60 | struct state *s = (struct state *)malloc(sizeof(struct state)); 61 | if (!s) { 62 | goto cleanup; 63 | } 64 | s->obj = NULL; 65 | s->rb = NULL; 66 | s->handlers = NULL; 67 | NULL_HOOKS(s, ALL_HOOKS); 68 | s->handlers = new_handlers(); 69 | if (!s->handlers) { 70 | goto cleanup; 71 | } 72 | s->obj = probe_bpf__open(); 73 | if (!s->obj) { 74 | goto cleanup; 75 | } 76 | s->obj->rodata->clock_adjustment = get_clock_offset(); 77 | 78 | SET_HANDLER_CONTEXTS(s, config, EVENT_HOOKS) 79 | 80 | if (probe_bpf__load(s->obj)) { 81 | goto cleanup; 82 | } 83 | 84 | s->rb = ring_buffer__new(bpf_map__fd(s->obj->maps.events), handle_event, 85 | (void *)s->handlers, NULL); 86 | if (!s->rb) { 87 | goto cleanup; 88 | } 89 | 90 | ATTACH_HOOKS_OR(s, cleanup, ALL_HOOKS); 91 | 92 | goto done; 93 | 94 | cleanup: 95 | destroy_state(s); 96 | s = NULL; 97 | 98 | done: 99 | return s; 100 | } 101 | 102 | void poll_state(struct state *s, int timeout) { 103 | if (s->rb) { 104 | ring_buffer__poll(s->rb, timeout); 105 | } 106 | } 107 | 108 | void cache_process(struct state *s, pid_t pid, 109 | const struct cached_process *process) { 110 | // this copies the data at the pointer into the kernel 111 | bpf_map_update_elem(bpf_map__fd(s->obj->maps.processes), &pid, process, 112 | BPF_ANY); 113 | } 114 | 115 | DECLARE_RULE_FLUSHERS(EVENT_HOOKS); 116 | 117 | void destroy_state(struct state *s) { 118 | if (s) { 119 | if (s->rb) { 120 | ring_buffer__free(s->rb); 121 | } 122 | DESTROY_HOOKS(s, ALL_HOOKS); 123 | if (s->obj) { 124 | probe_bpf__destroy(s->obj); 125 | } 126 | if (s->handlers) { 127 | destroy_handlers(s->handlers); 128 | } 129 | free((void *)s); 130 | } 131 | s = NULL; 132 | } 133 | -------------------------------------------------------------------------------- /libprobe/templates/include/probe.generated.h.j2: -------------------------------------------------------------------------------- 1 | // Code generated by scripts/generate-structures - DO NOT EDIT. 2 | // to modify, regenerate after modifying templates/probe_bpf.generated.h.j2 3 | 4 | // clang-format off 5 | 6 | #ifndef __PROBE__GENERATED_H 7 | #define __PROBE__GENERATED_H 8 | 9 | #include "probe_common.h" 10 | 11 | #define EVENT_HOOKS {{event_hooks}} 12 | #define ALL_HOOKS {{all_hooks}} 13 | 14 | {% for module in modules %} 15 | {{ module.render_c(loop.index0) }} 16 | {% endfor %} 17 | 18 | enum event_type { 19 | {% for module in modules %}{% set entry_point = module.structures | last %} 20 | type_{{ entry_point.name }}, 21 | {% endfor %} 22 | }; 23 | 24 | struct bpf_event_t { 25 | enum event_type type; 26 | union { 27 | {% for module in modules %}{% set entry_point = module.structures | last %} 28 | struct bpf_{{entry_point.name}} {{entry_point.name}}; 29 | {% endfor %} 30 | }; 31 | }; 32 | 33 | #ifdef BPF 34 | 35 | struct { 36 | __uint(type, BPF_MAP_TYPE_ARRAY); 37 | __uint(key_size, sizeof(u32)); 38 | __uint(value_size, sizeof(u32)); 39 | __uint(max_entries, {{ modules | length }}); 40 | } filter_rule_sizes SEC(".maps"); 41 | 42 | struct { 43 | __uint(type, BPF_MAP_TYPE_ARRAY); 44 | __uint(key_size, sizeof(u32)); 45 | __uint(value_size, sizeof(u32)); 46 | __uint(max_entries, {{ modules | length }}); 47 | } rejection_rule_sizes SEC(".maps"); 48 | 49 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 50 | INLINE_STATIC int ___test_{{module.name}}( 51 | struct bpf_{{entry_point.name}} *event, 52 | struct query_bpf_{{entry_point.name}} *rule 53 | ) { 54 | int conditional_true = 1; 55 | if (rule && event) { 56 | if (rule->___absolute == TRUE_ABSOLUTE) { 57 | return 1; 58 | } else if (rule->___absolute == FALSE_ABSOLUTE) { 59 | return 0; 60 | } else { 61 | {% for structure in module.structures %} 62 | {% for field in structure.fields %}{% if field.queryable and not field.complex %} 63 | if (rule->{{field.path}}{{field.name}}___operator == EQUAL_OPERATOR) { 64 | conditional_true = conditional_true && {{field.queryable | upper }}_EQUALITY(event->{{field.path}}{{field.name}},rule->{{field.path}}{{field.name}}); 65 | } else if (rule->{{field.path}}{{field.name}}___operator == NOT_EQUAL_OPERATOR) { 66 | conditional_true = conditional_true && {{field.queryable | upper }}_INEQUALITY(event->{{field.path}}{{field.name}}, rule->{{field.path}}{{field.name}}); 67 | } 68 | {% endif %}{% endfor %} 69 | {% endfor %} 70 | } 71 | } 72 | 73 | return conditional_true; 74 | } 75 | 76 | INLINE_STATIC int ___check_{{module.name}}( 77 | unsigned int size, 78 | void *rule_map, 79 | struct bpf_{{entry_point.name}} *event 80 | ) { 81 | int conditional_true = 0; 82 | if (!rule_map) return conditional_true; 83 | #pragma unroll 84 | for (int i = 0; i < MAX_RULE_SIZE; i++) { 85 | unsigned int index = i; 86 | if (index >= size) { 87 | return conditional_true; 88 | } 89 | struct query_bpf_{{entry_point.name}} *rule = bpf_map_lookup_elem(rule_map, &index); 90 | conditional_true = conditional_true || ___test_{{module.name}}(event, rule); 91 | } 92 | return conditional_true; 93 | } 94 | 95 | struct { 96 | __uint(type, BPF_MAP_TYPE_ARRAY); 97 | __uint(key_size, sizeof(u32)); 98 | __uint(value_size, sizeof(struct query_bpf_{{entry_point.name}})); 99 | __uint(max_entries, 8); 100 | } {{module.name}}_filters SEC(".maps"); 101 | 102 | struct { 103 | __uint(type, BPF_MAP_TYPE_ARRAY); 104 | __uint(key_size, sizeof(u32)); 105 | __uint(value_size, sizeof(struct query_bpf_{{entry_point.name}})); 106 | __uint(max_entries, 8); 107 | } {{module.name}}_rejections SEC(".maps"); 108 | {% endif %}{% endfor %} 109 | 110 | #endif 111 | 112 | #endif // __PROBE__GENERATED_H 113 | 114 | // clang-format on 115 | -------------------------------------------------------------------------------- /probe-sys/src/query_writer.rs: -------------------------------------------------------------------------------- 1 | use rule_compiler::{Atom, Operation, Operator, QueryWriter, QueryWriterFactory}; 2 | use std::fmt::Debug; 3 | 4 | use crate::compiler_generated::BpfQueryWriter; 5 | use crate::helpers::absolute_to_constant; 6 | use crate::traits::QueryStruct; 7 | 8 | pub(crate) struct InnerBpfQueryWriter { 9 | module: String, 10 | operation: Operation, 11 | current: T, 12 | conditionals: Vec, 13 | limit: usize, 14 | } 15 | 16 | impl InnerBpfQueryWriter { 17 | pub fn new(module: String, operation: Operation, limit: usize) -> Self { 18 | Self { 19 | module, 20 | operation, 21 | current: Default::default(), 22 | conditionals: vec![], 23 | limit, 24 | } 25 | } 26 | } 27 | 28 | impl QueryWriter for InnerBpfQueryWriter { 29 | fn write_statement<'a>( 30 | &mut self, 31 | field: &'a str, 32 | operator: &'a Operator, 33 | atom: &'a Atom, 34 | ) -> Result<(), String> { 35 | match atom { 36 | Atom::Number(value) => self 37 | .current 38 | .set_number(field.to_string(), *operator, *value)?, 39 | Atom::String(value) => { 40 | self.current 41 | .set_string(field.to_string(), *operator, value.to_string())? 42 | } 43 | }; 44 | Ok(()) 45 | } 46 | 47 | fn start_new_clause(&mut self) -> Result<(), String> { 48 | if self.conditionals.len() > self.limit { 49 | return Err(format!( 50 | "cannot add any more OR statements, max is {}", 51 | self.limit 52 | )); 53 | } 54 | if self.conditionals.is_empty() { 55 | self.conditionals.push(self.current); 56 | } 57 | self.current = Default::default(); 58 | Ok(()) 59 | } 60 | 61 | fn write_absolute(&mut self, value: bool) -> Result<(), String> { 62 | if self.conditionals.len() > self.limit { 63 | return Err(format!( 64 | "cannot add any more OR statements, max is {}", 65 | self.limit 66 | )); 67 | } 68 | self.current = Default::default(); 69 | self.current.set_absolute(absolute_to_constant(value)); 70 | self.conditionals.push(self.current); 71 | Ok(()) 72 | } 73 | 74 | fn flush(&mut self) -> Result<(), String> { 75 | Ok(()) 76 | } 77 | } 78 | 79 | impl InnerBpfQueryWriter { 80 | pub fn flush_probe<'a>(&mut self, probe: &'a super::Probe<'a>) -> Result<(), String> { 81 | let uninitialized: T = Default::default(); 82 | self.conditionals.push(self.current); 83 | for filter in &self.conditionals { 84 | if *filter != uninitialized { 85 | probe.apply_rule(self.module.clone(), self.operation, *filter) 86 | } 87 | } 88 | Ok(()) 89 | } 90 | } 91 | 92 | pub struct BpfQueryWriterFactory<'b> { 93 | probe: Option<&'b super::Probe<'b>>, 94 | } 95 | 96 | impl<'b> BpfQueryWriterFactory<'b> { 97 | #[allow(dead_code)] 98 | pub fn empty() -> Self { 99 | Self { probe: None } 100 | } 101 | 102 | pub fn new(probe: &'b super::Probe<'b>) -> Self { 103 | Self { probe: Some(probe) } 104 | } 105 | } 106 | 107 | impl<'b> QueryWriterFactory> for BpfQueryWriterFactory<'b> { 108 | fn create<'a>( 109 | &self, 110 | operation: Operation, 111 | table: &'a str, 112 | ) -> Result, String> { 113 | Ok(BpfQueryWriter::new( 114 | self.probe, 115 | table.to_string(), 116 | operation, 117 | )) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /scripts/generate-template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | from json import JSONEncoder 5 | from os import path 6 | from yaml import load, Loader 7 | 8 | CURRENT_DIRECTORY = path.dirname(path.realpath(__file__)) 9 | SCHEMAS_DIRECTORY = path.abspath(path.join(CURRENT_DIRECTORY, "..", "schemas")) 10 | OUTPUT_DIRECTORY = path.abspath(path.join(CURRENT_DIRECTORY, "..", 11 | "elasticsearch")) 12 | MODULES = ["bprm_check_security", "inode_unlink"] 13 | 14 | 15 | class Field: 16 | def __init__(self, field_type, format, ignore_above, properties): 17 | self.type = field_type 18 | self.ignore_above = ignore_above 19 | self.properties = properties 20 | self.format = format 21 | 22 | 23 | class FieldEncoder(JSONEncoder): 24 | def default(self, o): 25 | cleaned = {} 26 | for (k, v) in o.__dict__.items(): 27 | if v is not None: 28 | cleaned[k] = v 29 | return cleaned 30 | 31 | 32 | def tweak(data, override=None): 33 | fields = {} 34 | for f in data: 35 | if override == None: 36 | name = f.get("name") 37 | else: 38 | name = override 39 | ignore_above = None 40 | properties = None 41 | format = None 42 | field_type = f.get("type") 43 | if field_type == "date": 44 | format = "epoch_second" 45 | if field_type == "keyword": 46 | ignore_above = 1024 47 | if field_type == "group": 48 | field_type = None 49 | properties = tweak(f.get("fields")) 50 | if "." in name: 51 | tokens = name.split(".", 1) 52 | name = tokens[0] 53 | rest = tokens[1] 54 | field_type = None 55 | properties = tweak([f], rest) 56 | field = Field(field_type, format, ignore_above, properties) 57 | fields[name] = field 58 | return fields 59 | 60 | 61 | class Module: 62 | def __init__(self, filename): 63 | with open(filename) as file: 64 | self.name = path.splitext(path.basename(filename))[0] 65 | self.data = load(file, Loader=Loader) 66 | self.schema = {} 67 | 68 | def __wrap(self, fields): 69 | return { 70 | "index_patterns": [ 71 | "%s-*" % self.name 72 | ], 73 | "template": { 74 | "mappings": { 75 | "date_detection": False, 76 | "dynamic_templates": [ 77 | { 78 | "strings_as_keyword": { 79 | "mapping": { 80 | "ignore_above": 1024, 81 | "type": "keyword" 82 | }, 83 | "match_mapping_type": "string" 84 | } 85 | } 86 | ], 87 | "properties": fields, 88 | }, 89 | "settings": { 90 | "index": { 91 | "mapping": { 92 | "total_fields": { 93 | "limit": 10000 94 | } 95 | }, 96 | "refresh_interval": "5s" 97 | } 98 | } 99 | }, 100 | "priority": 1 101 | } 102 | 103 | def render(self): 104 | output_path = path.join(OUTPUT_DIRECTORY, "%s.json" % self.name) 105 | properties = tweak(self.data) 106 | rendered = json.dumps(self.__wrap(properties), 107 | indent=2, cls=FieldEncoder) 108 | with open(output_path, "w") as output: 109 | output.write(rendered) 110 | 111 | 112 | if __name__ == "__main__": 113 | schema_files = [ 114 | path.join(SCHEMAS_DIRECTORY, "%s.yml" % f) for f in MODULES 115 | ] 116 | modules = [Module(f) for f in schema_files] 117 | for module in modules: 118 | module.render() 119 | -------------------------------------------------------------------------------- /src/batcher.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error}; 2 | use std::sync::mpsc::RecvTimeoutError; 3 | use std::time::{Duration, SystemTime}; 4 | 5 | use crate::client::Client; 6 | use crate::globals::global_database; 7 | 8 | pub struct Batcher {} 9 | 10 | impl Batcher { 11 | pub fn run( 12 | local: bool, 13 | client: &Client, 14 | flush_rate: u64, 15 | max_batch_size: usize, 16 | max_batch_bytes: usize, 17 | workers: u32, 18 | ) { 19 | let (mut tx, rx) = spmc::channel(); 20 | for i in 0..workers { 21 | let worker_client = client.clone(); 22 | let transformer = probe_sys::Transformer::new(crate::handler::Handler {}); 23 | let rx = rx.clone(); 24 | let flush_timeout = Duration::new(flush_rate, 0); 25 | std::thread::spawn(move || { 26 | let mut batch = Vec::new(); 27 | let mut current_batch_bytes: usize = 0; 28 | let mut last_flush = SystemTime::now(); 29 | loop { 30 | match rx.recv_timeout(flush_timeout) { 31 | Ok((key, data)) => { 32 | match transformer.transform(data) { 33 | Ok((index, json)) => { 34 | current_batch_bytes += json.chars().count() + 1; // 1 == newline 35 | batch.push((key, (index, json))); 36 | } 37 | Err(e) => error!("worker {}: {:?}", i, e), 38 | }; 39 | } 40 | Err(RecvTimeoutError::Disconnected) => { 41 | error!("worker disconnected"); 42 | break; 43 | } 44 | Err(RecvTimeoutError::Timeout) => {} 45 | } 46 | let now = SystemTime::now(); 47 | let batch_size = batch.len(); 48 | // flush immediately if we have a clock reset 49 | let elapsed = now 50 | .duration_since(last_flush) 51 | .unwrap_or(flush_timeout) 52 | .as_secs(); 53 | if current_batch_bytes >= max_batch_bytes 54 | || batch_size >= max_batch_size 55 | || elapsed > flush_rate 56 | { 57 | if !local { 58 | match worker_client.send_batch(&batch) { 59 | Err(e) => error!("error sending batch: {}", e), 60 | _ => {} 61 | } 62 | } 63 | for (k, (_, v)) in &batch { 64 | if local { 65 | println!("{}", v); 66 | } 67 | match global_database().remove(k) { 68 | Ok(_) => debug!("worker {}: cleaned record", i), 69 | Err(e) => error!("worker {}: error removing record {:?}", i, e), 70 | } 71 | } 72 | batch.clear(); 73 | current_batch_bytes = 0; 74 | last_flush = now; 75 | } 76 | } 77 | }); 78 | } 79 | 80 | let mut subscriber = global_database().watch_prefix(vec![]); 81 | loop { 82 | match subscriber.next() { 83 | Some(event) => { 84 | for (_, key, data) in event.into_iter() { 85 | if data.is_some() { 86 | let value = data.clone().unwrap().to_vec(); 87 | let result = tx.send((key.clone(), value)); 88 | if result.is_err() { 89 | error!("sender: {}", result.unwrap_err().to_string()); 90 | } 91 | } 92 | } 93 | } 94 | None => { 95 | debug!("subscriber closed"); 96 | break; 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docker/llvm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11.3 2 | 3 | ADD patches /patches/ 4 | RUN apk add --update-cache \ 5 | clang-dev \ 6 | clang-static \ 7 | cmake \ 8 | g++ \ 9 | git \ 10 | libexecinfo-dev \ 11 | linux-headers \ 12 | make \ 13 | ninja \ 14 | patch \ 15 | python \ 16 | zlib-dev \ 17 | curl && \ 18 | rm -rf /var/cache/apk/* && \ 19 | \ 20 | mkdir -p /src/llvm && cd /src/llvm && \ 21 | \ 22 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/llvm-10.0.0rc3.src.tar.xz" \ 23 | | tar --extract --xz --strip-components=1 && \ 24 | \ 25 | mkdir -p projects/compiler-rt && \ 26 | \ 27 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/compiler-rt-10.0.0rc3.src.tar.xz" \ 28 | | tar --extract --xz --strip-components=1 --directory=projects/compiler-rt && \ 29 | \ 30 | mkdir -p projects/libcxx && \ 31 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/libcxx-10.0.0rc3.src.tar.xz" \ 32 | | tar --extract --xz --strip-components=1 --directory=projects/libcxx && \ 33 | \ 34 | mkdir -p projects/libcxxabi && \ 35 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/libcxxabi-10.0.0rc3.src.tar.xz" \ 36 | | tar --extract --xz --strip-components=1 --directory=projects/libcxxabi && \ 37 | \ 38 | mkdir -p projects/libunwind && \ 39 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/libunwind-10.0.0rc3.src.tar.xz" \ 40 | | tar --extract --xz --strip-components=1 --directory=projects/libunwind && \ 41 | \ 42 | mkdir -p tools/clang && \ 43 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/clang-10.0.0rc3.src.tar.xz" \ 44 | | tar --extract --xz --strip-components=1 --directory=tools/clang && \ 45 | \ 46 | mkdir -p tools/clang/tools/extra && \ 47 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/clang-tools-extra-10.0.0rc3.src.tar.xz" \ 48 | | tar --extract --xz --strip-components=1 --directory=tools/clang/tools/extra && \ 49 | \ 50 | mkdir -p tools/lld && \ 51 | curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0-rc3/lld-10.0.0rc3.src.tar.xz" \ 52 | | tar --extract --xz --strip-components=1 --directory=tools/lld && \ 53 | \ 54 | patch -p1 < /patches/LLVMConfig.patch && \ 55 | patch -p1 < /patches/strtoll.patch && \ 56 | \ 57 | mkdir -p build && cd build && \ 58 | cmake .. \ 59 | -DCMAKE_C_COMPILER=clang \ 60 | -DCMAKE_CXX_COMPILER=clang++ \ 61 | -DLIBCXXABI_LIBCXX_PATH=/src/llvm/projects/libcxx \ 62 | -DLIBCXXABI_LIBCXX_INCLUDES=/src/llvm/projects/libcxx/include \ 63 | -DLIBCXX_ENABLE_SHARED=OFF \ 64 | -DCLANG_DEFAULT_LINKER=lld \ 65 | -DLLVM_ENABLE_RTTI=ON \ 66 | -DLLVM_ENABLE_EH=ON \ 67 | -DLLVM_ENABLE_BINDINGS=OFF \ 68 | -DLIBCXXABI_ENABLE_STATIC_UNWINDER=ON \ 69 | -DLIBCXXABI_USE_LLVM_UNWINDER=ON \ 70 | -DLIBCXX_LIBCXXABI_INCLUDES_INTERNAL=/src/llvm/projects/libcxxabi/include \ 71 | -DLIBCXX_HAS_MUSL_LIBC=ON \ 72 | -DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=ON \ 73 | -DLIBUNWIND_ENABLE_SHARED=OFF \ 74 | -DCLANG_DEFAULT_CXX_STDLIB=libc++ \ 75 | -DCLANG_DEFAULT_RTLIB=compiler-rt \ 76 | -DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-pc-linux-musl \ 77 | -DCMAKE_INSTALL_PREFIX=/usr \ 78 | -DCMAKE_BUILD_TYPE=Release \ 79 | -DLLVM_TARGET_ARCH=x86_64 \ 80 | -DLLVM_TARGETS_TO_BUILD="X86;BPF" \ 81 | -DLIBCXXABI_USE_COMPILER_RT=ON \ 82 | -DLIBCXX_USE_COMPILER_RT=ON \ 83 | -DCOMPILER_RT_BUILD_BUILTINS=ON \ 84 | -DCOMPILER_RT_BUILD_SANITIZERS=OFF \ 85 | -DCOMPILER_RT_BUILD_XRAY=OFF \ 86 | -DCOMPILER_RT_USE_BUILTINS_LIBRARY=ON \ 87 | -G Ninja && \ 88 | ninja && \ 89 | ninja install && \ 90 | \ 91 | apk del --purge \ 92 | g++ \ 93 | git \ 94 | linux-headers \ 95 | patch \ 96 | python \ 97 | curl && \ 98 | \ 99 | apk --no-cache add libc-dev && \ 100 | \ 101 | cd / && rm -rf /src/llvm && \ 102 | ln -s /usr/bin/llvm-addr2line /usr/bin/addr2line && \ 103 | ln -s /usr/bin/llvm-ar /usr/bin/ar && \ 104 | ln -s /usr/bin/llvm-as /usr/bin/as && \ 105 | ln -s /usr/bin/llvm-dlltool /usr/bin/dlltool && \ 106 | ln -s /usr/bin/llvm-lipo /usr/bin/lipo && \ 107 | ln -s /usr/bin/llvm-nm /usr/bin/nm && \ 108 | ln -s /usr/bin/llvm-objcopy /usr/bin/objcopy && \ 109 | ln -s /usr/bin/llvm-objdump /usr/bin/objdump && \ 110 | ln -s /usr/bin/llvm-ranlib /usr/bin/ranlib && \ 111 | ln -s /usr/bin/llvm-readelf /usr/bin/readelf && \ 112 | ln -s /usr/bin/llvm-readobj /usr/bin/readobj && \ 113 | ln -s /usr/bin/llvm-rtdyld /usr/bin/rtdyld && \ 114 | ln -s /usr/bin/llvm-strip /usr/bin/strip 115 | -------------------------------------------------------------------------------- /probe-sys/templates/serial_generated.rs.j2: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use machine_uid; 4 | use pnet::datalink::interfaces; 5 | use protobuf::json::print_to_string; 6 | use protobuf::Message; 7 | use sha2::Digest; 8 | use sysinfo::{System, SystemExt}; 9 | use users::{Groups, Users}; 10 | 11 | use crate::errors::{SerializableResult, SerializationError}; 12 | use crate::ffi_generated as ffi; 13 | use crate::helpers::*; 14 | use crate::struct_pb::*; 15 | use crate::traits::SerializableEvent; 16 | 17 | {% for module in modules %}{% set entry_point = module.structures | last %} 18 | {{ module.render_rust_from_ffi() }} 19 | 20 | impl SerializableEvent for {{entry_point.final}} { 21 | fn to_json(&self) -> SerializableResult { 22 | match print_to_string(self) { 23 | Ok(result) => Ok(result), 24 | Err(e) => Err(SerializationError::Json(e)), 25 | } 26 | } 27 | 28 | fn to_bytes(&self) -> SerializableResult> { 29 | let mut event = Event::new(); 30 | event.{{entry_point.name}} = Some(self.clone()).into(); 31 | event.set_event_type(event::EventType::{{entry_point.final | upper}}); 32 | match event.write_to_bytes() { 33 | Ok(result) => Ok(result), 34 | Err(e) => Err(SerializationError::Bytes(e)), 35 | } 36 | } 37 | 38 | fn update_id(&mut self, id: &mut str) { 39 | self.event.as_mut().and_then(|e| { 40 | e.set_id(id.to_string().to_owned()); 41 | Some(e) 42 | }); 43 | } 44 | 45 | fn update_sequence(&mut self, seq: u64) { 46 | self.event.as_mut().and_then(|e| { 47 | e.set_sequence(seq); 48 | Some(e) 49 | }); 50 | } 51 | 52 | fn suffix(&self) -> &'static str { 53 | "{{module.name}}" 54 | } 55 | 56 | fn enrich_common<'a>(&'a mut self) -> SerializableResult<&'a mut Self> { 57 | { 58 | let cache = super::USERS_CACHE.lock().unwrap(); 59 | // real enrichments 60 | let user = self.user.get_mut_ref(); 61 | let uid = user.get_id().parse::().unwrap(); 62 | let group = user.group.get_mut_ref(); 63 | let gid = group.get_id().parse::().unwrap(); 64 | 65 | for enriched_group in cache.get_group_by_gid(gid) { 66 | group.set_name(enriched_group.name().to_string_lossy().to_string()); 67 | } 68 | for enriched_user in cache.get_user_by_uid(uid) { 69 | user.set_name(enriched_user.name().to_string_lossy().to_string()); 70 | } 71 | 72 | // effective enrichments 73 | let effective_user = user.effective.get_mut_ref(); 74 | let effective_uid = effective_user.get_id().parse::().unwrap(); 75 | let effective_group = effective_user.group.get_mut_ref(); 76 | let effective_gid = effective_group.get_id().parse::().unwrap(); 77 | for enriched_group in cache.get_group_by_gid(effective_gid) { 78 | effective_group.set_name(enriched_group.name().to_string_lossy().to_string()); 79 | } 80 | for enriched_user in cache.get_user_by_uid(effective_uid) { 81 | effective_user.set_name(enriched_user.name().to_string_lossy().to_string()); 82 | } 83 | } 84 | 85 | // entity id enrichments 86 | let machine_id = machine_uid::get().unwrap(); // this should probably be error checked 87 | 88 | let process = self.process.get_mut_ref(); 89 | let pid = process.get_pid(); 90 | let process_start = process.get_start(); 91 | let process_entity_id = format!( 92 | "{}{}{}", 93 | machine_id, 94 | format!("{:01$}", pid, 5), 95 | process_start 96 | ); 97 | process.set_entity_id(format!( 98 | "{:x}", 99 | sha2::Sha256::digest(process_entity_id.as_bytes()) 100 | )); 101 | 102 | let parent = process.parent.get_mut_ref(); 103 | let ppid = parent.get_pid(); 104 | let parent_start = parent.get_start(); 105 | let parent_entity_id = format!( 106 | "{}{}{}", 107 | machine_id, 108 | format!("{:01$}", ppid, 5), 109 | parent_start 110 | ); 111 | parent.set_entity_id(format!( 112 | "{:x}", 113 | sha2::Sha256::digest(parent_entity_id.as_bytes()) 114 | )); 115 | 116 | let system = System::new(); 117 | let host = self.host.get_mut_ref(); 118 | host.set_uptime(system.get_uptime()); 119 | for hostname in system.get_host_name() { 120 | host.set_hostname(hostname); 121 | } 122 | let all_interfaces = interfaces(); 123 | let active_interfaces = all_interfaces 124 | .iter() 125 | .filter(|e| e.is_up() && !e.is_loopback() && !e.ips.is_empty()); 126 | for interface in active_interfaces { 127 | if interface.mac.is_some() { 128 | host.mac.push(interface.mac.unwrap().to_string()); 129 | } 130 | for ip in &interface.ips { 131 | host.ip.push(ip.ip().to_string()); 132 | } 133 | } 134 | host.os = Some(Default::default()).into(); 135 | 136 | let os = host.os.get_mut_ref(); 137 | os.set_field_type("linux".to_string()); 138 | for os_name in system.get_name() { 139 | os.set_name(os_name); 140 | } 141 | for kernel_version in system.get_kernel_version() { 142 | os.set_kernel(kernel_version); 143 | } 144 | 145 | Ok(self) 146 | } 147 | } 148 | {% endfor %} 149 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use seahorse::{App, Context, Flag, FlagType}; 3 | use std::convert::{TryFrom, TryInto}; 4 | use std::time::Duration; 5 | 6 | use crate::client::Client; 7 | 8 | mod batcher; 9 | mod client; 10 | mod errors; 11 | mod globals; 12 | mod handler; 13 | mod logging; 14 | 15 | #[cfg(test)] 16 | mod tests; 17 | 18 | fn main() { 19 | let args: Vec = std::env::args().collect(); 20 | let app = App::new(env!("CARGO_PKG_NAME")) 21 | .description(env!("CARGO_PKG_DESCRIPTION")) 22 | .version(env!("CARGO_PKG_VERSION")) 23 | .action(run) 24 | .flag( 25 | Flag::new("filter", FlagType::String) 26 | .description("Apply filter to the probe") 27 | .alias("f"), 28 | ) 29 | .flag( 30 | Flag::new("debug", FlagType::Bool) 31 | .description("Verbose debugging") 32 | .alias("d"), 33 | ) 34 | .flag( 35 | Flag::new("workers", FlagType::Int) 36 | .description("Number of workers (default: #cores)") 37 | .alias("w"), 38 | ) 39 | .flag( 40 | Flag::new("flush", FlagType::Int) 41 | .description("Maximum seconds before a flush occurs (default: 30s)") 42 | .alias("r"), 43 | ) 44 | .flag( 45 | Flag::new("batch", FlagType::Int) 46 | .description("Maximum batch size before a flush occurs (default: 1000)") 47 | .alias("b"), 48 | ) 49 | .flag( 50 | Flag::new("size", FlagType::Int) 51 | .description("Maximum batch size (in bytes) before a flush occurs (default: 1MB)") 52 | .alias("s"), 53 | ) 54 | .flag( 55 | Flag::new("host", FlagType::String) 56 | .description( 57 | "Elasticsearch host that data is sent to (default: 'http://localhost:9200')", 58 | ) 59 | .alias("H"), 60 | ) 61 | .flag( 62 | Flag::new("creds", FlagType::String) 63 | .description("Credentials for Elasticsearch host") 64 | .alias("c"), 65 | ) 66 | .flag( 67 | Flag::new("insecure", FlagType::Bool) 68 | .description("Allow for insecure https connections to Elasticsearch host") 69 | .alias("i"), 70 | ) 71 | .flag( 72 | Flag::new("timeout", FlagType::Int) 73 | .description("Request timeout for Elasticsearch client (default: 5s)") 74 | .alias("t"), 75 | ) 76 | .flag( 77 | Flag::new("local", FlagType::Bool) 78 | .description( 79 | "Don't attempt to flush any output to Elasticsearch, just echo it to stdout", 80 | ) 81 | .alias("l"), 82 | ); 83 | 84 | app.run(args) 85 | } 86 | 87 | fn run(c: &Context) { 88 | let config = sled::Config::new().temporary(true); 89 | let db = config.open().expect("could not open database"); 90 | globals::initialize_global_database(db); 91 | 92 | let debug = c.bool_flag("debug"); 93 | logging::setup_logger(if debug { 94 | log::LevelFilter::Debug 95 | } else { 96 | log::LevelFilter::Info 97 | }); 98 | 99 | let mut filters: Vec<&str> = vec![]; 100 | let filter = c.string_flag("filter").unwrap_or_else(|_| String::from("")); 101 | if !filter.is_empty() { 102 | filters = filter.split(';').collect(); 103 | } 104 | 105 | let cores = num_cpus::get() as u32; 106 | let workers = c 107 | .int_flag("workers") 108 | .map_or(cores, |w| u32::try_from(w).unwrap_or(cores)); 109 | 110 | let batch_size = c 111 | .int_flag("batch") 112 | .map_or(500, |w| usize::try_from(w).unwrap_or(500)); 113 | 114 | let batch_bytes = c 115 | .int_flag("size") 116 | .map_or(1 << 20, |w| usize::try_from(w).unwrap_or(1 << 20)); 117 | 118 | let flush_rate = c 119 | .int_flag("flush") 120 | .map_or(30, |w| u64::try_from(w).unwrap_or(30)); 121 | 122 | let host = c 123 | .string_flag("host") 124 | .unwrap_or_else(|_| String::from("http://localhost:9200")); 125 | let creds = c.string_flag("creds").ok(); 126 | let insecure = c.bool_flag("insecure"); 127 | let timeout = c 128 | .int_flag("timeout") 129 | .map_or(5, |t| u64::try_from(t).unwrap_or(5)); 130 | let local = c.bool_flag("local"); 131 | 132 | let client = Client::new(host, creds, insecure, Duration::new(timeout, 0)); 133 | match setup_templates(local, &client) { 134 | Err(e) => { 135 | error!("error setting up templates: {}", e); 136 | std::process::exit(1); 137 | } 138 | _ => {} 139 | } 140 | std::thread::spawn(move || loop { 141 | batcher::Batcher::run(local, &client, flush_rate, batch_size, batch_bytes, workers) 142 | }); 143 | 144 | match probe_sys::Probe::new() 145 | .debug(debug) 146 | .run(handler::Handler {}) 147 | { 148 | Ok(probe) => match probe.apply(filters) { 149 | Err(e) => { 150 | error!("error setting up probe: {}", e); 151 | std::process::exit(1); 152 | } 153 | _ => loop { 154 | probe.poll((flush_rate * 1000).try_into().unwrap()); 155 | }, 156 | }, 157 | Err(e) => { 158 | error!("error setting up probe: {}", e.to_string()); 159 | std::process::exit(1); 160 | } 161 | } 162 | } 163 | 164 | fn setup_templates(local: bool, client: &Client) -> Result<(), String> { 165 | if !local { 166 | client.ensure_template("bprm_check_security")?; 167 | client.ensure_template("inode_unlink")?; 168 | } 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /probe-sys/templates/probe_generated.rs.j2: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use log::{debug, warn}; 4 | use rule_compiler::{compile, Operation}; 5 | use std::convert::TryInto; 6 | use std::mem::transmute_copy; 7 | use std::os::raw::c_int; 8 | use std::os::unix::ffi::OsStrExt; 9 | use std::panic; 10 | use std::path::Path; 11 | use sysinfo::{ProcessExt, System, SystemExt}; 12 | 13 | use crate::errors::Error; 14 | use crate::ffi_generated as ffi; 15 | use crate::query_writer::BpfQueryWriterFactory; 16 | use crate::struct_pb; 17 | use crate::traits::{ProbeHandler, QueryStruct}; 18 | 19 | pub struct Probe<'a> { 20 | ctx: Option<*mut ffi::state>, 21 | // store the closures so that we make sure it has 22 | // the same lifetime as the state wrapper 23 | {% for module in modules %}{% set entry_point = module.structures | last %} 24 | _{{module.name}}_handler: Option>, 25 | {% endfor %} 26 | debug: bool, 27 | } 28 | 29 | impl<'a> Probe<'a> { 30 | pub fn new() -> Self { 31 | Self { 32 | ctx: None, 33 | {% for module in modules %} 34 | _{{module.name}}_handler: None, 35 | {% endfor %} 36 | debug: false, 37 | } 38 | } 39 | 40 | pub fn apply(&mut self, rules: Vec<&str>) -> Result<(), String> { 41 | for rule in &rules { 42 | let compiled = compile(rule)?; 43 | let query_writer = &BpfQueryWriterFactory::new(self); 44 | compiled.encode(query_writer)? 45 | } 46 | Ok(()) 47 | } 48 | 49 | pub fn debug(&mut self, debug: bool) -> &mut Self { 50 | self.debug = debug; 51 | self 52 | } 53 | 54 | pub fn run(&mut self, handler: F) -> Result<&mut Self, Error> 55 | where 56 | F: 'a + ProbeHandler + panic::RefUnwindSafe + Copy, 57 | U: std::fmt::Display, 58 | { 59 | {% for module in modules %}{% set entry_point = module.structures | last %} 60 | let mut {{module.name}}_wrapper = move |e: ffi::{{entry_point.name}}| { 61 | let result = panic::catch_unwind(|| { 62 | handler 63 | .enqueue(&mut struct_pb::{{entry_point.final}}::from(e)) 64 | .unwrap_or_else(|e| warn!("error enqueuing data: {}", e)); 65 | }); 66 | if result.is_err() { 67 | debug!("panic while handling event"); 68 | } 69 | }; 70 | let ({{module.name}}_closure, {{module.name}}_callback) = 71 | unsafe { ffi::unpack_{{module.name}}_closure(&mut {{module.name}}_wrapper) }; 72 | {% endfor %} 73 | let state_config = ffi::state_configuration { 74 | debug: self.debug, 75 | {% for module in modules %} 76 | {{module.name}}_ctx: {{module.name}}_closure, 77 | {{module.name}}_handler: {{module.name}}_callback, 78 | {% endfor %} 79 | }; 80 | let state = unsafe { ffi::new_state(state_config) }; 81 | if state.is_null() { 82 | return Err(Error::InitializationError); 83 | } 84 | let mut system = System::new(); 85 | system.refresh_processes(); 86 | let empty_path = Path::new(""); 87 | for (pid, process) in system.get_processes() { 88 | let exe = process.exe(); 89 | if exe == empty_path { 90 | continue; 91 | } 92 | let name = process.name(); 93 | let cmd = process.cmd(); 94 | let mut cached: ffi::cached_process = Default::default(); 95 | for (dest, src) in cached.executable.iter_mut().zip(exe.as_os_str().as_bytes().iter()) { 96 | *dest = *src as _; 97 | } 98 | for (dest, src) in cached.name.iter_mut().zip(name.as_bytes().iter()) { 99 | *dest = *src as _; 100 | } 101 | for (index, c) in cached.args.iter_mut().enumerate() { 102 | if cmd.len() > index { 103 | for (dest, src) in c.iter_mut().zip(cmd[index].as_bytes().iter()) { 104 | *dest = *src as _; 105 | } 106 | cached.args_count += 1; 107 | } 108 | } 109 | 110 | if cached.args_count < cmd.len().try_into().unwrap() { 111 | cached.truncated = 1; 112 | } 113 | unsafe { ffi::cache_process(state, *pid as i32, &cached) }; 114 | } 115 | self.ctx = Some(state); 116 | {% for module in modules %} 117 | self._{{module.name}}_handler = Some(Box::new({{module.name}}_wrapper)); 118 | {% endfor %} 119 | Ok(self) 120 | } 121 | 122 | pub fn apply_rule(&self, module: String, operation: Operation, rule: T) { 123 | match self.ctx { 124 | Some(ctx) => match (module.as_str(), operation) { 125 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 126 | ("{{module.name}}", Operation::Filter) => unsafe { 127 | ffi::flush_{{module.name}}_filter_rule(ctx, transmute_copy(&rule)); 128 | }, 129 | ("{{module.name}}", Operation::Reject) => unsafe { 130 | let rule = transmute_copy(&rule); 131 | ffi::flush_{{module.name}}_rejection_rule(ctx, rule); 132 | }, 133 | {% endif %}{% endfor %} 134 | _ => return, 135 | }, 136 | _ => return, 137 | } 138 | } 139 | 140 | pub fn poll(&self, timeout: i32) { 141 | match self.ctx { 142 | Some(ctx) => unsafe { ffi::poll_state(ctx, timeout as c_int) }, 143 | _ => return, 144 | } 145 | } 146 | } 147 | 148 | impl<'a> Drop for Probe<'a> { 149 | fn drop(&mut self) { 150 | match self.ctx { 151 | Some(ctx) => unsafe { ffi::destroy_state(ctx) }, 152 | _ => return, 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example BPF LSM 2 | 3 | This repo contains scripts and an example program to play around with BPF CO-RE modules and an LSM. 4 | 5 | In order to be completely portable (assuming a new enough kernel), and to show how this would interop with a Rust program, the userspace components are written in a combination of Rust and C compiled with clang and linked against musl. The result should be that you can drop the output binary on any Linux 5.11+ kernel (for ringbuffer, LSM hook support, and inode local storage) and have it run. 6 | 7 | It also has some ideas around sharing userspace data via tracepoints with lsm hooks. 8 | 9 | ## Quickstart 10 | 11 | ```bash 12 | vagrant up 13 | make 14 | ``` 15 | 16 | In one VM ssh session: 17 | 18 | ```bash 19 | vagrant@ubuntu-hirsute:~$ sudo /vagrant/probe -b 1 -f \ 20 | 'reject bprm_check_security when user.id == 1000 and process.executable == "/usr/bin/ls"' -l 21 | ``` 22 | 23 | In another VM ssh session you should see something like the following happen: 24 | 25 | ```bash 26 | vagrant@ubuntu-hirsute:~$ ls 27 | -bash: /usr/bin/ls: Operation not permitted 28 | ``` 29 | 30 | In the first terminal you should see an event that looks like: 31 | 32 | ```json 33 | { 34 | "@timestamp": "1613149814", 35 | "event": { 36 | "id": "11c3ce30-ff30-4e1f-bf59-ad8a4851fd9a", 37 | "kind": "event", 38 | "category": "process", 39 | "action": "execution-denied", 40 | "type": "start", 41 | "outcome": "failure", 42 | "module": "bpf-lsm", 43 | "provider": "bprm-check-security", 44 | "sequence": "0" 45 | }, 46 | "host": { 47 | "hostname": "ubuntu-hirsute", 48 | "ip": ["10.0.2.15", "fe80::45:b3ff:fe9e:e735"], 49 | "mac": ["02:45:b3:9e:e7:35"], 50 | "uptime": "238884", 51 | "os": { 52 | "type": "linux", 53 | "name": "Ubuntu", 54 | "kernel": "5.11.0-rc6-bpf-lsm" 55 | } 56 | }, 57 | "process": { 58 | "pid": 214904, 59 | "entity_id": "698d5f16ba47c2808140278a9e4127f95f9cd514e8156a9572f8f262f7adb10a", 60 | "name": "ls", 61 | "ppid": 185070, 62 | "executable": "/usr/bin/ls", 63 | "args_count": "2", 64 | "start": "1613149814", 65 | "thread.id": "214904", 66 | "command_line": "ls --color=auto", 67 | "args": ["ls", "--color=auto"], 68 | "parent": { 69 | "pid": 185070, 70 | "entity_id": "2adab6f443f9827e3473403b54d3808467a9bafb5950594d3beb67ca0d691c75", 71 | "name": "bash", 72 | "args_count": "1", 73 | "args": ["-bash"], 74 | "ppid": 185069, 75 | "start": "1613121319", 76 | "thread.id": "185070", 77 | "executable": "/usr/bin/bash" 78 | } 79 | }, 80 | "user": { 81 | "id": "1000", 82 | "name": "vagrant", 83 | "group": { 84 | "id": "1000", 85 | "name": "vagrant" 86 | }, 87 | "effective": { 88 | "id": "1000", 89 | "name": "vagrant", 90 | "group": { 91 | "id": "1000", 92 | "name": "vagrant" 93 | } 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | If you want to test out file unlinking checks you can add a filter like `reject inode_unlink when file.path == "/home/vagrant/test.txt" and user.id == 0` and see it in action: 100 | 101 | ```bash 102 | vagrant@ubuntu-hirsute:~$ touch test.txt 103 | vagrant@ubuntu-hirsute:~$ rm test.txt 104 | vagrant@ubuntu-hirsute:~$ touch test.txt 105 | vagrant@ubuntu-hirsute:~$ sudo rm test.txt 106 | rm: cannot remove 'test.txt': Operation not permitted 107 | ``` 108 | 109 | ```json 110 | { 111 | "@timestamp": "1613149749", 112 | "event": { 113 | "id": "5e3f9cd4-291a-469f-8b73-35eff181a917", 114 | "kind": "event", 115 | "category": "file", 116 | "action": "unlink-denied", 117 | "type": "deletion", 118 | "outcome": "failure", 119 | "module": "bpf-lsm", 120 | "provider": "inode-unlink", 121 | "sequence": "3" 122 | }, 123 | "host": { 124 | "hostname": "ubuntu-hirsute", 125 | "ip": ["10.0.2.15", "fe80::45:b3ff:fe9e:e735"], 126 | "mac": ["02:45:b3:9e:e7:35"], 127 | "uptime": "238819", 128 | "os": { 129 | "type": "linux", 130 | "name": "Ubuntu", 131 | "kernel": "5.11.0-rc6-bpf-lsm" 132 | } 133 | }, 134 | "process": { 135 | "pid": 214840, 136 | "entity_id": "f70111f169cd7dea79d78de015ff48ebf6209292d77df45e227dbbdc97bfcac5", 137 | "name": "rm", 138 | "ppid": 214839, 139 | "executable": "/usr/bin/rm", 140 | "args_count": "2", 141 | "start": "1613149749", 142 | "thread.id": "214840", 143 | "command_line": "rm test.txt", 144 | "args": ["rm", "test.txt"], 145 | "parent": { 146 | "pid": 214839, 147 | "entity_id": "2ad02210f987bee9cb66bf8949d83594d42d1d13fc647fbc255cab0c62ddb7b6", 148 | "name": "sudo", 149 | "args_count": "3", 150 | "args": ["sudo", "rm", "test.txt"], 151 | "ppid": 185070, 152 | "start": "1613149749", 153 | "thread.id": "214839", 154 | "executable": "/usr/bin/sudo" 155 | } 156 | }, 157 | "user": { 158 | "id": "0", 159 | "name": "root", 160 | "group": { 161 | "id": "0", 162 | "name": "root" 163 | }, 164 | "effective": { 165 | "id": "0", 166 | "name": "root", 167 | "group": { 168 | "id": "0", 169 | "name": "root" 170 | } 171 | } 172 | }, 173 | "file": { 174 | "name": "test.txt", 175 | "directory": "/home/vagrant", 176 | "path": "/home/vagrant/test.txt", 177 | "extension": "txt", 178 | "inode": "72843" 179 | } 180 | } 181 | ``` 182 | 183 | ## Kernel 184 | 185 | The Vagrantfile boots a virtualbox VM with a custom Linux 5.11-rc6 build with BPF LSM kernel options 186 | all enabled, a rust toolchain installed, and lldb for debugging builds with `rust-lldb`. 187 | 188 | ## Toolchains 189 | 190 | The toolchain for this are all in Docker containers. The containers contain a clang 10-based compiler 191 | that targets musl and libc++ in order to statically compile everything. As a result, you can't use any 192 | Rust code that leverages `proc_macro` as this requires dynamically linking against glibc and gcc. 193 | -------------------------------------------------------------------------------- /probe-sys/src/probe_generated.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use log::{debug, warn}; 4 | use rule_compiler::{compile, Operation}; 5 | use std::convert::TryInto; 6 | use std::mem::transmute_copy; 7 | use std::os::raw::c_int; 8 | use std::os::unix::ffi::OsStrExt; 9 | use std::panic; 10 | use std::path::Path; 11 | use sysinfo::{ProcessExt, System, SystemExt}; 12 | 13 | use crate::errors::Error; 14 | use crate::ffi_generated as ffi; 15 | use crate::query_writer::BpfQueryWriterFactory; 16 | use crate::struct_pb; 17 | use crate::traits::{ProbeHandler, QueryStruct}; 18 | 19 | pub struct Probe<'a> { 20 | ctx: Option<*mut ffi::state>, 21 | // store the closures so that we make sure it has 22 | // the same lifetime as the state wrapper 23 | _bprm_check_security_handler: Option>, 24 | _inode_unlink_handler: Option>, 25 | debug: bool, 26 | } 27 | 28 | impl<'a> Probe<'a> { 29 | pub fn new() -> Self { 30 | Self { 31 | ctx: None, 32 | _bprm_check_security_handler: None, 33 | _inode_unlink_handler: None, 34 | debug: false, 35 | } 36 | } 37 | 38 | pub fn apply(&mut self, rules: Vec<&str>) -> Result<(), String> { 39 | for rule in &rules { 40 | let compiled = compile(rule)?; 41 | let query_writer = &BpfQueryWriterFactory::new(self); 42 | compiled.encode(query_writer)? 43 | } 44 | Ok(()) 45 | } 46 | 47 | pub fn debug(&mut self, debug: bool) -> &mut Self { 48 | self.debug = debug; 49 | self 50 | } 51 | 52 | pub fn run(&mut self, handler: F) -> Result<&mut Self, Error> 53 | where 54 | F: 'a + ProbeHandler + panic::RefUnwindSafe + Copy, 55 | U: std::fmt::Display, 56 | { 57 | let mut bprm_check_security_wrapper = move |e: ffi::bprm_check_security_event_t| { 58 | let result = panic::catch_unwind(|| { 59 | handler 60 | .enqueue(&mut struct_pb::BprmCheckSecurityEvent::from(e)) 61 | .unwrap_or_else(|e| warn!("error enqueuing data: {}", e)); 62 | }); 63 | if result.is_err() { 64 | debug!("panic while handling event"); 65 | } 66 | }; 67 | let (bprm_check_security_closure, bprm_check_security_callback) = 68 | unsafe { ffi::unpack_bprm_check_security_closure(&mut bprm_check_security_wrapper) }; 69 | let mut inode_unlink_wrapper = move |e: ffi::inode_unlink_event_t| { 70 | let result = panic::catch_unwind(|| { 71 | handler 72 | .enqueue(&mut struct_pb::InodeUnlinkEvent::from(e)) 73 | .unwrap_or_else(|e| warn!("error enqueuing data: {}", e)); 74 | }); 75 | if result.is_err() { 76 | debug!("panic while handling event"); 77 | } 78 | }; 79 | let (inode_unlink_closure, inode_unlink_callback) = 80 | unsafe { ffi::unpack_inode_unlink_closure(&mut inode_unlink_wrapper) }; 81 | let state_config = ffi::state_configuration { 82 | debug: self.debug, 83 | bprm_check_security_ctx: bprm_check_security_closure, 84 | bprm_check_security_handler: bprm_check_security_callback, 85 | inode_unlink_ctx: inode_unlink_closure, 86 | inode_unlink_handler: inode_unlink_callback, 87 | }; 88 | let state = unsafe { ffi::new_state(state_config) }; 89 | if state.is_null() { 90 | return Err(Error::InitializationError); 91 | } 92 | let mut system = System::new(); 93 | system.refresh_processes(); 94 | let empty_path = Path::new(""); 95 | for (pid, process) in system.get_processes() { 96 | let exe = process.exe(); 97 | if exe == empty_path { 98 | continue; 99 | } 100 | let name = process.name(); 101 | let cmd = process.cmd(); 102 | let mut cached: ffi::cached_process = Default::default(); 103 | for (dest, src) in cached.executable.iter_mut().zip(exe.as_os_str().as_bytes().iter()) { 104 | *dest = *src as _; 105 | } 106 | for (dest, src) in cached.name.iter_mut().zip(name.as_bytes().iter()) { 107 | *dest = *src as _; 108 | } 109 | for (index, c) in cached.args.iter_mut().enumerate() { 110 | if cmd.len() > index { 111 | for (dest, src) in c.iter_mut().zip(cmd[index].as_bytes().iter()) { 112 | *dest = *src as _; 113 | } 114 | cached.args_count += 1; 115 | } 116 | } 117 | 118 | if cached.args_count < cmd.len().try_into().unwrap() { 119 | cached.truncated = 1; 120 | } 121 | unsafe { ffi::cache_process(state, *pid as i32, &cached) }; 122 | } 123 | self.ctx = Some(state); 124 | self._bprm_check_security_handler = Some(Box::new(bprm_check_security_wrapper)); 125 | self._inode_unlink_handler = Some(Box::new(inode_unlink_wrapper)); 126 | Ok(self) 127 | } 128 | 129 | pub fn apply_rule(&self, module: String, operation: Operation, rule: T) { 130 | match self.ctx { 131 | Some(ctx) => match (module.as_str(), operation) { 132 | ("bprm_check_security", Operation::Filter) => unsafe { 133 | ffi::flush_bprm_check_security_filter_rule(ctx, transmute_copy(&rule)); 134 | }, 135 | ("bprm_check_security", Operation::Reject) => unsafe { 136 | let rule = transmute_copy(&rule); 137 | ffi::flush_bprm_check_security_rejection_rule(ctx, rule); 138 | }, 139 | ("inode_unlink", Operation::Filter) => unsafe { 140 | ffi::flush_inode_unlink_filter_rule(ctx, transmute_copy(&rule)); 141 | }, 142 | ("inode_unlink", Operation::Reject) => unsafe { 143 | let rule = transmute_copy(&rule); 144 | ffi::flush_inode_unlink_rejection_rule(ctx, rule); 145 | }, 146 | _ => return, 147 | }, 148 | _ => return, 149 | } 150 | } 151 | 152 | pub fn poll(&self, timeout: i32) { 153 | match self.ctx { 154 | Some(ctx) => unsafe { ffi::poll_state(ctx, timeout as c_int) }, 155 | _ => return, 156 | } 157 | } 158 | } 159 | 160 | impl<'a> Drop for Probe<'a> { 161 | fn drop(&mut self) { 162 | match self.ctx { 163 | Some(ctx) => unsafe { ffi::destroy_state(ctx) }, 164 | _ => return, 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /probe-sys/src/ffi_generated.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use super::compiler_generated::*; 4 | use std::os::raw::{c_char, c_int, c_void}; 5 | 6 | #[repr(C)] 7 | #[derive(Copy, Clone)] 8 | pub struct cached_process { 9 | pub name: [c_char; 256], 10 | pub executable: [c_char; 256], 11 | pub args: [[c_char; 128]; 64], 12 | pub args_count: u64, 13 | pub truncated: i32, 14 | } 15 | 16 | impl Default for cached_process { 17 | fn default() -> Self { 18 | unsafe { std::mem::zeroed() } 19 | } 20 | } 21 | 22 | #[repr(C)] 23 | #[derive(Copy, Clone)] 24 | pub struct bprm_check_security_event_event_t { 25 | pub action: [c_char; 256], 26 | pub outcome: [c_char; 256], 27 | } 28 | #[repr(C)] 29 | #[derive(Copy, Clone)] 30 | pub struct bprm_check_security_event_process_parent_t { 31 | pub pid: u32, 32 | pub entity_id: [c_char; 256], 33 | pub name: [c_char; 256], 34 | pub args_count: u64, 35 | pub args: [[c_char; 128]; 64], 36 | pub ppid: u32, 37 | pub start: u64, 38 | pub thread__id: u64, 39 | pub executable: [c_char; 256], 40 | } 41 | #[repr(C)] 42 | #[derive(Copy, Clone)] 43 | pub struct bprm_check_security_event_process_t { 44 | pub pid: u32, 45 | pub entity_id: [c_char; 256], 46 | pub name: [c_char; 256], 47 | pub ppid: u32, 48 | pub executable: [c_char; 256], 49 | pub args_count: u64, 50 | pub start: u64, 51 | pub thread__id: u64, 52 | pub args: [[c_char; 128]; 64], 53 | pub parent: bprm_check_security_event_process_parent_t, 54 | } 55 | #[repr(C)] 56 | #[derive(Copy, Clone)] 57 | pub struct bprm_check_security_event_user_group_t { 58 | pub id: u32, 59 | } 60 | #[repr(C)] 61 | #[derive(Copy, Clone)] 62 | pub struct bprm_check_security_event_user_effective_group_t { 63 | pub id: u32, 64 | } 65 | #[repr(C)] 66 | #[derive(Copy, Clone)] 67 | pub struct bprm_check_security_event_user_effective_t { 68 | pub id: u32, 69 | pub group: bprm_check_security_event_user_effective_group_t, 70 | } 71 | #[repr(C)] 72 | #[derive(Copy, Clone)] 73 | pub struct bprm_check_security_event_user_t { 74 | pub id: u32, 75 | pub group: bprm_check_security_event_user_group_t, 76 | pub effective: bprm_check_security_event_user_effective_t, 77 | } 78 | #[repr(C)] 79 | #[derive(Copy, Clone)] 80 | pub struct bprm_check_security_event_t { 81 | pub __timestamp: u64, 82 | pub event: bprm_check_security_event_event_t, 83 | pub process: bprm_check_security_event_process_t, 84 | pub user: bprm_check_security_event_user_t, 85 | } 86 | 87 | pub type bprm_check_security_event_handler = extern "C" fn(ctx: *mut c_void, e: bprm_check_security_event_t); 88 | #[repr(C)] 89 | #[derive(Copy, Clone)] 90 | pub struct inode_unlink_event_event_t { 91 | pub action: [c_char; 256], 92 | pub outcome: [c_char; 256], 93 | } 94 | #[repr(C)] 95 | #[derive(Copy, Clone)] 96 | pub struct inode_unlink_event_process_parent_t { 97 | pub pid: u32, 98 | pub entity_id: [c_char; 256], 99 | pub name: [c_char; 256], 100 | pub args_count: u64, 101 | pub args: [[c_char; 128]; 64], 102 | pub ppid: u32, 103 | pub start: u64, 104 | pub thread__id: u64, 105 | pub executable: [c_char; 256], 106 | } 107 | #[repr(C)] 108 | #[derive(Copy, Clone)] 109 | pub struct inode_unlink_event_process_t { 110 | pub pid: u32, 111 | pub entity_id: [c_char; 256], 112 | pub name: [c_char; 256], 113 | pub ppid: u32, 114 | pub executable: [c_char; 256], 115 | pub args_count: u64, 116 | pub start: u64, 117 | pub thread__id: u64, 118 | pub args: [[c_char; 128]; 64], 119 | pub parent: inode_unlink_event_process_parent_t, 120 | } 121 | #[repr(C)] 122 | #[derive(Copy, Clone)] 123 | pub struct inode_unlink_event_user_group_t { 124 | pub id: u32, 125 | } 126 | #[repr(C)] 127 | #[derive(Copy, Clone)] 128 | pub struct inode_unlink_event_user_effective_group_t { 129 | pub id: u32, 130 | } 131 | #[repr(C)] 132 | #[derive(Copy, Clone)] 133 | pub struct inode_unlink_event_user_effective_t { 134 | pub id: u32, 135 | pub group: inode_unlink_event_user_effective_group_t, 136 | } 137 | #[repr(C)] 138 | #[derive(Copy, Clone)] 139 | pub struct inode_unlink_event_user_t { 140 | pub id: u32, 141 | pub group: inode_unlink_event_user_group_t, 142 | pub effective: inode_unlink_event_user_effective_t, 143 | } 144 | #[repr(C)] 145 | #[derive(Copy, Clone)] 146 | pub struct inode_unlink_event_file_t { 147 | pub path: [c_char; 256], 148 | pub inode: u64, 149 | } 150 | #[repr(C)] 151 | #[derive(Copy, Clone)] 152 | pub struct inode_unlink_event_t { 153 | pub __timestamp: u64, 154 | pub event: inode_unlink_event_event_t, 155 | pub process: inode_unlink_event_process_t, 156 | pub user: inode_unlink_event_user_t, 157 | pub file: inode_unlink_event_file_t, 158 | } 159 | 160 | pub type inode_unlink_event_handler = extern "C" fn(ctx: *mut c_void, e: inode_unlink_event_t); 161 | 162 | #[repr(C)] 163 | #[derive(Copy, Clone)] 164 | pub struct state_configuration { 165 | pub debug: bool, 166 | pub bprm_check_security_ctx: *mut c_void, 167 | pub bprm_check_security_handler: bprm_check_security_event_handler, 168 | pub inode_unlink_ctx: *mut c_void, 169 | pub inode_unlink_handler: inode_unlink_event_handler, 170 | } 171 | pub enum state {} 172 | extern "C" { 173 | pub fn new_state(config: state_configuration) -> *mut state; 174 | pub fn poll_state(_self: *mut state, timeout: c_int); 175 | pub fn destroy_state(_self: *mut state); 176 | pub fn cache_process(_self: *mut state, pid: i32, process: *const cached_process); 177 | pub fn flush_bprm_check_security_filter_rule(_self: *mut state, rule: query_bpf_bprm_check_security_event_t); 178 | pub fn flush_bprm_check_security_rejection_rule(_self: *mut state, rule: query_bpf_bprm_check_security_event_t); 179 | pub fn flush_inode_unlink_filter_rule(_self: *mut state, rule: query_bpf_inode_unlink_event_t); 180 | pub fn flush_inode_unlink_rejection_rule(_self: *mut state, rule: query_bpf_inode_unlink_event_t); 181 | } 182 | 183 | pub unsafe fn unpack_bprm_check_security_closure(closure: &mut F) -> (*mut c_void, bprm_check_security_event_handler) 184 | where 185 | F: FnMut(bprm_check_security_event_t), 186 | { 187 | extern "C" fn trampoline(data: *mut c_void, e: bprm_check_security_event_t) 188 | where 189 | F: FnMut(bprm_check_security_event_t), 190 | { 191 | let closure: &mut F = unsafe { &mut *(data as *mut F) }; 192 | (*closure)(e); 193 | } 194 | (closure as *mut F as *mut c_void, trampoline::) 195 | } 196 | pub unsafe fn unpack_inode_unlink_closure(closure: &mut F) -> (*mut c_void, inode_unlink_event_handler) 197 | where 198 | F: FnMut(inode_unlink_event_t), 199 | { 200 | extern "C" fn trampoline(data: *mut c_void, e: inode_unlink_event_t) 201 | where 202 | F: FnMut(inode_unlink_event_t), 203 | { 204 | let closure: &mut F = unsafe { &mut *(data as *mut F) }; 205 | (*closure)(e); 206 | } 207 | (closure as *mut F as *mut c_void, trampoline::) 208 | } 209 | -------------------------------------------------------------------------------- /elasticsearch/bprm_check_security.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ 3 | "bprm_check_security-*" 4 | ], 5 | "template": { 6 | "mappings": { 7 | "date_detection": false, 8 | "dynamic_templates": [ 9 | { 10 | "strings_as_keyword": { 11 | "mapping": { 12 | "ignore_above": 1024, 13 | "type": "keyword" 14 | }, 15 | "match_mapping_type": "string" 16 | } 17 | } 18 | ], 19 | "properties": { 20 | "@timestamp": { 21 | "type": "date", 22 | "format": "epoch_second" 23 | }, 24 | "event": { 25 | "properties": { 26 | "id": { 27 | "type": "keyword", 28 | "ignore_above": 1024 29 | }, 30 | "kind": { 31 | "type": "keyword", 32 | "ignore_above": 1024 33 | }, 34 | "category": { 35 | "type": "keyword", 36 | "ignore_above": 1024 37 | }, 38 | "action": { 39 | "type": "keyword", 40 | "ignore_above": 1024 41 | }, 42 | "type": { 43 | "type": "keyword", 44 | "ignore_above": 1024 45 | }, 46 | "outcome": { 47 | "type": "keyword", 48 | "ignore_above": 1024 49 | }, 50 | "module": { 51 | "type": "keyword", 52 | "ignore_above": 1024 53 | }, 54 | "provider": { 55 | "type": "keyword", 56 | "ignore_above": 1024 57 | }, 58 | "sequence": { 59 | "type": "long" 60 | }, 61 | "ingested": { 62 | "type": "date", 63 | "format": "epoch_second" 64 | } 65 | } 66 | }, 67 | "host": { 68 | "properties": { 69 | "hostname": { 70 | "type": "keyword", 71 | "ignore_above": 1024 72 | }, 73 | "ip": { 74 | "type": "ip" 75 | }, 76 | "mac": { 77 | "type": "keyword", 78 | "ignore_above": 1024 79 | }, 80 | "uptime": { 81 | "type": "long" 82 | }, 83 | "os": { 84 | "properties": { 85 | "type": { 86 | "type": "keyword", 87 | "ignore_above": 1024 88 | }, 89 | "name": { 90 | "type": "keyword", 91 | "ignore_above": 1024 92 | }, 93 | "kernel": { 94 | "type": "keyword", 95 | "ignore_above": 1024 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "process": { 102 | "properties": { 103 | "pid": { 104 | "type": "long" 105 | }, 106 | "entity_id": { 107 | "type": "keyword", 108 | "ignore_above": 1024 109 | }, 110 | "name": { 111 | "type": "wildcard" 112 | }, 113 | "ppid": { 114 | "type": "long" 115 | }, 116 | "executable": { 117 | "type": "wildcard" 118 | }, 119 | "args_count": { 120 | "type": "long" 121 | }, 122 | "start": { 123 | "type": "date", 124 | "format": "epoch_second" 125 | }, 126 | "thread": { 127 | "properties": { 128 | "id": { 129 | "type": "long" 130 | } 131 | } 132 | }, 133 | "command_line": { 134 | "type": "keyword", 135 | "ignore_above": 1024 136 | }, 137 | "args": { 138 | "type": "keyword", 139 | "ignore_above": 1024 140 | }, 141 | "parent": { 142 | "properties": { 143 | "pid": { 144 | "type": "long" 145 | }, 146 | "entity_id": { 147 | "type": "keyword", 148 | "ignore_above": 1024 149 | }, 150 | "name": { 151 | "type": "wildcard" 152 | }, 153 | "args_count": { 154 | "type": "long" 155 | }, 156 | "args": { 157 | "type": "keyword", 158 | "ignore_above": 1024 159 | }, 160 | "ppid": { 161 | "type": "long" 162 | }, 163 | "start": { 164 | "type": "date", 165 | "format": "epoch_second" 166 | }, 167 | "thread": { 168 | "properties": { 169 | "id": { 170 | "type": "long" 171 | } 172 | } 173 | }, 174 | "executable": { 175 | "type": "wildcard" 176 | } 177 | } 178 | } 179 | } 180 | }, 181 | "user": { 182 | "properties": { 183 | "id": { 184 | "type": "keyword", 185 | "ignore_above": 1024 186 | }, 187 | "name": { 188 | "type": "wildcard" 189 | }, 190 | "group": { 191 | "properties": { 192 | "id": { 193 | "type": "keyword", 194 | "ignore_above": 1024 195 | }, 196 | "name": { 197 | "type": "keyword", 198 | "ignore_above": 1024 199 | } 200 | } 201 | }, 202 | "effective": { 203 | "properties": { 204 | "id": { 205 | "type": "keyword", 206 | "ignore_above": 1024 207 | }, 208 | "name": { 209 | "type": "wildcard" 210 | }, 211 | "group": { 212 | "properties": { 213 | "id": { 214 | "type": "keyword", 215 | "ignore_above": 1024 216 | }, 217 | "name": { 218 | "type": "keyword", 219 | "ignore_above": 1024 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } 228 | }, 229 | "settings": { 230 | "index": { 231 | "mapping": { 232 | "total_fields": { 233 | "limit": 10000 234 | } 235 | }, 236 | "refresh_interval": "5s" 237 | } 238 | } 239 | }, 240 | "priority": 1 241 | } -------------------------------------------------------------------------------- /probe-sys/templates/compiler_generated.rs.j2: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use rule_compiler::{Atom, Operation, Operator, QueryWriter}; 4 | use std::convert::TryFrom; 5 | use std::os::raw::c_char; 6 | 7 | use crate::constants::UNSET_OPERATOR; 8 | use crate::helpers::operator_to_constant; 9 | use crate::query_writer::InnerBpfQueryWriter; 10 | use crate::traits::QueryStruct; 11 | 12 | {% for module in modules %}{% set entry_point = module.structures | last %} 13 | {% for structure in module.structures %}{% if structure.queryable %} 14 | #[repr(C)] 15 | #[derive(Debug, Copy, Clone, PartialEq)] 16 | pub struct query_bpf_{{structure.name}} { 17 | {% if entry_point.name == structure.name %} 18 | pub ___absolute: u8, 19 | {% endif %} 20 | {% for field in structure.fields %}{% if field.queryable %} 21 | {% if not field.complex %} 22 | pub {{field.name}}___operator: u8, 23 | {% endif %} 24 | pub {{field.name}}: {% if field.type.size is not none %}[{{field.type.rust}}; {{field.type.size}}]{% else %}{% if field.complex%}query_bpf_{%endif%}{{field.type.rust}}{% endif %}{% if field.type.lifetime %}<'a>{%endif%}, 25 | {% endif %}{% endfor %} 26 | } 27 | 28 | impl Default for query_bpf_{{structure.name}} { 29 | fn default() -> Self { 30 | unsafe { std::mem::zeroed() } 31 | } 32 | } 33 | {% endif %}{% endfor %} 34 | 35 | {% if entry_point.queryable %} 36 | impl QueryStruct for query_bpf_{{entry_point.name}} { 37 | fn set_absolute(&mut self, value: u8) { 38 | self.___absolute = value; 39 | } 40 | 41 | fn set_number(&mut self, path: String, operator: Operator, value: u64) -> Result<(), String> { 42 | match path.as_str() { 43 | {% for structure in module.structures %} 44 | {% for field in structure.fields %} 45 | {% if field.queryable == "number" %} 46 | "{{field.path}}{{field.name}}" => { 47 | if self.{{field.path}}{{field.name}}___operator != UNSET_OPERATOR { 48 | // we can only hold a single condition per variable for now 49 | return Err(format!("{} already in condition", path)); 50 | } 51 | let v = {{field.type.rust}}::try_from(value).map_err(|_| String::from("{{field.path}}{{field.name}} must be a {{field.type.rust}}"))?; 52 | self.{{field.path}}{{field.name}} = v; 53 | self.{{field.path}}{{field.name}}___operator = operator_to_constant(operator); 54 | Ok(()) 55 | } 56 | {% endif %} 57 | {% endfor %} 58 | {% endfor %} 59 | _ => Err(format!("numeric field named {} not found in schema", path)), 60 | } 61 | } 62 | 63 | fn set_string( 64 | &mut self, 65 | path: String, 66 | operator: Operator, 67 | value: String, 68 | ) -> Result<(), String> { 69 | match path.as_str() { 70 | {% for structure in module.structures %} 71 | {% for field in structure.fields %} 72 | {% if field.queryable == "string" %} 73 | "{{field.path}}{{field.name}}" => { 74 | if self.{{field.path}}{{field.name}}___operator != UNSET_OPERATOR { 75 | // we can only hold a single condition per variable for now 76 | return Err(format!("{} already in condition", path)); 77 | } 78 | if value.len() < {{field.type.size}} { 79 | for (dest, src) in self.{{field.path}}{{field.name}}.iter_mut().zip(value.as_bytes().iter()) { 80 | *dest = *src as _; 81 | } 82 | self.{{field.path}}{{field.name}}___operator = operator_to_constant(operator); 83 | Ok(()) 84 | } else { 85 | Err(format!("{{field.path}}{{field.name}} is too long, maximum {{field.type.size}} characters, given value is {} characters", value.len())) 86 | } 87 | }, 88 | {% endif %} 89 | {% endfor %} 90 | {% endfor %} 91 | _ => Err(format!("string field named {} not found in schema", path)), 92 | } 93 | } 94 | 95 | fn flush<'a>(&mut self, _probe: &'a super::Probe<'a>) -> Result<(), String> { 96 | Ok(()) 97 | } 98 | } 99 | {% endif %}{% endfor %} 100 | 101 | pub struct BpfQueryWriter<'a> { 102 | table: String, 103 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 104 | write_query_{{entry_point.name}}: InnerBpfQueryWriter, 105 | {% endif %}{% endfor %} 106 | probe: Option<&'a super::Probe<'a>>, 107 | } 108 | 109 | impl<'a> BpfQueryWriter<'a> { 110 | pub fn new(probe: Option<&'a super::Probe>, table: String, operation: Operation) -> Self { 111 | Self { 112 | table: table, 113 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 114 | write_query_{{entry_point.name}}: InnerBpfQueryWriter::::new( 115 | "{{module.name}}".into(), 116 | operation, 117 | 8, 118 | ), 119 | {% endif %}{% endfor %} 120 | probe: probe, 121 | } 122 | } 123 | } 124 | 125 | impl<'b> QueryWriter for BpfQueryWriter<'b> { 126 | fn write_statement<'a>( 127 | &mut self, 128 | field: &'a str, 129 | operator: &'a Operator, 130 | atom: &'a Atom, 131 | ) -> Result<(), String> { 132 | match self.table.as_str() { 133 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 134 | "{{module.name}}" => self.write_query_{{entry_point.name}}.write_statement(field, operator, atom), 135 | {% endif %}{%endfor%} 136 | _ => Err(format!("invalid table name {}", self.table)), 137 | } 138 | } 139 | 140 | fn start_new_clause(&mut self) -> Result<(), String> { 141 | match self.table.as_str() { 142 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 143 | "{{module.name}}" => self.write_query_{{entry_point.name}}.start_new_clause(), 144 | {% endif %}{%endfor%} 145 | _ => Err(format!("invalid table name {}", self.table)), 146 | } 147 | } 148 | 149 | fn write_absolute(&mut self, value: bool) -> Result<(), String> { 150 | match self.table.as_str() { 151 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 152 | "{{module.name}}" => self.write_query_{{entry_point.name}}.write_absolute(value), 153 | {% endif %}{%endfor%} 154 | _ => Err(format!("invalid table name {}", self.table)), 155 | } 156 | } 157 | 158 | fn flush(&mut self) -> Result<(), String> { 159 | match self.probe { 160 | Some(probe) => match self.table.as_str() { 161 | {% for module in modules %}{% set entry_point = module.structures | last %}{% if entry_point.queryable %} 162 | "{{module.name}}" => self.write_query_{{entry_point.name}}.flush_probe(probe), 163 | {% endif %}{%endfor%} 164 | _ => Err(format!("invalid table name {}", self.table)), 165 | }, 166 | _ => Ok(()) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use crate::globals::get_template; 4 | use backoff::backoff::Backoff; 5 | use backoff::{retry, ExponentialBackoff, SystemClock}; 6 | use base64::encode; 7 | use instant::Instant; 8 | use sled::IVec; 9 | use std::format; 10 | use std::sync::Arc; 11 | use std::time::Duration; 12 | use ureq::{Agent, AgentBuilder, Error}; 13 | 14 | pub struct SkipVerifier {} 15 | 16 | impl rustls::ServerCertVerifier for SkipVerifier { 17 | fn verify_server_cert( 18 | &self, 19 | _roots: &rustls::RootCertStore, 20 | _presented_certs: &[rustls::Certificate], 21 | _dns_name: webpki::DNSNameRef<'_>, 22 | _ocsp_response: &[u8], 23 | ) -> Result { 24 | Ok(rustls::ServerCertVerified::assertion()) 25 | } 26 | } 27 | 28 | const INITIAL_INTERVAL_MILLIS: u64 = 500; 29 | const RANDOMIZATION_FACTOR: f64 = 0.5; 30 | const MULTIPLIER: f64 = 1.5; 31 | const MAX_INTERVAL_MILLIS: u64 = 2_000; 32 | const MAX_ELAPSED_TIME_MILLIS: u64 = 15_000; 33 | 34 | #[derive(Clone)] 35 | pub struct Client { 36 | inner: Agent, 37 | base: String, 38 | creds: Option, 39 | } 40 | 41 | fn make_url(base: String, path: String) -> String { 42 | base.trim_end_matches('/').to_owned() + &path 43 | } 44 | 45 | fn make_batch_entry(index: &String, data: &String) -> String { 46 | format!("{{\"create\":{{ \"_index\" : \"{}-1\"}}\n{}\n", index, data) 47 | } 48 | 49 | fn backoff() -> ExponentialBackoff { 50 | let mut e = ExponentialBackoff { 51 | current_interval: Duration::from_millis(INITIAL_INTERVAL_MILLIS), 52 | initial_interval: Duration::from_millis(INITIAL_INTERVAL_MILLIS), 53 | randomization_factor: RANDOMIZATION_FACTOR, 54 | multiplier: MULTIPLIER, 55 | max_interval: Duration::from_millis(MAX_INTERVAL_MILLIS), 56 | max_elapsed_time: Some(Duration::from_millis(MAX_ELAPSED_TIME_MILLIS)), 57 | clock: SystemClock::default(), 58 | start_time: Instant::now(), 59 | }; 60 | e.reset(); 61 | e 62 | } 63 | 64 | impl Client { 65 | pub fn new( 66 | base: String, 67 | creds: Option, 68 | ignore_validation: bool, 69 | timeout: std::time::Duration, 70 | ) -> Self { 71 | let mut agent_builder = AgentBuilder::new().timeout(timeout); 72 | if ignore_validation { 73 | let mut client_config = rustls::ClientConfig::new(); 74 | client_config 75 | .dangerous() 76 | .set_certificate_verifier(Arc::new(SkipVerifier {})); 77 | let tls_config = Arc::new(client_config); 78 | agent_builder = agent_builder.tls_config(tls_config) 79 | } 80 | let encoded = creds.map(|c| encode(c.as_bytes())); 81 | Self { 82 | base, 83 | creds: encoded, 84 | inner: agent_builder.build(), 85 | } 86 | } 87 | 88 | fn construct_request(&self, method: &str, content_type: &str, url: &String) -> ureq::Request { 89 | let path = format!("{}{}", self.base, url); 90 | let request = self 91 | .inner 92 | .request(method, &path) 93 | .set("Content-Type", content_type); 94 | match &self.creds { 95 | Some(creds) => { 96 | let header = format!("Basic {}", creds); 97 | request.set("Authorization", &header) 98 | } 99 | None => request, 100 | } 101 | } 102 | 103 | fn construct_basic_request(&self, method: &str, url: &String) -> ureq::Request { 104 | self.construct_request(method, "application/json", url) 105 | } 106 | 107 | fn construct_bulk_request(&self, method: &str, url: &String) -> ureq::Request { 108 | self.construct_request(method, "application/x-ndjson", url) 109 | } 110 | 111 | fn do_request( 112 | &self, 113 | method: &str, 114 | url: &String, 115 | data: Option<&[u8]>, 116 | ) -> Result { 117 | let response = match data { 118 | Some(payload) => retry(backoff(), || { 119 | let resp = self 120 | .construct_basic_request(method, url) 121 | .send_bytes(payload); 122 | match resp { 123 | Ok(r) => Ok(r), 124 | Err(Error::Status(_, r)) => Ok(r), 125 | Err(e) => Err(backoff::Error::Transient(e)), 126 | } 127 | }), 128 | None => retry(backoff(), || { 129 | let resp = self.construct_basic_request(method, url).call(); 130 | match resp { 131 | Ok(r) => Ok(r), 132 | Err(Error::Status(_, r)) => Ok(r), 133 | Err(e) => Err(backoff::Error::Transient(e)), 134 | } 135 | }), 136 | }; 137 | let response_text = response 138 | .map_err(|e| e.to_string())? 139 | .into_string() 140 | .map_err(|e| e.to_string())?; 141 | let error_message = ajson::get(&response_text, "error.reason"); 142 | match error_message { 143 | Some(message) => Err(message.to_string()), 144 | None => Ok(response_text), 145 | } 146 | } 147 | 148 | fn get(&self, url: &String) -> Result { 149 | self.do_request("GET", url, None) 150 | } 151 | 152 | fn put(&self, url: &String, data: &[u8]) -> Result { 153 | self.do_request("PUT", url, Some(data)) 154 | } 155 | 156 | pub fn ensure_template(&self, name: &str) -> Result<(), String> { 157 | let url = format!("/_index_template/{}", name); 158 | match self.get(&url) { 159 | Ok(_) => Ok(()), // we already have the template, don't bother creating a new one 160 | Err(_) => { 161 | // assume we don't have the template installed, so attempt to install it 162 | let template = get_template(name)?; 163 | self.put(&url, template)?; 164 | Ok(()) 165 | } 166 | } 167 | } 168 | 169 | pub fn send_batch(&self, batch: &Vec<(IVec, (String, String))>) -> Result<(), String> { 170 | if batch.is_empty() { 171 | return Ok(()) 172 | } 173 | let url = String::from("/_bulk"); 174 | let payload = batch 175 | .iter() 176 | .map(|(_, (index, data))| make_batch_entry(index, data)) 177 | .collect::(); 178 | let response = retry(backoff(), || { 179 | let resp = self.construct_bulk_request("POST", &url).send_bytes(payload.as_bytes()); 180 | match resp { 181 | Ok(r) => Ok(r), 182 | Err(Error::Status(_, r)) => Ok(r), 183 | Err(e) => Err(backoff::Error::Transient(e)), 184 | } 185 | }); 186 | let response_text = response 187 | .map_err(|e| e.to_string())? 188 | .into_string() 189 | .map_err(|e| e.to_string())?; 190 | let error_message = ajson::get(&response_text, "error.reason"); 191 | match error_message { 192 | Some(message) => Err(message.to_string()), 193 | None => Ok(()), 194 | } 195 | } 196 | } 197 | 198 | // don't ever mutate state in the client 199 | unsafe impl Send for Client {} 200 | unsafe impl Sync for Client {} 201 | -------------------------------------------------------------------------------- /elasticsearch/inode_unlink.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ 3 | "inode_unlink-*" 4 | ], 5 | "template": { 6 | "mappings": { 7 | "date_detection": false, 8 | "dynamic_templates": [ 9 | { 10 | "strings_as_keyword": { 11 | "mapping": { 12 | "ignore_above": 1024, 13 | "type": "keyword" 14 | }, 15 | "match_mapping_type": "string" 16 | } 17 | } 18 | ], 19 | "properties": { 20 | "@timestamp": { 21 | "type": "date", 22 | "format": "epoch_second" 23 | }, 24 | "event": { 25 | "properties": { 26 | "id": { 27 | "type": "keyword", 28 | "ignore_above": 1024 29 | }, 30 | "kind": { 31 | "type": "keyword", 32 | "ignore_above": 1024 33 | }, 34 | "category": { 35 | "type": "keyword", 36 | "ignore_above": 1024 37 | }, 38 | "action": { 39 | "type": "keyword", 40 | "ignore_above": 1024 41 | }, 42 | "type": { 43 | "type": "keyword", 44 | "ignore_above": 1024 45 | }, 46 | "outcome": { 47 | "type": "keyword", 48 | "ignore_above": 1024 49 | }, 50 | "module": { 51 | "type": "keyword", 52 | "ignore_above": 1024 53 | }, 54 | "provider": { 55 | "type": "keyword", 56 | "ignore_above": 1024 57 | }, 58 | "sequence": { 59 | "type": "long" 60 | }, 61 | "ingested": { 62 | "type": "date", 63 | "format": "epoch_second" 64 | } 65 | } 66 | }, 67 | "host": { 68 | "properties": { 69 | "hostname": { 70 | "type": "keyword", 71 | "ignore_above": 1024 72 | }, 73 | "ip": { 74 | "type": "ip" 75 | }, 76 | "mac": { 77 | "type": "keyword", 78 | "ignore_above": 1024 79 | }, 80 | "uptime": { 81 | "type": "long" 82 | }, 83 | "os": { 84 | "properties": { 85 | "type": { 86 | "type": "keyword", 87 | "ignore_above": 1024 88 | }, 89 | "name": { 90 | "type": "keyword", 91 | "ignore_above": 1024 92 | }, 93 | "kernel": { 94 | "type": "keyword", 95 | "ignore_above": 1024 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "process": { 102 | "properties": { 103 | "pid": { 104 | "type": "long" 105 | }, 106 | "entity_id": { 107 | "type": "keyword", 108 | "ignore_above": 1024 109 | }, 110 | "name": { 111 | "type": "wildcard" 112 | }, 113 | "ppid": { 114 | "type": "long" 115 | }, 116 | "executable": { 117 | "type": "wildcard" 118 | }, 119 | "args_count": { 120 | "type": "long" 121 | }, 122 | "start": { 123 | "type": "date", 124 | "format": "epoch_second" 125 | }, 126 | "thread": { 127 | "properties": { 128 | "id": { 129 | "type": "long" 130 | } 131 | } 132 | }, 133 | "command_line": { 134 | "type": "keyword", 135 | "ignore_above": 1024 136 | }, 137 | "args": { 138 | "type": "keyword", 139 | "ignore_above": 1024 140 | }, 141 | "parent": { 142 | "properties": { 143 | "pid": { 144 | "type": "long" 145 | }, 146 | "entity_id": { 147 | "type": "keyword", 148 | "ignore_above": 1024 149 | }, 150 | "name": { 151 | "type": "wildcard" 152 | }, 153 | "args_count": { 154 | "type": "long" 155 | }, 156 | "args": { 157 | "type": "keyword", 158 | "ignore_above": 1024 159 | }, 160 | "ppid": { 161 | "type": "long" 162 | }, 163 | "start": { 164 | "type": "date", 165 | "format": "epoch_second" 166 | }, 167 | "thread": { 168 | "properties": { 169 | "id": { 170 | "type": "long" 171 | } 172 | } 173 | }, 174 | "executable": { 175 | "type": "wildcard" 176 | } 177 | } 178 | } 179 | } 180 | }, 181 | "user": { 182 | "properties": { 183 | "id": { 184 | "type": "keyword", 185 | "ignore_above": 1024 186 | }, 187 | "name": { 188 | "type": "wildcard" 189 | }, 190 | "group": { 191 | "properties": { 192 | "id": { 193 | "type": "keyword", 194 | "ignore_above": 1024 195 | }, 196 | "name": { 197 | "type": "keyword", 198 | "ignore_above": 1024 199 | } 200 | } 201 | }, 202 | "effective": { 203 | "properties": { 204 | "id": { 205 | "type": "keyword", 206 | "ignore_above": 1024 207 | }, 208 | "name": { 209 | "type": "wildcard" 210 | }, 211 | "group": { 212 | "properties": { 213 | "id": { 214 | "type": "keyword", 215 | "ignore_above": 1024 216 | }, 217 | "name": { 218 | "type": "keyword", 219 | "ignore_above": 1024 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | }, 227 | "file": { 228 | "properties": { 229 | "name": { 230 | "type": "wildcard" 231 | }, 232 | "directory": { 233 | "type": "wildcard" 234 | }, 235 | "path": { 236 | "type": "wildcard" 237 | }, 238 | "extension": { 239 | "type": "keyword", 240 | "ignore_above": 1024 241 | }, 242 | "inode": { 243 | "type": "keyword", 244 | "ignore_above": 1024 245 | } 246 | } 247 | } 248 | } 249 | }, 250 | "settings": { 251 | "index": { 252 | "mapping": { 253 | "total_fields": { 254 | "limit": 10000 255 | } 256 | }, 257 | "refresh_interval": "5s" 258 | } 259 | } 260 | }, 261 | "priority": 1 262 | } -------------------------------------------------------------------------------- /probe-sys/src/struct.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package probe.protobuf; 4 | 5 | option (rustproto.carllerche_bytes_for_bytes_all) = true; 6 | option (rustproto.carllerche_bytes_for_string_all) = true; 7 | option optimize_for = SPEED; 8 | 9 | message BprmCheckSecurityEventEvent { 10 | optional string id = 1 [json_name="id"]; 11 | optional string kind = 2 [json_name="kind"]; 12 | optional string category = 3 [json_name="category"]; 13 | optional string action = 4 [json_name="action"]; 14 | optional string field_type = 5 [json_name="type"]; 15 | optional string outcome = 6 [json_name="outcome"]; 16 | optional string module = 7 [json_name="module"]; 17 | optional string provider = 8 [json_name="provider"]; 18 | optional uint64 sequence = 9 [json_name="sequence"]; 19 | optional uint64 ingested = 10 [json_name="ingested"]; 20 | } 21 | message BprmCheckSecurityEventHostOs { 22 | optional string field_type = 1 [json_name="type"]; 23 | optional string name = 2 [json_name="name"]; 24 | optional string kernel = 3 [json_name="kernel"]; 25 | } 26 | message BprmCheckSecurityEventHost { 27 | optional string hostname = 1 [json_name="hostname"]; 28 | repeated string ip = 2 [json_name="ip"]; 29 | repeated string mac = 3 [json_name="mac"]; 30 | optional uint64 uptime = 4 [json_name="uptime"]; 31 | optional BprmCheckSecurityEventHostOs os = 5 [json_name="os"]; 32 | } 33 | message BprmCheckSecurityEventProcessParent { 34 | optional uint32 pid = 1 [json_name="pid"]; 35 | optional string entity_id = 2 [json_name="entity_id"]; 36 | optional string name = 3 [json_name="name"]; 37 | optional uint64 args_count = 4 [json_name="args_count"]; 38 | repeated string args = 5 [json_name="args"]; 39 | optional uint32 ppid = 6 [json_name="ppid"]; 40 | optional uint64 start = 7 [json_name="start"]; 41 | optional uint64 thread_id = 8 [json_name="thread.id"]; 42 | optional string executable = 9 [json_name="executable"]; 43 | } 44 | message BprmCheckSecurityEventProcess { 45 | optional uint32 pid = 1 [json_name="pid"]; 46 | optional string entity_id = 2 [json_name="entity_id"]; 47 | optional string name = 3 [json_name="name"]; 48 | optional uint32 ppid = 4 [json_name="ppid"]; 49 | optional string executable = 5 [json_name="executable"]; 50 | optional uint64 args_count = 6 [json_name="args_count"]; 51 | optional uint64 start = 7 [json_name="start"]; 52 | optional uint64 thread_id = 8 [json_name="thread.id"]; 53 | optional string command_line = 9 [json_name="command_line"]; 54 | repeated string args = 10 [json_name="args"]; 55 | optional BprmCheckSecurityEventProcessParent parent = 11 [json_name="parent"]; 56 | } 57 | message BprmCheckSecurityEventUserGroup { 58 | optional string id = 1 [json_name="id"]; 59 | optional string name = 2 [json_name="name"]; 60 | } 61 | message BprmCheckSecurityEventUserEffectiveGroup { 62 | optional string id = 1 [json_name="id"]; 63 | optional string name = 2 [json_name="name"]; 64 | } 65 | message BprmCheckSecurityEventUserEffective { 66 | optional string id = 1 [json_name="id"]; 67 | optional string name = 2 [json_name="name"]; 68 | optional BprmCheckSecurityEventUserEffectiveGroup group = 3 [json_name="group"]; 69 | } 70 | message BprmCheckSecurityEventUser { 71 | optional string id = 1 [json_name="id"]; 72 | optional string name = 2 [json_name="name"]; 73 | optional BprmCheckSecurityEventUserGroup group = 3 [json_name="group"]; 74 | optional BprmCheckSecurityEventUserEffective effective = 4 [json_name="effective"]; 75 | } 76 | message BprmCheckSecurityEvent { 77 | optional uint64 timestamp = 1 [json_name="@timestamp"]; 78 | optional BprmCheckSecurityEventEvent event = 2 [json_name="event"]; 79 | optional BprmCheckSecurityEventHost host = 3 [json_name="host"]; 80 | optional BprmCheckSecurityEventProcess process = 4 [json_name="process"]; 81 | optional BprmCheckSecurityEventUser user = 5 [json_name="user"]; 82 | } 83 | message InodeUnlinkEventEvent { 84 | optional string id = 1 [json_name="id"]; 85 | optional string kind = 2 [json_name="kind"]; 86 | optional string category = 3 [json_name="category"]; 87 | optional string action = 4 [json_name="action"]; 88 | optional string field_type = 5 [json_name="type"]; 89 | optional string outcome = 6 [json_name="outcome"]; 90 | optional string module = 7 [json_name="module"]; 91 | optional string provider = 8 [json_name="provider"]; 92 | optional uint64 sequence = 9 [json_name="sequence"]; 93 | optional uint64 ingested = 10 [json_name="ingested"]; 94 | } 95 | message InodeUnlinkEventHostOs { 96 | optional string field_type = 1 [json_name="type"]; 97 | optional string name = 2 [json_name="name"]; 98 | optional string kernel = 3 [json_name="kernel"]; 99 | } 100 | message InodeUnlinkEventHost { 101 | optional string hostname = 1 [json_name="hostname"]; 102 | repeated string ip = 2 [json_name="ip"]; 103 | repeated string mac = 3 [json_name="mac"]; 104 | optional uint64 uptime = 4 [json_name="uptime"]; 105 | optional InodeUnlinkEventHostOs os = 5 [json_name="os"]; 106 | } 107 | message InodeUnlinkEventProcessParent { 108 | optional uint32 pid = 1 [json_name="pid"]; 109 | optional string entity_id = 2 [json_name="entity_id"]; 110 | optional string name = 3 [json_name="name"]; 111 | optional uint64 args_count = 4 [json_name="args_count"]; 112 | repeated string args = 5 [json_name="args"]; 113 | optional uint32 ppid = 6 [json_name="ppid"]; 114 | optional uint64 start = 7 [json_name="start"]; 115 | optional uint64 thread_id = 8 [json_name="thread.id"]; 116 | optional string executable = 9 [json_name="executable"]; 117 | } 118 | message InodeUnlinkEventProcess { 119 | optional uint32 pid = 1 [json_name="pid"]; 120 | optional string entity_id = 2 [json_name="entity_id"]; 121 | optional string name = 3 [json_name="name"]; 122 | optional uint32 ppid = 4 [json_name="ppid"]; 123 | optional string executable = 5 [json_name="executable"]; 124 | optional uint64 args_count = 6 [json_name="args_count"]; 125 | optional uint64 start = 7 [json_name="start"]; 126 | optional uint64 thread_id = 8 [json_name="thread.id"]; 127 | optional string command_line = 9 [json_name="command_line"]; 128 | repeated string args = 10 [json_name="args"]; 129 | optional InodeUnlinkEventProcessParent parent = 11 [json_name="parent"]; 130 | } 131 | message InodeUnlinkEventUserGroup { 132 | optional string id = 1 [json_name="id"]; 133 | optional string name = 2 [json_name="name"]; 134 | } 135 | message InodeUnlinkEventUserEffectiveGroup { 136 | optional string id = 1 [json_name="id"]; 137 | optional string name = 2 [json_name="name"]; 138 | } 139 | message InodeUnlinkEventUserEffective { 140 | optional string id = 1 [json_name="id"]; 141 | optional string name = 2 [json_name="name"]; 142 | optional InodeUnlinkEventUserEffectiveGroup group = 3 [json_name="group"]; 143 | } 144 | message InodeUnlinkEventUser { 145 | optional string id = 1 [json_name="id"]; 146 | optional string name = 2 [json_name="name"]; 147 | optional InodeUnlinkEventUserGroup group = 3 [json_name="group"]; 148 | optional InodeUnlinkEventUserEffective effective = 4 [json_name="effective"]; 149 | } 150 | message InodeUnlinkEventFile { 151 | optional string name = 1 [json_name="name"]; 152 | optional string directory = 2 [json_name="directory"]; 153 | optional string path = 3 [json_name="path"]; 154 | optional string extension = 4 [json_name="extension"]; 155 | optional string inode = 5 [json_name="inode"]; 156 | } 157 | message InodeUnlinkEvent { 158 | optional uint64 timestamp = 1 [json_name="@timestamp"]; 159 | optional InodeUnlinkEventEvent event = 2 [json_name="event"]; 160 | optional InodeUnlinkEventHost host = 3 [json_name="host"]; 161 | optional InodeUnlinkEventProcess process = 4 [json_name="process"]; 162 | optional InodeUnlinkEventUser user = 5 [json_name="user"]; 163 | optional InodeUnlinkEventFile file = 6 [json_name="file"]; 164 | } 165 | 166 | message Event { 167 | enum EventType { 168 | BPRMCHECKSECURITYEVENT = 0; 169 | INODEUNLINKEVENT = 1; 170 | } 171 | required EventType event_type = 1; 172 | optional BprmCheckSecurityEvent bprm_check_security_event_t = 2; 173 | optional InodeUnlinkEvent inode_unlink_event_t = 3; 174 | } -------------------------------------------------------------------------------- /schemas/bprm_check_security.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "@timestamp" 3 | type: date 4 | description: Date/time when the event originated. 5 | - name: event 6 | description: Fields breaking down the event details. 7 | type: group 8 | fields: 9 | - name: id 10 | type: keyword 11 | enrichment: true 12 | description: Unique ID to describe the event. 13 | - name: kind 14 | type: keyword 15 | enrichment: true 16 | description: The kind of the event. The highest categorization field in the hierarchy. 17 | - name: category 18 | type: keyword 19 | enrichment: true 20 | description: Event category. The second categorization field in the hierarchy. 21 | - name: action 22 | type: keyword 23 | description: The action captured by the event. 24 | - name: type 25 | type: keyword 26 | enrichment: true 27 | description: Event type. The third categorization field in the hierarchy. 28 | - name: outcome 29 | type: keyword 30 | description: The outcome of the event. The lowest level categorization field in the hierarchy. 31 | - name: module 32 | type: keyword 33 | enrichment: true 34 | description: Name of the module this data is coming from. 35 | - name: provider 36 | type: keyword 37 | enrichment: true 38 | description: Source of the event 39 | - name: sequence 40 | type: long 41 | enrichment: true 42 | description: Sequence number of the event. 43 | - name: ingested 44 | type: date 45 | enrichment: true 46 | description: Timestamp when an event arrived in the central data store. 47 | - name: host 48 | description: Fields describing the relevant computing instance. 49 | type: group 50 | enrichment: true 51 | fields: 52 | - name: hostname 53 | type: keyword 54 | description: Hostname of the host. 55 | - name: ip 56 | type: ip 57 | description: Host ip addresses. 58 | override: 59 | c: n/a 60 | rust: n/a 61 | final: n/a 62 | proto: repeated string 63 | - name: mac 64 | type: keyword 65 | description: Host mac addresses. 66 | override: 67 | c: n/a 68 | rust: n/a 69 | final: n/a 70 | proto: repeated string 71 | - name: uptime 72 | type: long 73 | description: Seconds the host has been up. 74 | - name: os 75 | description: OS fields contain information about the operating system. 76 | type: group 77 | fields: 78 | - name: type 79 | type: keyword 80 | description: "Which commercial OS family (one of: linux, macos, unix or windows)." 81 | - name: name 82 | type: keyword 83 | description: Operating system name, without the version. 84 | - name: kernel 85 | type: keyword 86 | description: Operating system kernel version as a raw string. 87 | - name: process 88 | description: These fields contain information about a process. 89 | type: group 90 | fields: 91 | - name: pid 92 | format: string 93 | type: long 94 | description: Process id. 95 | override: 96 | c: unsigned int 97 | rust: u32 98 | proto: uint32 99 | final: u32 100 | - name: entity_id 101 | type: keyword 102 | description: Unique identifier for the process. 103 | - name: name 104 | type: wildcard 105 | queryable: string 106 | description: Process name. 107 | - name: ppid 108 | format: string 109 | type: long 110 | description: Parent process' pid. 111 | override: 112 | c: unsigned int 113 | rust: u32 114 | proto: uint32 115 | final: u32 116 | - name: executable 117 | type: wildcard 118 | queryable: string 119 | description: Absolute path to the process executable. 120 | - name: args_count 121 | type: long 122 | description: Length of the process.args array. 123 | - name: start 124 | type: date 125 | description: The time the process started. 126 | - name: thread.id 127 | format: string 128 | type: long 129 | description: Thread ID. 130 | - name: command_line 131 | type: keyword 132 | description: Full command line that started the process. 133 | enrichment: true 134 | - name: args 135 | type: keyword 136 | description: Array of process arguments. 137 | override: 138 | c: 139 | type: char 140 | suffix: "[64][128]" 141 | rust: "[[c_char; 128]; 64]" 142 | proto: repeated string 143 | final: Vec 144 | transform: 145 | method: "&mut convert_string_array" 146 | extra: event.get_args_count() 147 | setter: args.append 148 | - name: parent 149 | description: These fields contain information about a process. 150 | type: group 151 | fields: 152 | - name: pid 153 | format: string 154 | type: long 155 | description: Process id. 156 | override: 157 | c: unsigned int 158 | rust: u32 159 | proto: uint32 160 | final: u32 161 | - name: entity_id 162 | type: keyword 163 | description: Unique identifier for the process. 164 | - name: name 165 | type: wildcard 166 | queryable: string 167 | description: Process name. 168 | - name: args_count 169 | type: long 170 | description: Length of the process.args array. 171 | - name: args 172 | type: keyword 173 | description: Array of process arguments. 174 | override: 175 | c: 176 | type: char 177 | suffix: "[64][128]" 178 | rust: "[[c_char; 128]; 64]" 179 | proto: repeated string 180 | final: Vec 181 | transform: 182 | method: "&mut convert_string_array" 183 | extra: event.get_args_count() 184 | setter: args.append 185 | - name: ppid 186 | format: string 187 | type: long 188 | description: Parent process' pid. 189 | override: 190 | c: unsigned int 191 | rust: u32 192 | proto: uint32 193 | final: u32 194 | - name: start 195 | type: date 196 | description: The time the process started. 197 | - name: thread.id 198 | format: string 199 | type: long 200 | description: Thread ID. 201 | - name: executable 202 | type: wildcard 203 | queryable: string 204 | description: Absolute path to the process executable. 205 | - name: user 206 | description: Fields to describe the user relevant to the event. 207 | type: group 208 | fields: 209 | - name: id 210 | type: keyword 211 | description: Unique identifier of the user. 212 | queryable: number 213 | override: 214 | c: unsigned int 215 | rust: u32 216 | final: String 217 | proto: string 218 | transform: 219 | method: int_to_string 220 | - name: name 221 | enrichment: true 222 | type: wildcard 223 | description: Short name or login of the user. 224 | - name: group 225 | type: group 226 | description: User's group relevant to the event. 227 | fields: 228 | - name: id 229 | type: keyword 230 | description: Unique identifier for the group on the system/platform. 231 | override: 232 | c: unsigned int 233 | rust: u32 234 | final: String 235 | proto: string 236 | transform: 237 | method: int_to_string 238 | - name: name 239 | enrichment: true 240 | type: keyword 241 | description: Name of the group. 242 | - name: effective 243 | description: Fields to describe the user relevant to the event. 244 | type: group 245 | fields: 246 | - name: id 247 | type: keyword 248 | description: Unique identifier of the user. 249 | override: 250 | c: unsigned int 251 | rust: u32 252 | final: String 253 | proto: string 254 | transform: 255 | method: int_to_string 256 | - name: name 257 | enrichment: true 258 | type: wildcard 259 | description: Short name or login of the user. 260 | - name: group 261 | type: group 262 | description: User's group relevant to the event. 263 | fields: 264 | - name: id 265 | type: keyword 266 | description: Unique identifier for the group on the system/platform. 267 | override: 268 | c: unsigned int 269 | rust: u32 270 | final: String 271 | proto: string 272 | transform: 273 | method: int_to_string 274 | - name: name 275 | enrichment: true 276 | type: keyword 277 | description: Name of the group. 278 | -------------------------------------------------------------------------------- /schemas/inode_unlink.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "@timestamp" 3 | type: date 4 | description: Date/time when the event originated. 5 | - name: event 6 | description: Fields breaking down the event details. 7 | type: group 8 | fields: 9 | - name: id 10 | type: keyword 11 | enrichment: true 12 | description: Unique ID to describe the event. 13 | - name: kind 14 | type: keyword 15 | enrichment: true 16 | description: The kind of the event. The highest categorization field in the hierarchy. 17 | - name: category 18 | type: keyword 19 | enrichment: true 20 | description: Event category. The second categorization field in the hierarchy. 21 | - name: action 22 | type: keyword 23 | description: The action captured by the event. 24 | - name: type 25 | type: keyword 26 | enrichment: true 27 | description: Event type. The third categorization field in the hierarchy. 28 | - name: outcome 29 | type: keyword 30 | description: The outcome of the event. The lowest level categorization field in the hierarchy. 31 | - name: module 32 | type: keyword 33 | enrichment: true 34 | description: Name of the module this data is coming from. 35 | - name: provider 36 | type: keyword 37 | enrichment: true 38 | description: Source of the event 39 | - name: sequence 40 | type: long 41 | enrichment: true 42 | description: Sequence number of the event. 43 | - name: ingested 44 | type: date 45 | enrichment: true 46 | description: Timestamp when an event arrived in the central data store. 47 | - name: host 48 | description: Fields describing the relevant computing instance. 49 | type: group 50 | enrichment: true 51 | fields: 52 | - name: hostname 53 | type: keyword 54 | description: Hostname of the host. 55 | - name: ip 56 | type: ip 57 | description: Host ip addresses. 58 | override: 59 | c: n/a 60 | rust: n/a 61 | final: n/a 62 | proto: repeated string 63 | - name: mac 64 | type: keyword 65 | description: Host mac addresses. 66 | override: 67 | c: n/a 68 | rust: n/a 69 | final: n/a 70 | proto: repeated string 71 | - name: uptime 72 | type: long 73 | description: Seconds the host has been up. 74 | - name: os 75 | description: OS fields contain information about the operating system. 76 | type: group 77 | fields: 78 | - name: type 79 | type: keyword 80 | description: "Which commercial OS family (one of: linux, macos, unix or windows)." 81 | - name: name 82 | type: keyword 83 | description: Operating system name, without the version. 84 | - name: kernel 85 | type: keyword 86 | description: Operating system kernel version as a raw string. 87 | - name: process 88 | description: These fields contain information about a process. 89 | type: group 90 | fields: 91 | - name: pid 92 | format: string 93 | type: long 94 | description: Process id. 95 | override: 96 | c: unsigned int 97 | rust: u32 98 | proto: uint32 99 | final: u32 100 | - name: entity_id 101 | type: keyword 102 | description: Unique identifier for the process. 103 | - name: name 104 | type: wildcard 105 | queryable: string 106 | description: Process name. 107 | - name: ppid 108 | format: string 109 | type: long 110 | description: Parent process' pid. 111 | override: 112 | c: unsigned int 113 | rust: u32 114 | proto: uint32 115 | final: u32 116 | - name: executable 117 | type: wildcard 118 | description: Absolute path to the process executable. 119 | - name: args_count 120 | type: long 121 | description: Length of the process.args array. 122 | - name: start 123 | type: date 124 | description: The time the process started. 125 | - name: thread.id 126 | format: string 127 | type: long 128 | description: Thread ID. 129 | - name: command_line 130 | type: keyword 131 | description: Full command line that started the process. 132 | enrichment: true 133 | - name: args 134 | type: keyword 135 | description: Array of process arguments. 136 | override: 137 | c: 138 | type: char 139 | suffix: "[64][128]" 140 | rust: "[[c_char; 128]; 64]" 141 | proto: repeated string 142 | final: Vec 143 | transform: 144 | method: "&mut convert_string_array" 145 | extra: event.get_args_count() 146 | setter: args.append 147 | - name: parent 148 | description: These fields contain information about a process. 149 | type: group 150 | fields: 151 | - name: pid 152 | format: string 153 | type: long 154 | description: Process id. 155 | override: 156 | c: unsigned int 157 | rust: u32 158 | proto: uint32 159 | final: u32 160 | - name: entity_id 161 | type: keyword 162 | description: Unique identifier for the process. 163 | - name: name 164 | type: wildcard 165 | description: Process name. 166 | - name: args_count 167 | type: long 168 | description: Length of the process.args array. 169 | - name: args 170 | type: keyword 171 | description: Array of process arguments. 172 | override: 173 | c: 174 | type: char 175 | suffix: "[64][128]" 176 | rust: "[[c_char; 128]; 64]" 177 | proto: repeated string 178 | final: Vec 179 | transform: 180 | method: "&mut convert_string_array" 181 | extra: event.get_args_count() 182 | setter: args.append 183 | - name: ppid 184 | format: string 185 | type: long 186 | description: Parent process' pid. 187 | override: 188 | c: unsigned int 189 | rust: u32 190 | proto: uint32 191 | final: u32 192 | - name: start 193 | type: date 194 | description: The time the process started. 195 | - name: thread.id 196 | format: string 197 | type: long 198 | description: Thread ID. 199 | - name: executable 200 | type: wildcard 201 | description: Absolute path to the process executable. 202 | - name: user 203 | description: Fields to describe the user relevant to the event. 204 | type: group 205 | fields: 206 | - name: id 207 | type: keyword 208 | description: Unique identifier of the user. 209 | queryable: number 210 | override: 211 | c: unsigned int 212 | rust: u32 213 | final: String 214 | proto: string 215 | transform: 216 | method: int_to_string 217 | - name: name 218 | enrichment: true 219 | type: wildcard 220 | description: Short name or login of the user. 221 | - name: group 222 | type: group 223 | description: User's group relevant to the event. 224 | fields: 225 | - name: id 226 | type: keyword 227 | description: Unique identifier for the group on the system/platform. 228 | override: 229 | c: unsigned int 230 | rust: u32 231 | final: String 232 | proto: string 233 | transform: 234 | method: int_to_string 235 | - name: name 236 | enrichment: true 237 | type: keyword 238 | description: Name of the group. 239 | - name: effective 240 | description: Fields to describe the user relevant to the event. 241 | type: group 242 | fields: 243 | - name: id 244 | type: keyword 245 | description: Unique identifier of the user. 246 | override: 247 | c: unsigned int 248 | rust: u32 249 | final: String 250 | proto: string 251 | transform: 252 | method: int_to_string 253 | - name: name 254 | enrichment: true 255 | type: wildcard 256 | description: Short name or login of the user. 257 | - name: group 258 | type: group 259 | description: User's group relevant to the event. 260 | fields: 261 | - name: id 262 | type: keyword 263 | description: Unique identifier for the group on the system/platform. 264 | override: 265 | c: unsigned int 266 | rust: u32 267 | final: String 268 | proto: string 269 | transform: 270 | method: int_to_string 271 | - name: name 272 | enrichment: true 273 | type: keyword 274 | description: Name of the group. 275 | - name: file 276 | description: Fields describing files. 277 | type: group 278 | fields: 279 | - name: name 280 | type: wildcard 281 | enrichment: true 282 | description: Name of the file including the extension, without the directory. 283 | - name: directory 284 | type: wildcard 285 | enrichment: true 286 | description: Directory where the file is located. 287 | - name: path 288 | type: wildcard 289 | queryable: string 290 | description: Full path to the file, including the file name. 291 | - name: extension 292 | type: keyword 293 | enrichment: true 294 | description: File extension, excluding the leading dot. 295 | - name: inode 296 | type: keyword 297 | description: Inode representing the file in the filesystem. 298 | override: 299 | c: unsigned long 300 | rust: u64 301 | final: String 302 | proto: string 303 | transform: 304 | method: int_to_string 305 | -------------------------------------------------------------------------------- /libprobe/src/include/probe_bpf.h: -------------------------------------------------------------------------------- 1 | #ifndef __PROBE_BPF_H 2 | #define __PROBE_BPF_H 3 | 4 | // clang-format off 5 | #include "vmlinux.h" 6 | #include 7 | #include 8 | #include 9 | // clang-format on 10 | 11 | #define BPF 12 | 13 | #include "probe.generated.h" 14 | 15 | char _license[] SEC("license") = "GPL"; 16 | 17 | const volatile unsigned long clock_adjustment = 0; 18 | 19 | INLINE_STATIC unsigned long adjust_timestamp(unsigned long timestamp) { 20 | return (timestamp + clock_adjustment) / 1000000000l; 21 | } 22 | 23 | struct { 24 | __uint(type, BPF_MAP_TYPE_RINGBUF); 25 | __uint(max_entries, 256 * 1024); 26 | } events SEC(".maps"); 27 | 28 | const struct cached_process empty_cached_process = {}; 29 | 30 | struct { 31 | __uint(type, BPF_MAP_TYPE_HASH); 32 | __uint(max_entries, 10240); 33 | __type(key, pid_t); 34 | __type(value, struct cached_process); 35 | } processes SEC(".maps"); 36 | 37 | INLINE_STATIC struct cached_process * 38 | get_or_create_cached_process(struct task_struct *task) { 39 | pid_t pid = BPF_CORE_READ(task, tgid); 40 | bpf_map_update_elem(&processes, &pid, &empty_cached_process, BPF_NOEXIST); 41 | return bpf_map_lookup_elem(&processes, &pid); 42 | } 43 | 44 | INLINE_STATIC struct cached_process * 45 | get_cached_process(struct task_struct *task) { 46 | pid_t pid = BPF_CORE_READ(task, tgid); 47 | return bpf_map_lookup_elem(&processes, &pid); 48 | } 49 | 50 | INLINE_STATIC void update_cached_process(struct task_struct *task, 51 | const struct cached_process *p) { 52 | pid_t pid = BPF_CORE_READ(task, tgid); 53 | bpf_map_update_elem(&processes, &pid, p, BPF_ANY); 54 | } 55 | 56 | INLINE_STATIC void delete_cached_process(struct task_struct *task) { 57 | pid_t pid = BPF_CORE_READ(task, tgid); 58 | bpf_map_delete_elem(&processes, &pid); 59 | } 60 | 61 | const struct cached_file empty_cached_file = {}; 62 | 63 | struct { 64 | __uint(type, BPF_MAP_TYPE_INODE_STORAGE); 65 | __uint(map_flags, BPF_F_NO_PREALLOC); 66 | __type(key, int); 67 | __type(value, struct cached_file); 68 | } files SEC(".maps"); 69 | 70 | INLINE_STATIC struct cached_file * 71 | get_or_create_cached_file(struct inode *inode) { 72 | return bpf_inode_storage_get(&files, inode, 0, 73 | BPF_LOCAL_STORAGE_GET_F_CREATE); 74 | } 75 | 76 | INLINE_STATIC struct cached_file *get_cached_file(struct inode *inode) { 77 | return bpf_inode_storage_get(&files, inode, 0, 0); 78 | } 79 | 80 | #define TRACEPOINT(family, module, ctx) \ 81 | SEC("tp/" #family "/" #module) \ 82 | static int module##_hook(ctx) 83 | 84 | // consider setting this up as a set of bpf tail calls to get around stack and 85 | // instruction size limitations for the attribute checking routines 86 | #define __check_rejection_filter(m, p, e, r) \ 87 | const char success[] = "success"; \ 88 | const char failure[] = "failure"; \ 89 | const char denied[] = "" #p "-denied"; \ 90 | const char allowed[] = "" #p "-allowed"; \ 91 | if (r == 0) { /* don't override what the user has set */ \ 92 | unsigned int index = m##_index; \ 93 | unsigned int *size = bpf_map_lookup_elem(&rejection_rule_sizes, &index); \ 94 | if (size && *size > 0) { \ 95 | if (___check_##m(*size, &m##_rejections, &event->m##_event_t)) { \ 96 | SET_STRING(e->event.action, denied); \ 97 | SET_STRING(e->event.outcome, failure); \ 98 | r = -EPERM; \ 99 | } \ 100 | } \ 101 | } \ 102 | if (r == 0) { \ 103 | SET_STRING(e->event.action, allowed); \ 104 | SET_STRING(e->event.outcome, success); \ 105 | } 106 | 107 | #define __basic_process_info_for_task(x, task, ...) \ 108 | x.pid = BPF_CORE_READ(task, ##__VA_ARGS__, tgid); \ 109 | x.thread__id = BPF_CORE_READ(task, ##__VA_ARGS__, pid); \ 110 | x.ppid = BPF_CORE_READ(task, ##__VA_ARGS__, real_parent, tgid); \ 111 | x.start = adjust_timestamp(BPF_CORE_READ(task, ##__VA_ARGS__, start_time)) 112 | 113 | #define __copy_cached_process(x, cached) \ 114 | x.args_count = cached->args_count; \ 115 | memcpy(x.executable, cached->executable, MAX_PATH_SIZE); \ 116 | memcpy(x.name, cached->name, MAX_PATH_SIZE); \ 117 | _Pragma("unroll") for (int i = 0; i < MAX_ARGS && i < cached->args_count; \ 118 | i++) memcpy(x.args[i], cached->args[i], ARGSIZE) 119 | 120 | #define SLEEPABLE_LSM_HOOK(module, ...) \ 121 | SEC("lsm.s/" #module) \ 122 | int BPF_PROG(module##_hook, ##__VA_ARGS__) 123 | 124 | #define NOEVENT_LSM_HOOK(module, ...) \ 125 | SEC("lsm/" #module) \ 126 | int BPF_PROG(module##_hook, ##__VA_ARGS__) 127 | 128 | #define LSM_HOOK(module, prefix, ...) \ 129 | INLINE_STATIC int ____##module(unsigned long long *ctx, ##__VA_ARGS__, \ 130 | struct bpf_##module##_event_t *event, \ 131 | struct task_struct *current_task); \ 132 | SEC("lsm/" #module) \ 133 | int BPF_PROG(module##_hook, ##__VA_ARGS__) { \ 134 | int __ret = 0; \ 135 | struct bpf_event_t *event = bpf_ringbuf_reserve( \ 136 | &events, sizeof(struct bpf_event_t), RINGBUFFER_FLAGS); \ 137 | if (event) { \ 138 | event->type = type_##module##_event_t; \ 139 | struct bpf_##module##_event_t *e = &event->module##_event_t; \ 140 | struct task_struct *c = (struct task_struct *)bpf_get_current_task(); \ 141 | struct cached_process *cached; \ 142 | \ 143 | e->__timestamp = adjust_timestamp(bpf_ktime_get_boot_ns()); \ 144 | e->user.id = BPF_CORE_READ(c, real_cred, uid.val); \ 145 | e->user.group.id = BPF_CORE_READ(c, real_cred, gid.val); \ 146 | e->user.effective.id = BPF_CORE_READ(c, cred, uid.val); \ 147 | e->user.effective.group.id = BPF_CORE_READ(c, cred, gid.val); \ 148 | \ 149 | __basic_process_info_for_task(e->process, c); \ 150 | if ((cached = get_cached_process(c))) { \ 151 | __copy_cached_process(e->process, cached); \ 152 | } \ 153 | \ 154 | __basic_process_info_for_task(e->process.parent, c, real_parent); \ 155 | if ((cached = get_cached_process(BPF_CORE_READ(c, real_parent)))) { \ 156 | __copy_cached_process(e->process.parent, cached); \ 157 | } \ 158 | \ 159 | _Pragma("GCC diagnostic push") \ 160 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") __ret = \ 161 | ____##module(___bpf_ctx_cast(__VA_ARGS__), e, c); \ 162 | _Pragma("GCC diagnostic pop") \ 163 | __check_rejection_filter(module, prefix, e, __ret); \ 164 | bpf_ringbuf_submit(event, RINGBUFFER_FLAGS); \ 165 | } \ 166 | return __ret; \ 167 | } \ 168 | static int ____##module(unsigned long long *ctx, ##__VA_ARGS__, \ 169 | struct bpf_##module##_event_t *event, \ 170 | struct task_struct *current_task) 171 | 172 | #define COMPLETE_LSM_HOOK(module, prefix, ...) \ 173 | LSM_HOOK(module, prefix, ##__VA_ARGS__) { return 0; } 174 | 175 | // call this at the beginning of a hook to make the verifier happy 176 | #define initialize_event() \ 177 | if (!event) \ 178 | return 0; 179 | 180 | INLINE_STATIC int __last_index_of(const char *x, const char y, size_t len) { 181 | const char *a = x; 182 | int current_index = -1; 183 | #pragma unroll 184 | for (int i = 0; i < len; i++) { 185 | if (!*a) 186 | return current_index; // return whatever our current is at the end of 187 | // the string 188 | if (*a == y) 189 | current_index = i; 190 | a++; 191 | } 192 | return current_index; 193 | } 194 | 195 | INLINE_STATIC void set_basename(char *x, const char *y, size_t len) { 196 | int last_slash = __last_index_of(y, '/', len) % len; 197 | if (last_slash < 0) 198 | return; 199 | last_slash++; 200 | size_t end = len - last_slash; 201 | #pragma unroll 202 | for (size_t i = 0; i < len && i < end; i++) { 203 | x[i] = y[i + last_slash]; 204 | } 205 | } 206 | 207 | INLINE_STATIC void set_dirname(char *x, const char *y, size_t len) { 208 | int last_slash = __last_index_of(y, '/', len) % len; 209 | if (last_slash < 0) 210 | return; 211 | last_slash++; 212 | #pragma unroll 213 | for (size_t i = 0; i < len && i < last_slash; i++) { 214 | x[i] = y[i]; 215 | } 216 | } 217 | 218 | #endif // __PROBE_BPF_H 219 | -------------------------------------------------------------------------------- /libprobe/src/include/probe_macros.h: -------------------------------------------------------------------------------- 1 | #ifndef __MACROS_H 2 | #define __MACROS_H 3 | 4 | /* 5 | * general macros for variadic expansions 6 | */ 7 | #define GET_MACRO(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, \ 8 | NAME, ...) \ 9 | NAME 10 | 11 | #define FE_0(WHAT) 12 | #define FE_1(WHAT, _ctx) WHAT(_ctx) 13 | #define FE_2(WHAT, _ctx, ...) WHAT(_ctx) FE_1(WHAT, __VA_ARGS__) 14 | #define FE_3(WHAT, _ctx, ...) WHAT(_ctx) FE_2(WHAT, __VA_ARGS__) 15 | #define FE_4(WHAT, _ctx, ...) WHAT(_ctx) FE_3(WHAT, __VA_ARGS__) 16 | #define FE_5(WHAT, _ctx, ...) WHAT(_ctx) FE_4(WHAT, __VA_ARGS__) 17 | #define FE_6(WHAT, _ctx, ...) WHAT(_ctx) FE_5(WHAT, __VA_ARGS__) 18 | #define FE_7(WHAT, _ctx, ...) WHAT(_ctx) FE_6(WHAT, __VA_ARGS__) 19 | #define FE_8(WHAT, _ctx, ...) WHAT(_ctx) FE_7(WHAT, __VA_ARGS__) 20 | #define FE_9(WHAT, _ctx, ...) WHAT(_ctx) FE_8(WHAT, __VA_ARGS__) 21 | #define FE_10(WHAT, _ctx, ...) WHAT(_ctx) FE_9(WHAT, __VA_ARGS__) 22 | #define FE_11(WHAT, _ctx, ...) WHAT(_ctx) FE_10(WHAT, __VA_ARGS__) 23 | #define FE_12(WHAT, _ctx, ...) WHAT(_ctx) FE_11(WHAT, __VA_ARGS__) 24 | #define FE_13(WHAT, _ctx, ...) WHAT(_ctx) FE_12(WHAT, __VA_ARGS__) 25 | #define FOR_EACH0(action, ...) \ 26 | GET_MACRO(_0, __VA_ARGS__, FE_13, FE_12, FE_11, FE_10, FE_9, FE_8, FE_7, \ 27 | FE_6, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0) \ 28 | (action, __VA_ARGS__) 29 | 30 | #define FE1_0(WHAT) 31 | #define FE1_1(WHAT, _ctx, _x) WHAT(_ctx, _x) 32 | #define FE1_2(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_1(WHAT, _ctx, __VA_ARGS__) 33 | #define FE1_3(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_2(WHAT, _ctx, __VA_ARGS__) 34 | #define FE1_4(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_3(WHAT, _ctx, __VA_ARGS__) 35 | #define FE1_5(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_4(WHAT, _ctx, __VA_ARGS__) 36 | #define FE1_6(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_5(WHAT, _ctx, __VA_ARGS__) 37 | #define FE1_7(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_6(WHAT, _ctx, __VA_ARGS__) 38 | #define FE1_8(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_7(WHAT, _ctx, __VA_ARGS__) 39 | #define FE1_9(WHAT, _ctx, _x, ...) WHAT(_ctx, _x) FE1_8(WHAT, _ctx, __VA_ARGS__) 40 | #define FE1_10(WHAT, _ctx, _x, ...) \ 41 | WHAT(_ctx, _x) FE1_9(WHAT, _ctx, __VA_ARGS__) 42 | #define FE1_11(WHAT, _ctx, _x, ...) \ 43 | WHAT(_ctx, _x) FE1_10(WHAT, _ctx, __VA_ARGS__) 44 | #define FE1_12(WHAT, _ctx, _x, ...) \ 45 | WHAT(_ctx, _x) FE1_11(WHAT, _ctx, __VA_ARGS__) 46 | #define FE1_13(WHAT, _ctx, _x, ...) \ 47 | WHAT(_ctx, _x) FE1_12(WHAT, _ctx, __VA_ARGS__) 48 | #define FOR_EACH1(action, _ctx, ...) \ 49 | GET_MACRO(_0, __VA_ARGS__, FE1_13, FE1_12, FE1_11, FE1_10, FE1_9, FE1_8, \ 50 | FE1_7, FE1_6, FE1_5, FE1_4, FE1_3, FE1_2, FE1_1, FE1_0) \ 51 | (action, _ctx, __VA_ARGS__) 52 | 53 | #define FE2_0(WHAT) 54 | #define FE2_1(WHAT, _ctx, _x, _y) WHAT(_ctx, _x, _y) 55 | #define FE2_2(WHAT, _ctx, _x, _y, ...) \ 56 | WHAT(_ctx, _x, _y) FE2_1(WHAT, _ctx, _x, __VA_ARGS__) 57 | #define FE2_3(WHAT, _ctx, _x, _y, ...) \ 58 | WHAT(_ctx, _x, _y) FE2_2(WHAT, _ctx, _x, __VA_ARGS__) 59 | #define FE2_4(WHAT, _ctx, _x, _y, ...) \ 60 | WHAT(_ctx, _x, _y) FE2_3(WHAT, _ctx, _x, __VA_ARGS__) 61 | #define FE2_5(WHAT, _ctx, _x, _y, ...) \ 62 | WHAT(_ctx, _x, _y) FE2_4(WHAT, _ctx, _x, __VA_ARGS__) 63 | #define FE2_6(WHAT, _ctx, _x, _y, ...) \ 64 | WHAT(_ctx, _x, _y) FE2_5(WHAT, _ctx, _x, __VA_ARGS__) 65 | #define FE2_7(WHAT, _ctx, _x, _y, ...) \ 66 | WHAT(_ctx, _x, _y) FE2_6(WHAT, _ctx, _x, __VA_ARGS__) 67 | #define FE2_8(WHAT, _ctx, _x, _y, ...) \ 68 | WHAT(_ctx, _x, _y) FE2_7(WHAT, _ctx, _x, __VA_ARGS__) 69 | #define FE2_9(WHAT, _ctx, _x, _y, ...) \ 70 | WHAT(_ctx, _x, _y) FE2_8(WHAT, _ctx, _x, __VA_ARGS__) 71 | #define FE2_10(WHAT, _ctx, _x, _y, ...) \ 72 | WHAT(_ctx, _x, _y) FE2_9(WHAT, _ctx, _x, __VA_ARGS__) 73 | #define FE2_11(WHAT, _ctx, _x, _y, ...) \ 74 | WHAT(_ctx, _x, _y) FE2_10(WHAT, _ctx, _x, __VA_ARGS__) 75 | #define FE2_12(WHAT, _ctx, _x, _y, ...) \ 76 | WHAT(_ctx, _x, _y) FE2_11(WHAT, _ctx, _x, __VA_ARGS__) 77 | #define FE2_13(WHAT, _ctx, _x, _y, ...) \ 78 | WHAT(_ctx, _x, _y) FE2_12(WHAT, _ctx, _x, __VA_ARGS__) 79 | #define FOR_EACH2(action, _ctx, _x, ...) \ 80 | GET_MACRO(_0, __VA_ARGS__, FE2_13, FE2_12, FE2_11, FE2_10, FE2_9, FE2_8, \ 81 | FE2_7, FE2_6, FE2_5, FE2_4, FE2_3, FE2_2, FE2_1, FE2_0) \ 82 | (action, _ctx, _x, __VA_ARGS__) 83 | 84 | /* 85 | * macros for management of bpf program hooks 86 | */ 87 | #define DECLARE_HOOK(name) struct bpf_link *name##_hook; 88 | #define NULL_HOOK(s, name) s->name##_hook = NULL; 89 | #define SET_HANDLER_CONTEXT(s, config, name) \ 90 | if (config.name##_handler) { \ 91 | s->handlers->name##_handler->ctx = config.name##_ctx; \ 92 | s->handlers->name##_handler->handler = config.name##_handler; \ 93 | } 94 | #define ATTACH_HOOK_OR(s, label, name) \ 95 | s->name##_hook = bpf_program__attach(s->obj->progs.name##_hook); \ 96 | if (!s->name##_hook) { \ 97 | goto label; \ 98 | } 99 | #define DESTROY_HOOK(s, name) \ 100 | if (s->name##_hook) \ 101 | bpf_link__destroy(s->name##_hook); 102 | #define DECLARE_HOOKS(...) FOR_EACH0(DECLARE_HOOK, __VA_ARGS__) 103 | #define NULL_HOOKS(s, ...) FOR_EACH1(NULL_HOOK, s, __VA_ARGS__) 104 | #define SET_HANDLER_CONTEXTS(s, config, ...) \ 105 | FOR_EACH2(SET_HANDLER_CONTEXT, s, config, __VA_ARGS__) 106 | #define ATTACH_HOOKS_OR(s, label, ...) \ 107 | FOR_EACH2(ATTACH_HOOK_OR, s, label, __VA_ARGS__) 108 | #define DESTROY_HOOKS(s, ...) FOR_EACH1(DESTROY_HOOK, s, __VA_ARGS__) 109 | 110 | /* 111 | * macros for managing state configuration 112 | */ 113 | #define DECLARE_HANDLER_CONFIGURATION(name) \ 114 | void *name##_ctx; \ 115 | name##_event_handler *name##_handler; 116 | #define DECLARE_HANDLER_CONFIGURATIONS(...) \ 117 | FOR_EACH0(DECLARE_HANDLER_CONFIGURATION, __VA_ARGS__) 118 | 119 | /* 120 | * macros for management of bpf event handlers 121 | */ 122 | #define DECLARE_HANDLER_WRAPPER(name) \ 123 | struct handle_event_wrapper *name##_handler; 124 | #define DECLARE_HANDLER(name) \ 125 | typedef void name##_event_handler(void *ctx, struct bpf_##name##_event_t e); \ 126 | static int handle_##name##_event(void *ctx, \ 127 | struct bpf_##name##_event_t bpf_data) { \ 128 | struct handle_event_wrapper *handle = ctx; \ 129 | name##_event_handler *callback = handle->handler; \ 130 | callback(handle->ctx, bpf_data); \ 131 | return 0; \ 132 | } 133 | #define WRAP_HANDLER_OR(h, label, name) \ 134 | if (!(h->name##_handler = (struct handle_event_wrapper *)malloc( \ 135 | sizeof(struct handle_event_wrapper)))) \ 136 | goto label; 137 | #define HANDLER_CASE(name) \ 138 | case type_##name##_event_t: \ 139 | return handle_##name##_event(handlers->name##_handler, \ 140 | event->name##_event_t); 141 | #define DESTROY_HANDLER(h, name) \ 142 | if (h->name##_handler) \ 143 | free((void *)h->name##_handler); 144 | #define DECLARE_HANDLER_WRAPPERS(...) \ 145 | FOR_EACH0(DECLARE_HANDLER_WRAPPER, __VA_ARGS__) 146 | #define DECLARE_HANDLERS(...) FOR_EACH0(DECLARE_HANDLER, __VA_ARGS__) 147 | #define WRAP_HANDLERS_OR(h, label, ...) \ 148 | FOR_EACH2(WRAP_HANDLER_OR, h, label, __VA_ARGS__) 149 | #define HANDLER_CASES(...) FOR_EACH0(HANDLER_CASE, __VA_ARGS__) 150 | #define DESTROY_HANDLERS(h, ...) FOR_EACH1(DESTROY_HANDLER, h, __VA_ARGS__) 151 | 152 | /* 153 | * macros for managing rule addition 154 | */ 155 | #define DECLARE_RULE_FLUSHER(name) \ 156 | static unsigned int name##_rejections_size = 0; \ 157 | static unsigned int name##_filters_size = 0; \ 158 | void flush_##name##_rejection_rule(struct state *s, \ 159 | struct query_bpf_##name##_event_t rule) { \ 160 | bpf_map_update_elem(bpf_map__fd(s->obj->maps.name##_rejections), \ 161 | &name##_rejections_size, &rule, BPF_ANY); \ 162 | name##_rejections_size++; \ 163 | unsigned int index = name##_index; \ 164 | bpf_map_update_elem(bpf_map__fd(s->obj->maps.rejection_rule_sizes), \ 165 | &index, &name##_rejections_size, BPF_ANY); \ 166 | } \ 167 | \ 168 | void flush_##name##_filter_rule(struct state *s, \ 169 | struct query_bpf_##name##_event_t rule) { \ 170 | bpf_map_update_elem(bpf_map__fd(s->obj->maps.name##_filters), \ 171 | &name##_filters_size, &rule, BPF_ANY); \ 172 | name##_filters_size++; \ 173 | unsigned int index = name##_index; \ 174 | bpf_map_update_elem(bpf_map__fd(s->obj->maps.filter_rule_sizes), &index, \ 175 | &name##_filters_size, BPF_ANY); \ 176 | } 177 | #define DECLARE_RULE_FLUSHERS(...) FOR_EACH0(DECLARE_RULE_FLUSHER, __VA_ARGS__) 178 | 179 | #endif // __MACROS_H 180 | -------------------------------------------------------------------------------- /libprobe/src/include/probe.generated.h: -------------------------------------------------------------------------------- 1 | // Code generated by scripts/generate-structures - DO NOT EDIT. 2 | // to modify, regenerate after modifying templates/probe_bpf.generated.h.j2 3 | 4 | // clang-format off 5 | 6 | #ifndef __PROBE__GENERATED_H 7 | #define __PROBE__GENERATED_H 8 | 9 | #include "probe_common.h" 10 | 11 | #define EVENT_HOOKS bprm_check_security, inode_unlink 12 | #define ALL_HOOKS bprm_check_security, inode_unlink, sys_enter_execve, sys_exit_fork, sys_exit_vfork, sys_exit_clone, sys_exit_clone3, sched_process_free, inode_getattr 13 | 14 | #define bprm_check_security_index 0 15 | 16 | struct bpf_bprm_check_security_event_event_t { 17 | char action[256]; 18 | char outcome[256]; 19 | }; 20 | struct bpf_bprm_check_security_event_process_parent_t { 21 | unsigned int pid; 22 | char entity_id[256]; 23 | char name[256]; 24 | unsigned long args_count; 25 | char args[64][128]; 26 | unsigned int ppid; 27 | unsigned long start; 28 | unsigned long thread__id; 29 | char executable[256]; 30 | }; 31 | struct bpf_bprm_check_security_event_process_t { 32 | unsigned int pid; 33 | char entity_id[256]; 34 | char name[256]; 35 | unsigned int ppid; 36 | char executable[256]; 37 | unsigned long args_count; 38 | unsigned long start; 39 | unsigned long thread__id; 40 | char args[64][128]; 41 | struct bpf_bprm_check_security_event_process_parent_t parent; 42 | }; 43 | struct bpf_bprm_check_security_event_user_group_t { 44 | unsigned int id; 45 | }; 46 | struct bpf_bprm_check_security_event_user_effective_group_t { 47 | unsigned int id; 48 | }; 49 | struct bpf_bprm_check_security_event_user_effective_t { 50 | unsigned int id; 51 | struct bpf_bprm_check_security_event_user_effective_group_t group; 52 | }; 53 | struct bpf_bprm_check_security_event_user_t { 54 | unsigned int id; 55 | struct bpf_bprm_check_security_event_user_group_t group; 56 | struct bpf_bprm_check_security_event_user_effective_t effective; 57 | }; 58 | struct bpf_bprm_check_security_event_t { 59 | unsigned long __timestamp; 60 | struct bpf_bprm_check_security_event_event_t event; 61 | struct bpf_bprm_check_security_event_process_t process; 62 | struct bpf_bprm_check_security_event_user_t user; 63 | }; 64 | 65 | struct query_bpf_bprm_check_security_event_process_parent_t { 66 | char name___operator; 67 | char name[256]; 68 | char executable___operator; 69 | char executable[256]; 70 | }; 71 | struct query_bpf_bprm_check_security_event_process_t { 72 | char name___operator; 73 | char name[256]; 74 | char executable___operator; 75 | char executable[256]; 76 | struct query_bpf_bprm_check_security_event_process_parent_t parent; 77 | }; 78 | struct query_bpf_bprm_check_security_event_user_t { 79 | char id___operator; 80 | unsigned int id; 81 | }; 82 | struct query_bpf_bprm_check_security_event_t { 83 | char ___absolute; 84 | struct query_bpf_bprm_check_security_event_process_t process; 85 | struct query_bpf_bprm_check_security_event_user_t user; 86 | }; 87 | #define inode_unlink_index 1 88 | 89 | struct bpf_inode_unlink_event_event_t { 90 | char action[256]; 91 | char outcome[256]; 92 | }; 93 | struct bpf_inode_unlink_event_process_parent_t { 94 | unsigned int pid; 95 | char entity_id[256]; 96 | char name[256]; 97 | unsigned long args_count; 98 | char args[64][128]; 99 | unsigned int ppid; 100 | unsigned long start; 101 | unsigned long thread__id; 102 | char executable[256]; 103 | }; 104 | struct bpf_inode_unlink_event_process_t { 105 | unsigned int pid; 106 | char entity_id[256]; 107 | char name[256]; 108 | unsigned int ppid; 109 | char executable[256]; 110 | unsigned long args_count; 111 | unsigned long start; 112 | unsigned long thread__id; 113 | char args[64][128]; 114 | struct bpf_inode_unlink_event_process_parent_t parent; 115 | }; 116 | struct bpf_inode_unlink_event_user_group_t { 117 | unsigned int id; 118 | }; 119 | struct bpf_inode_unlink_event_user_effective_group_t { 120 | unsigned int id; 121 | }; 122 | struct bpf_inode_unlink_event_user_effective_t { 123 | unsigned int id; 124 | struct bpf_inode_unlink_event_user_effective_group_t group; 125 | }; 126 | struct bpf_inode_unlink_event_user_t { 127 | unsigned int id; 128 | struct bpf_inode_unlink_event_user_group_t group; 129 | struct bpf_inode_unlink_event_user_effective_t effective; 130 | }; 131 | struct bpf_inode_unlink_event_file_t { 132 | char path[256]; 133 | unsigned long inode; 134 | }; 135 | struct bpf_inode_unlink_event_t { 136 | unsigned long __timestamp; 137 | struct bpf_inode_unlink_event_event_t event; 138 | struct bpf_inode_unlink_event_process_t process; 139 | struct bpf_inode_unlink_event_user_t user; 140 | struct bpf_inode_unlink_event_file_t file; 141 | }; 142 | 143 | struct query_bpf_inode_unlink_event_process_t { 144 | char name___operator; 145 | char name[256]; 146 | }; 147 | struct query_bpf_inode_unlink_event_user_t { 148 | char id___operator; 149 | unsigned int id; 150 | }; 151 | struct query_bpf_inode_unlink_event_file_t { 152 | char path___operator; 153 | char path[256]; 154 | }; 155 | struct query_bpf_inode_unlink_event_t { 156 | char ___absolute; 157 | struct query_bpf_inode_unlink_event_process_t process; 158 | struct query_bpf_inode_unlink_event_user_t user; 159 | struct query_bpf_inode_unlink_event_file_t file; 160 | }; 161 | 162 | enum event_type { 163 | type_bprm_check_security_event_t, 164 | type_inode_unlink_event_t, 165 | }; 166 | 167 | struct bpf_event_t { 168 | enum event_type type; 169 | union { 170 | struct bpf_bprm_check_security_event_t bprm_check_security_event_t; 171 | struct bpf_inode_unlink_event_t inode_unlink_event_t; 172 | }; 173 | }; 174 | 175 | #ifdef BPF 176 | 177 | struct { 178 | __uint(type, BPF_MAP_TYPE_ARRAY); 179 | __uint(key_size, sizeof(u32)); 180 | __uint(value_size, sizeof(u32)); 181 | __uint(max_entries, 2); 182 | } filter_rule_sizes SEC(".maps"); 183 | 184 | struct { 185 | __uint(type, BPF_MAP_TYPE_ARRAY); 186 | __uint(key_size, sizeof(u32)); 187 | __uint(value_size, sizeof(u32)); 188 | __uint(max_entries, 2); 189 | } rejection_rule_sizes SEC(".maps"); 190 | 191 | INLINE_STATIC int ___test_bprm_check_security( 192 | struct bpf_bprm_check_security_event_t *event, 193 | struct query_bpf_bprm_check_security_event_t *rule 194 | ) { 195 | int conditional_true = 1; 196 | if (rule && event) { 197 | if (rule->___absolute == TRUE_ABSOLUTE) { 198 | return 1; 199 | } else if (rule->___absolute == FALSE_ABSOLUTE) { 200 | return 0; 201 | } else { 202 | if (rule->process.parent.name___operator == EQUAL_OPERATOR) { 203 | conditional_true = conditional_true && STRING_EQUALITY(event->process.parent.name,rule->process.parent.name); 204 | } else if (rule->process.parent.name___operator == NOT_EQUAL_OPERATOR) { 205 | conditional_true = conditional_true && STRING_INEQUALITY(event->process.parent.name, rule->process.parent.name); 206 | } 207 | if (rule->process.parent.executable___operator == EQUAL_OPERATOR) { 208 | conditional_true = conditional_true && STRING_EQUALITY(event->process.parent.executable,rule->process.parent.executable); 209 | } else if (rule->process.parent.executable___operator == NOT_EQUAL_OPERATOR) { 210 | conditional_true = conditional_true && STRING_INEQUALITY(event->process.parent.executable, rule->process.parent.executable); 211 | } 212 | if (rule->process.name___operator == EQUAL_OPERATOR) { 213 | conditional_true = conditional_true && STRING_EQUALITY(event->process.name,rule->process.name); 214 | } else if (rule->process.name___operator == NOT_EQUAL_OPERATOR) { 215 | conditional_true = conditional_true && STRING_INEQUALITY(event->process.name, rule->process.name); 216 | } 217 | if (rule->process.executable___operator == EQUAL_OPERATOR) { 218 | conditional_true = conditional_true && STRING_EQUALITY(event->process.executable,rule->process.executable); 219 | } else if (rule->process.executable___operator == NOT_EQUAL_OPERATOR) { 220 | conditional_true = conditional_true && STRING_INEQUALITY(event->process.executable, rule->process.executable); 221 | } 222 | if (rule->user.id___operator == EQUAL_OPERATOR) { 223 | conditional_true = conditional_true && NUMBER_EQUALITY(event->user.id,rule->user.id); 224 | } else if (rule->user.id___operator == NOT_EQUAL_OPERATOR) { 225 | conditional_true = conditional_true && NUMBER_INEQUALITY(event->user.id, rule->user.id); 226 | } 227 | } 228 | } 229 | 230 | return conditional_true; 231 | } 232 | 233 | INLINE_STATIC int ___check_bprm_check_security( 234 | unsigned int size, 235 | void *rule_map, 236 | struct bpf_bprm_check_security_event_t *event 237 | ) { 238 | int conditional_true = 0; 239 | if (!rule_map) return conditional_true; 240 | #pragma unroll 241 | for (int i = 0; i < MAX_RULE_SIZE; i++) { 242 | unsigned int index = i; 243 | if (index >= size) { 244 | return conditional_true; 245 | } 246 | struct query_bpf_bprm_check_security_event_t *rule = bpf_map_lookup_elem(rule_map, &index); 247 | conditional_true = conditional_true || ___test_bprm_check_security(event, rule); 248 | } 249 | return conditional_true; 250 | } 251 | 252 | struct { 253 | __uint(type, BPF_MAP_TYPE_ARRAY); 254 | __uint(key_size, sizeof(u32)); 255 | __uint(value_size, sizeof(struct query_bpf_bprm_check_security_event_t)); 256 | __uint(max_entries, 8); 257 | } bprm_check_security_filters SEC(".maps"); 258 | 259 | struct { 260 | __uint(type, BPF_MAP_TYPE_ARRAY); 261 | __uint(key_size, sizeof(u32)); 262 | __uint(value_size, sizeof(struct query_bpf_bprm_check_security_event_t)); 263 | __uint(max_entries, 8); 264 | } bprm_check_security_rejections SEC(".maps"); 265 | INLINE_STATIC int ___test_inode_unlink( 266 | struct bpf_inode_unlink_event_t *event, 267 | struct query_bpf_inode_unlink_event_t *rule 268 | ) { 269 | int conditional_true = 1; 270 | if (rule && event) { 271 | if (rule->___absolute == TRUE_ABSOLUTE) { 272 | return 1; 273 | } else if (rule->___absolute == FALSE_ABSOLUTE) { 274 | return 0; 275 | } else { 276 | if (rule->process.name___operator == EQUAL_OPERATOR) { 277 | conditional_true = conditional_true && STRING_EQUALITY(event->process.name,rule->process.name); 278 | } else if (rule->process.name___operator == NOT_EQUAL_OPERATOR) { 279 | conditional_true = conditional_true && STRING_INEQUALITY(event->process.name, rule->process.name); 280 | } 281 | if (rule->user.id___operator == EQUAL_OPERATOR) { 282 | conditional_true = conditional_true && NUMBER_EQUALITY(event->user.id,rule->user.id); 283 | } else if (rule->user.id___operator == NOT_EQUAL_OPERATOR) { 284 | conditional_true = conditional_true && NUMBER_INEQUALITY(event->user.id, rule->user.id); 285 | } 286 | if (rule->file.path___operator == EQUAL_OPERATOR) { 287 | conditional_true = conditional_true && STRING_EQUALITY(event->file.path,rule->file.path); 288 | } else if (rule->file.path___operator == NOT_EQUAL_OPERATOR) { 289 | conditional_true = conditional_true && STRING_INEQUALITY(event->file.path, rule->file.path); 290 | } 291 | } 292 | } 293 | 294 | return conditional_true; 295 | } 296 | 297 | INLINE_STATIC int ___check_inode_unlink( 298 | unsigned int size, 299 | void *rule_map, 300 | struct bpf_inode_unlink_event_t *event 301 | ) { 302 | int conditional_true = 0; 303 | if (!rule_map) return conditional_true; 304 | #pragma unroll 305 | for (int i = 0; i < MAX_RULE_SIZE; i++) { 306 | unsigned int index = i; 307 | if (index >= size) { 308 | return conditional_true; 309 | } 310 | struct query_bpf_inode_unlink_event_t *rule = bpf_map_lookup_elem(rule_map, &index); 311 | conditional_true = conditional_true || ___test_inode_unlink(event, rule); 312 | } 313 | return conditional_true; 314 | } 315 | 316 | struct { 317 | __uint(type, BPF_MAP_TYPE_ARRAY); 318 | __uint(key_size, sizeof(u32)); 319 | __uint(value_size, sizeof(struct query_bpf_inode_unlink_event_t)); 320 | __uint(max_entries, 8); 321 | } inode_unlink_filters SEC(".maps"); 322 | 323 | struct { 324 | __uint(type, BPF_MAP_TYPE_ARRAY); 325 | __uint(key_size, sizeof(u32)); 326 | __uint(value_size, sizeof(struct query_bpf_inode_unlink_event_t)); 327 | __uint(max_entries, 8); 328 | } inode_unlink_rejections SEC(".maps"); 329 | 330 | #endif 331 | 332 | #endif // __PROBE__GENERATED_H 333 | 334 | // clang-format on -------------------------------------------------------------------------------- /probe-sys/src/compiler_generated.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | 3 | use rule_compiler::{Atom, Operation, Operator, QueryWriter}; 4 | use std::convert::TryFrom; 5 | use std::os::raw::c_char; 6 | 7 | use crate::constants::UNSET_OPERATOR; 8 | use crate::helpers::operator_to_constant; 9 | use crate::query_writer::InnerBpfQueryWriter; 10 | use crate::traits::QueryStruct; 11 | 12 | #[repr(C)] 13 | #[derive(Debug, Copy, Clone, PartialEq)] 14 | pub struct query_bpf_bprm_check_security_event_process_parent_t { 15 | pub name___operator: u8, 16 | pub name: [c_char; 256], 17 | pub executable___operator: u8, 18 | pub executable: [c_char; 256], 19 | } 20 | 21 | impl Default for query_bpf_bprm_check_security_event_process_parent_t { 22 | fn default() -> Self { 23 | unsafe { std::mem::zeroed() } 24 | } 25 | } 26 | #[repr(C)] 27 | #[derive(Debug, Copy, Clone, PartialEq)] 28 | pub struct query_bpf_bprm_check_security_event_process_t { 29 | pub name___operator: u8, 30 | pub name: [c_char; 256], 31 | pub executable___operator: u8, 32 | pub executable: [c_char; 256], 33 | pub parent: query_bpf_bprm_check_security_event_process_parent_t, 34 | } 35 | 36 | impl Default for query_bpf_bprm_check_security_event_process_t { 37 | fn default() -> Self { 38 | unsafe { std::mem::zeroed() } 39 | } 40 | } 41 | #[repr(C)] 42 | #[derive(Debug, Copy, Clone, PartialEq)] 43 | pub struct query_bpf_bprm_check_security_event_user_t { 44 | pub id___operator: u8, 45 | pub id: u32, 46 | } 47 | 48 | impl Default for query_bpf_bprm_check_security_event_user_t { 49 | fn default() -> Self { 50 | unsafe { std::mem::zeroed() } 51 | } 52 | } 53 | #[repr(C)] 54 | #[derive(Debug, Copy, Clone, PartialEq)] 55 | pub struct query_bpf_bprm_check_security_event_t { 56 | pub ___absolute: u8, 57 | pub process: query_bpf_bprm_check_security_event_process_t, 58 | pub user: query_bpf_bprm_check_security_event_user_t, 59 | } 60 | 61 | impl Default for query_bpf_bprm_check_security_event_t { 62 | fn default() -> Self { 63 | unsafe { std::mem::zeroed() } 64 | } 65 | } 66 | 67 | impl QueryStruct for query_bpf_bprm_check_security_event_t { 68 | fn set_absolute(&mut self, value: u8) { 69 | self.___absolute = value; 70 | } 71 | 72 | fn set_number(&mut self, path: String, operator: Operator, value: u64) -> Result<(), String> { 73 | match path.as_str() { 74 | "user.id" => { 75 | if self.user.id___operator != UNSET_OPERATOR { 76 | // we can only hold a single condition per variable for now 77 | return Err(format!("{} already in condition", path)); 78 | } 79 | let v = u32::try_from(value).map_err(|_| String::from("user.id must be a u32"))?; 80 | self.user.id = v; 81 | self.user.id___operator = operator_to_constant(operator); 82 | Ok(()) 83 | } 84 | _ => Err(format!("numeric field named {} not found in schema", path)), 85 | } 86 | } 87 | 88 | fn set_string( 89 | &mut self, 90 | path: String, 91 | operator: Operator, 92 | value: String, 93 | ) -> Result<(), String> { 94 | match path.as_str() { 95 | "process.parent.name" => { 96 | if self.process.parent.name___operator != UNSET_OPERATOR { 97 | // we can only hold a single condition per variable for now 98 | return Err(format!("{} already in condition", path)); 99 | } 100 | if value.len() < 256 { 101 | for (dest, src) in self.process.parent.name.iter_mut().zip(value.as_bytes().iter()) { 102 | *dest = *src as _; 103 | } 104 | self.process.parent.name___operator = operator_to_constant(operator); 105 | Ok(()) 106 | } else { 107 | Err(format!("process.parent.name is too long, maximum 256 characters, given value is {} characters", value.len())) 108 | } 109 | }, 110 | "process.parent.executable" => { 111 | if self.process.parent.executable___operator != UNSET_OPERATOR { 112 | // we can only hold a single condition per variable for now 113 | return Err(format!("{} already in condition", path)); 114 | } 115 | if value.len() < 256 { 116 | for (dest, src) in self.process.parent.executable.iter_mut().zip(value.as_bytes().iter()) { 117 | *dest = *src as _; 118 | } 119 | self.process.parent.executable___operator = operator_to_constant(operator); 120 | Ok(()) 121 | } else { 122 | Err(format!("process.parent.executable is too long, maximum 256 characters, given value is {} characters", value.len())) 123 | } 124 | }, 125 | "process.name" => { 126 | if self.process.name___operator != UNSET_OPERATOR { 127 | // we can only hold a single condition per variable for now 128 | return Err(format!("{} already in condition", path)); 129 | } 130 | if value.len() < 256 { 131 | for (dest, src) in self.process.name.iter_mut().zip(value.as_bytes().iter()) { 132 | *dest = *src as _; 133 | } 134 | self.process.name___operator = operator_to_constant(operator); 135 | Ok(()) 136 | } else { 137 | Err(format!("process.name is too long, maximum 256 characters, given value is {} characters", value.len())) 138 | } 139 | }, 140 | "process.executable" => { 141 | if self.process.executable___operator != UNSET_OPERATOR { 142 | // we can only hold a single condition per variable for now 143 | return Err(format!("{} already in condition", path)); 144 | } 145 | if value.len() < 256 { 146 | for (dest, src) in self.process.executable.iter_mut().zip(value.as_bytes().iter()) { 147 | *dest = *src as _; 148 | } 149 | self.process.executable___operator = operator_to_constant(operator); 150 | Ok(()) 151 | } else { 152 | Err(format!("process.executable is too long, maximum 256 characters, given value is {} characters", value.len())) 153 | } 154 | }, 155 | _ => Err(format!("string field named {} not found in schema", path)), 156 | } 157 | } 158 | 159 | fn flush<'a>(&mut self, _probe: &'a super::Probe<'a>) -> Result<(), String> { 160 | Ok(()) 161 | } 162 | } 163 | #[repr(C)] 164 | #[derive(Debug, Copy, Clone, PartialEq)] 165 | pub struct query_bpf_inode_unlink_event_process_t { 166 | pub name___operator: u8, 167 | pub name: [c_char; 256], 168 | } 169 | 170 | impl Default for query_bpf_inode_unlink_event_process_t { 171 | fn default() -> Self { 172 | unsafe { std::mem::zeroed() } 173 | } 174 | } 175 | #[repr(C)] 176 | #[derive(Debug, Copy, Clone, PartialEq)] 177 | pub struct query_bpf_inode_unlink_event_user_t { 178 | pub id___operator: u8, 179 | pub id: u32, 180 | } 181 | 182 | impl Default for query_bpf_inode_unlink_event_user_t { 183 | fn default() -> Self { 184 | unsafe { std::mem::zeroed() } 185 | } 186 | } 187 | #[repr(C)] 188 | #[derive(Debug, Copy, Clone, PartialEq)] 189 | pub struct query_bpf_inode_unlink_event_file_t { 190 | pub path___operator: u8, 191 | pub path: [c_char; 256], 192 | } 193 | 194 | impl Default for query_bpf_inode_unlink_event_file_t { 195 | fn default() -> Self { 196 | unsafe { std::mem::zeroed() } 197 | } 198 | } 199 | #[repr(C)] 200 | #[derive(Debug, Copy, Clone, PartialEq)] 201 | pub struct query_bpf_inode_unlink_event_t { 202 | pub ___absolute: u8, 203 | pub process: query_bpf_inode_unlink_event_process_t, 204 | pub user: query_bpf_inode_unlink_event_user_t, 205 | pub file: query_bpf_inode_unlink_event_file_t, 206 | } 207 | 208 | impl Default for query_bpf_inode_unlink_event_t { 209 | fn default() -> Self { 210 | unsafe { std::mem::zeroed() } 211 | } 212 | } 213 | 214 | impl QueryStruct for query_bpf_inode_unlink_event_t { 215 | fn set_absolute(&mut self, value: u8) { 216 | self.___absolute = value; 217 | } 218 | 219 | fn set_number(&mut self, path: String, operator: Operator, value: u64) -> Result<(), String> { 220 | match path.as_str() { 221 | "user.id" => { 222 | if self.user.id___operator != UNSET_OPERATOR { 223 | // we can only hold a single condition per variable for now 224 | return Err(format!("{} already in condition", path)); 225 | } 226 | let v = u32::try_from(value).map_err(|_| String::from("user.id must be a u32"))?; 227 | self.user.id = v; 228 | self.user.id___operator = operator_to_constant(operator); 229 | Ok(()) 230 | } 231 | _ => Err(format!("numeric field named {} not found in schema", path)), 232 | } 233 | } 234 | 235 | fn set_string( 236 | &mut self, 237 | path: String, 238 | operator: Operator, 239 | value: String, 240 | ) -> Result<(), String> { 241 | match path.as_str() { 242 | "process.name" => { 243 | if self.process.name___operator != UNSET_OPERATOR { 244 | // we can only hold a single condition per variable for now 245 | return Err(format!("{} already in condition", path)); 246 | } 247 | if value.len() < 256 { 248 | for (dest, src) in self.process.name.iter_mut().zip(value.as_bytes().iter()) { 249 | *dest = *src as _; 250 | } 251 | self.process.name___operator = operator_to_constant(operator); 252 | Ok(()) 253 | } else { 254 | Err(format!("process.name is too long, maximum 256 characters, given value is {} characters", value.len())) 255 | } 256 | }, 257 | "file.path" => { 258 | if self.file.path___operator != UNSET_OPERATOR { 259 | // we can only hold a single condition per variable for now 260 | return Err(format!("{} already in condition", path)); 261 | } 262 | if value.len() < 256 { 263 | for (dest, src) in self.file.path.iter_mut().zip(value.as_bytes().iter()) { 264 | *dest = *src as _; 265 | } 266 | self.file.path___operator = operator_to_constant(operator); 267 | Ok(()) 268 | } else { 269 | Err(format!("file.path is too long, maximum 256 characters, given value is {} characters", value.len())) 270 | } 271 | }, 272 | _ => Err(format!("string field named {} not found in schema", path)), 273 | } 274 | } 275 | 276 | fn flush<'a>(&mut self, _probe: &'a super::Probe<'a>) -> Result<(), String> { 277 | Ok(()) 278 | } 279 | } 280 | 281 | pub struct BpfQueryWriter<'a> { 282 | table: String, 283 | write_query_bprm_check_security_event_t: InnerBpfQueryWriter, 284 | write_query_inode_unlink_event_t: InnerBpfQueryWriter, 285 | probe: Option<&'a super::Probe<'a>>, 286 | } 287 | 288 | impl<'a> BpfQueryWriter<'a> { 289 | pub fn new(probe: Option<&'a super::Probe>, table: String, operation: Operation) -> Self { 290 | Self { 291 | table: table, 292 | write_query_bprm_check_security_event_t: InnerBpfQueryWriter::::new( 293 | "bprm_check_security".into(), 294 | operation, 295 | 8, 296 | ), 297 | write_query_inode_unlink_event_t: InnerBpfQueryWriter::::new( 298 | "inode_unlink".into(), 299 | operation, 300 | 8, 301 | ), 302 | probe: probe, 303 | } 304 | } 305 | } 306 | 307 | impl<'b> QueryWriter for BpfQueryWriter<'b> { 308 | fn write_statement<'a>( 309 | &mut self, 310 | field: &'a str, 311 | operator: &'a Operator, 312 | atom: &'a Atom, 313 | ) -> Result<(), String> { 314 | match self.table.as_str() { 315 | "bprm_check_security" => self.write_query_bprm_check_security_event_t.write_statement(field, operator, atom), 316 | "inode_unlink" => self.write_query_inode_unlink_event_t.write_statement(field, operator, atom), 317 | _ => Err(format!("invalid table name {}", self.table)), 318 | } 319 | } 320 | 321 | fn start_new_clause(&mut self) -> Result<(), String> { 322 | match self.table.as_str() { 323 | "bprm_check_security" => self.write_query_bprm_check_security_event_t.start_new_clause(), 324 | "inode_unlink" => self.write_query_inode_unlink_event_t.start_new_clause(), 325 | _ => Err(format!("invalid table name {}", self.table)), 326 | } 327 | } 328 | 329 | fn write_absolute(&mut self, value: bool) -> Result<(), String> { 330 | match self.table.as_str() { 331 | "bprm_check_security" => self.write_query_bprm_check_security_event_t.write_absolute(value), 332 | "inode_unlink" => self.write_query_inode_unlink_event_t.write_absolute(value), 333 | _ => Err(format!("invalid table name {}", self.table)), 334 | } 335 | } 336 | 337 | fn flush(&mut self) -> Result<(), String> { 338 | match self.probe { 339 | Some(probe) => match self.table.as_str() { 340 | "bprm_check_security" => self.write_query_bprm_check_security_event_t.flush_probe(probe), 341 | "inode_unlink" => self.write_query_inode_unlink_event_t.flush_probe(probe), 342 | _ => Err(format!("invalid table name {}", self.table)), 343 | }, 344 | _ => Ok(()) 345 | } 346 | } 347 | } -------------------------------------------------------------------------------- /scripts/generate-structures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import path 4 | from yaml import load, Loader 5 | from typing import NamedTuple 6 | import re 7 | import subprocess 8 | 9 | from jinja2 import Template 10 | 11 | CURRENT_DIRECTORY = path.dirname(path.realpath(__file__)) 12 | SCHEMAS_DIRECTORY = path.abspath(path.join(CURRENT_DIRECTORY, "..", "schemas")) 13 | OUTPUT_DIRECTORY = path.abspath(path.join(CURRENT_DIRECTORY, "..", "src")) 14 | LSM_MODULES = ["bprm_check_security", "inode_unlink"] 15 | SLEEPABLE_LSM_MODULES = ["inode_getattr"] 16 | TRACEPOINTS = [ 17 | ("syscalls", "sys_enter_execve"), 18 | ("syscalls", "sys_exit_fork"), 19 | ("syscalls", "sys_exit_vfork"), 20 | ("syscalls", "sys_exit_clone"), 21 | ("syscalls", "sys_exit_clone3"), 22 | ("sched", "sched_process_free"), 23 | ] 24 | TEMPLATES = [ 25 | ("libprobe", ["include", "probe.generated.h.j2"]), 26 | ("probe-sys", ["ffi_generated.rs.j2"]), 27 | ("probe-sys", ["probe_generated.rs.j2"]), 28 | ("probe-sys", ["serial_generated.rs.j2"]), 29 | ("probe-sys", ["transform_generated.rs.j2"]), 30 | ("probe-sys", ["compiler_generated.rs.j2"]), 31 | ("probe-sys", ["struct.proto.j2"]), 32 | ] 33 | 34 | 35 | def get_template_path(project, filepath): 36 | return path.join(CURRENT_DIRECTORY, "..", project, "templates", *filepath) 37 | 38 | 39 | def get_output_path(project, filepath): 40 | output_name = path.join(CURRENT_DIRECTORY, "..", project, "src", *filepath) 41 | return path.splitext(output_name)[0] 42 | 43 | 44 | def render(all_hooks, event_hooks, modules, tracepoints): 45 | for (project, filepath) in TEMPLATES: 46 | template_path = get_template_path(project, filepath) 47 | output_path = get_output_path(project, filepath) 48 | with open(template_path) as template_file: 49 | template = Template(template_file.read(), trim_blocks=True) 50 | with open(output_path, "w") as output: 51 | output.write( 52 | template.render(all_hooks=all_hooks, 53 | event_hooks=event_hooks, 54 | modules=modules, 55 | tracepoints=tracepoints)) 56 | if project == "libprobe": 57 | subprocess.run(["clang-format", "-i", output_path], check=True) 58 | 59 | 60 | # add to this as need be 61 | NAME_NORMALIZATION = re.compile(r"[@\.]") 62 | UNDERSCORE_NORMALIZATION = re.compile(r"__") 63 | 64 | 65 | def normalize_name(name): 66 | return NAME_NORMALIZATION.sub("__", name) 67 | 68 | 69 | def rustify(name): 70 | keywords = ["type"] 71 | if name in keywords: 72 | return "field_%s" % name 73 | if name == "__timestamp": 74 | return "timestamp" 75 | return UNDERSCORE_NORMALIZATION.sub("_", name) 76 | 77 | 78 | def rustify_class(name): 79 | return ''.join(word.title() for word in name.split('_')) 80 | 81 | 82 | class Tracepoint(NamedTuple): 83 | family: str 84 | hook: str 85 | 86 | 87 | class Transformation(NamedTuple): 88 | method: str 89 | 90 | 91 | class Type: 92 | def __init__(self, 93 | final, 94 | proto, 95 | rust, 96 | name, 97 | size=None, 98 | transformation=None, 99 | lifetime=False, 100 | setter=None): 101 | self.transformation = transformation 102 | self.final = final 103 | self.proto = proto 104 | self.rust = rust 105 | self.name = name 106 | self.size = size 107 | self.lifetime = lifetime 108 | self.setter = setter 109 | 110 | def __render_c_name(self): 111 | if isinstance(self.name, dict): 112 | return self.name.get('type') 113 | else: 114 | return self.name 115 | 116 | def __render_c_suffix(self): 117 | if isinstance(self.name, dict): 118 | return self.name.get('suffix') 119 | elif self.size: 120 | return "[%d]" % self.size 121 | else: 122 | return "" 123 | 124 | def render_c(self, prefix, name, complex): 125 | if complex: 126 | return "struct %s%s %s%s;" % (prefix, self.__render_c_name(), name, 127 | self.__render_c_suffix()) 128 | else: 129 | return "%s %s%s;" % (self.__render_c_name(), name, 130 | self.__render_c_suffix()) 131 | 132 | def __render_rust_setter_name(self, dest): 133 | if self.setter: 134 | return self.setter 135 | return "set_%s" % dest 136 | 137 | def __render_rust_setter_transformation(self, dest, source): 138 | extra = "" 139 | if isinstance(self.transformation, dict): 140 | method = self.transformation.get("method") 141 | extra = self.transformation.get("extra", "") 142 | else: 143 | method = self.transformation.method 144 | if extra != "": 145 | extra += ", " 146 | return "event.%s(%s(%se.%s.into()));" % ( 147 | self.__render_rust_setter_name(dest), method, extra, source) 148 | 149 | def render_rust_setter(self, dest, source): 150 | if not self.transformation: 151 | return "event.set_%s(e.%s);" % (dest, source) 152 | return self.__render_rust_setter_transformation(dest, source) 153 | 154 | 155 | BPF_TYPES = { 156 | 'date': 157 | Type('u64', 'uint64', 'u64', 'unsigned long'), 158 | 'long': 159 | Type('u64', 'uint64', 'u64', 'unsigned long'), 160 | 'keyword': 161 | Type('String', 'string', 'c_char', 'char', 256, 162 | Transformation('transform_string')), 163 | 'wildcard': 164 | Type('String', 'string', 'c_char', 'char', 256, 165 | Transformation('transform_string')), 166 | } 167 | 168 | 169 | class Field: 170 | def __init__(self, type, enrichment, final, name, original, complex, 171 | queryable, path): 172 | self.type = type 173 | self.enrichment = enrichment 174 | self.final = final 175 | self.name = name 176 | self.original = original 177 | self.complex = complex 178 | self.queryable = queryable 179 | self.path = path 180 | 181 | def render_rust_setter(self): 182 | if self.complex: 183 | if self.enrichment: 184 | return "event.%s = Some(Default::default()).into();" % self.final 185 | else: 186 | return "event.%s = Some(e.%s.into()).into();" % (self.final, 187 | self.name) 188 | if not self.enrichment: 189 | return self.type.render_rust_setter(self.final, self.name) 190 | 191 | def render_c_field(self): 192 | if not self.enrichment: 193 | return self.type.render_c("bpf_", self.name, self.complex) 194 | 195 | def __render_c_query_field_op(self): 196 | if not self.complex: 197 | return "char %s___operator;" % self.name 198 | 199 | def __render_c_query_field(self): 200 | return self.type.render_c("query_bpf_", self.name, self.complex) 201 | 202 | def render_c_query_field(self): 203 | if self.queryable: 204 | decls = [ 205 | self.__render_c_query_field_op(), 206 | self.__render_c_query_field() 207 | ] 208 | compacted = [d for d in decls if d] 209 | return "\n ".join(compacted) 210 | 211 | 212 | class Structure: 213 | rust_from_ffi_template = ''' 214 | impl From for %s { 215 | fn from(e: ffi::%s) -> Self { 216 | let mut event = Self::default(); 217 | %s 218 | event 219 | } 220 | } 221 | ''' 222 | 223 | def __init__(self, final, name, enrichment, lifetime, fields, queryable): 224 | self.final = final 225 | self.name = name 226 | self.enrichment = enrichment 227 | self.lifetime = lifetime 228 | self.fields = fields 229 | self.queryable = queryable 230 | 231 | def render_rust_from_ffi(self): 232 | if not self.enrichment: 233 | decls = [f.render_rust_setter() for f in self.fields] 234 | compacted = [d for d in decls if d] 235 | return self.rust_from_ffi_template.strip() % ( 236 | self.name, self.final, self.name, "\n ".join(compacted)) 237 | 238 | def __render_c_fields(self): 239 | decls = [f.render_c_field() for f in self.fields] 240 | compacted = [d for d in decls if d] 241 | return "\n ".join(compacted) 242 | 243 | def __render_c_query_fields(self, entrypoint): 244 | decls = [f.render_c_query_field() for f in self.fields] 245 | compacted = [d for d in decls if d] 246 | if entrypoint: 247 | compacted.insert(0, "char ___absolute;") 248 | return "\n ".join(compacted) 249 | 250 | def render_c_field_struct(self): 251 | if not self.enrichment: 252 | return "struct bpf_%s {\n %s\n};" % (self.name, 253 | self.__render_c_fields()) 254 | 255 | def render_c_query_struct(self, entrypoint): 256 | if self.queryable: 257 | return "struct query_bpf_%s {\n %s\n};" % ( 258 | self.name, self.__render_c_query_fields(entrypoint)) 259 | 260 | 261 | class Module: 262 | def __init__(self, filename): 263 | with open(filename) as file: 264 | self.name = path.splitext(path.basename(filename))[0] 265 | self.final = rustify_class(self.name) 266 | self.data = load(file, Loader=Loader) 267 | self.structures = self.__build_structs(BPF_TYPES, False) 268 | 269 | def __structure_name(self, namespaces): 270 | return "_".join([self.name, "event"] + namespaces + ["t"]) 271 | 272 | def __rusty_name(self, namespaces): 273 | name = "_".join([self.name, "event"] + namespaces) 274 | return rustify_class(name) 275 | 276 | def __render_module_index(self, index): 277 | return "#define %s_index %d" % (self.name, index) 278 | 279 | def __render_c_field_structs(self): 280 | decls = [s.render_c_field_struct() for s in self.structures] 281 | compacted = [d for d in decls if d] 282 | return "\n".join(compacted) 283 | 284 | def __render_c_query_structs(self): 285 | decls = [ 286 | s.render_c_query_struct(s == self.structures[-1]) 287 | for s in self.structures 288 | ] 289 | compacted = [d for d in decls if d] 290 | return "\n".join(compacted) 291 | 292 | def __build_structs(self, 293 | types, 294 | lifetime, 295 | enrichment=False, 296 | namespaces=[], 297 | child=None): 298 | data = child if child else self.data 299 | structures = [] 300 | fields = [] 301 | parent_queryable = False 302 | for field in data: 303 | field_enrichment = field.get('enrichment', enrichment) 304 | name = field.get('name') 305 | field_type = field.get('type') 306 | normalized_name = normalize_name(name) 307 | rust_name = rustify(normalized_name) 308 | complex = False 309 | queryable = False 310 | if field_type == 'group': 311 | complex = True 312 | child_namespaces = namespaces + [normalized_name] 313 | child_lifetime = field.get('lifetime', False) 314 | if child_lifetime: 315 | lifetime = True 316 | child_structures = self.__build_structs( 317 | types, child_lifetime, field_enrichment, child_namespaces, 318 | field.get('fields')) 319 | for c in child_structures: 320 | if c.queryable: 321 | queryable = True 322 | parent_queryable = True 323 | break 324 | field_type = Type(self.__rusty_name(child_namespaces), 325 | self.__rusty_name(child_namespaces), 326 | self.__structure_name(child_namespaces), 327 | self.__structure_name(child_namespaces), 328 | None, None, child_lifetime, None) 329 | structures += child_structures 330 | else: 331 | queryable = field.get('queryable', False) 332 | if queryable: 333 | parent_queryable = True 334 | if 'override' in field: 335 | field_type = Type(field['override']['final'], 336 | field['override']['proto'], 337 | field['override']['rust'], 338 | field['override']['c'], None, 339 | field['override'].get('transform', 340 | None), False, 341 | field['override'].get('setter', None)) 342 | else: 343 | field_type = types[field_type] 344 | field_path = ".".join(namespaces) 345 | if len(namespaces) > 0: 346 | field_path += "." 347 | fields.append( 348 | Field(field_type, field_enrichment, rust_name, normalized_name, 349 | name, complex, queryable, field_path)) 350 | structure = Structure(self.__rusty_name(namespaces), 351 | self.__structure_name(namespaces), enrichment, 352 | lifetime, fields, parent_queryable) 353 | structures.append(structure) 354 | return structures 355 | 356 | def render_rust_from_ffi(self): 357 | decls = [s.render_rust_from_ffi() for s in self.structures] 358 | compacted = [d for d in decls if d] 359 | return "\n\n".join(compacted) 360 | 361 | def render_c(self, index): 362 | return "\n\n".join([ 363 | self.__render_module_index(index), 364 | self.__render_c_field_structs(), 365 | self.__render_c_query_structs(), 366 | ]) 367 | 368 | 369 | if __name__ == "__main__": 370 | schema_files = [ 371 | path.join(SCHEMAS_DIRECTORY, "%s.yml" % f) for f in LSM_MODULES 372 | ] 373 | modules = [Module(f) for f in schema_files] 374 | tracepoints = [Tracepoint(family, hook) for (family, hook) in TRACEPOINTS] 375 | all_hooks = ", ".join([m.name 376 | for m in modules] + [t.hook for t in tracepoints] + SLEEPABLE_LSM_MODULES) 377 | event_hooks = ", ".join([m.name for m in modules]) 378 | render(all_hooks, event_hooks, modules, tracepoints) 379 | --------------------------------------------------------------------------------