diff --git a/fact-api/build.rs b/fact-api/build.rs index 4a84a8ed..4bd67073 100644 --- a/fact-api/build.rs +++ b/fact-api/build.rs @@ -3,8 +3,12 @@ use anyhow::Context; fn main() -> anyhow::Result<()> { tonic_prost_build::configure() .build_server(false) + .include_file("mod.rs") .compile_protos( - &["../third_party/stackrox/proto/internalapi/sensor/sfa_iservice.proto"], + &[ + "../third_party/stackrox/proto/internalapi/sensor/sfa_iservice.proto", + "../third_party/stackrox/proto/internalapi/sensor/signal_iservice.proto", + ], &["../third_party/stackrox/proto"], ) .context("Failed to compile protos. Please makes sure you update your git submodules!")?; diff --git a/fact-api/src/lib.rs b/fact-api/src/lib.rs index 5d145007..4d7dd058 100644 --- a/fact-api/src/lib.rs +++ b/fact-api/src/lib.rs @@ -1 +1 @@ -tonic::include_proto!("sensor"); +tonic::include_proto!("mod"); diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index 38395132..a3858373 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -1,9 +1,14 @@ -#include +#pragma once + +// clang-format off +#include "vmlinux.h" #include "maps.h" #include "process.h" #include "types.h" -#include "vmlinux.h" + +#include +// clang-format on __always_inline static void submit_event(struct metrics_by_hook_t* m, file_activity_type_t event_type, const char filename[PATH_MAX], struct dentry* dentry) { struct event_t* event = bpf_ringbuf_reserve(&rb, sizeof(struct event_t), 0); @@ -14,12 +19,16 @@ __always_inline static void submit_event(struct metrics_by_hook_t* m, file_activ event->type = event_type; event->timestamp = bpf_ktime_get_boot_ns(); - bpf_probe_read_str(event->filename, PATH_MAX, filename); + if (filename != NULL) { + bpf_probe_read_str(event->filename, PATH_MAX, filename); + } - struct helper_t* helper = get_helper(); - const char* p = get_host_path(helper->buf, dentry); - if (p != NULL) { - bpf_probe_read_str(event->host_file, PATH_MAX, p); + if (dentry != NULL) { + struct helper_t* helper = get_helper(); + const char* p = get_host_path(helper->buf, dentry); + if (p != NULL) { + bpf_probe_read_str(event->host_file, PATH_MAX, p); + } } int64_t err = process_fill(&event->process); diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 8a05e389..ef7175b1 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -91,3 +91,10 @@ int BPF_PROG(trace_path_unlink, struct path* dir, struct dentry* dentry) { m->path_unlink.error++; return 0; } + +SEC("lsm/bprm_check_security") +int BPF_PROG(trace_bprm_check, struct linux_binprm* bprm) { + struct metrics_t* m = get_metrics(); + submit_event(&m->bprm_check, PROCESS_EXEC, NULL, NULL); + return 0; +} diff --git a/fact-ebpf/src/bpf/process.h b/fact-ebpf/src/bpf/process.h index 0032d3b2..01ba98e2 100644 --- a/fact-ebpf/src/bpf/process.h +++ b/fact-ebpf/src/bpf/process.h @@ -112,6 +112,7 @@ __always_inline static int64_t process_fill(process_t* p) { p->gid = (uid_gid >> 32) & 0xFFFFFFFF; p->login_uid = BPF_CORE_READ(task, loginuid.val); p->pid = (bpf_get_current_pid_tgid() >> 32) & 0xFFFFFFFF; + p->start_time = BPF_CORE_READ(task, start_boottime); u_int64_t err = bpf_get_current_comm(p->comm, TASK_COMM_LEN); if (err != 0) { bpf_printk("Failed to fill task comm"); diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index f32ade10..3b838693 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -30,6 +30,7 @@ typedef struct process_t { lineage_t lineage[LINEAGE_MAX]; unsigned int lineage_len; char in_root_mount_ns; + unsigned long start_time; } process_t; typedef enum file_activity_type_t { @@ -37,6 +38,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_OPEN = 0, FILE_ACTIVITY_CREATION, FILE_ACTIVITY_UNLINK, + PROCESS_EXEC, } file_activity_type_t; struct event_t { @@ -73,4 +75,5 @@ struct metrics_by_hook_t { struct metrics_t { struct metrics_by_hook_t file_open; struct metrics_by_hook_t path_unlink; + struct metrics_by_hook_t bprm_check; }; diff --git a/fact/src/bpf.rs b/fact/src/bpf.rs index b1b24e44..b4603949 100644 --- a/fact/src/bpf.rs +++ b/fact/src/bpf.rs @@ -139,7 +139,8 @@ impl Bpf { fn load_progs(&mut self) -> anyhow::Result<()> { let btf = Btf::from_sys_fs()?; self.load_lsm_prog("trace_file_open", "file_open", &btf)?; - self.load_lsm_prog("trace_path_unlink", "path_unlink", &btf) + self.load_lsm_prog("trace_path_unlink", "path_unlink", &btf)?; + self.load_lsm_prog("trace_bprm_check", "bprm_check_security", &btf) } fn attach_progs(&mut self) -> anyhow::Result<()> { @@ -174,7 +175,9 @@ impl Bpf { while let Some(event) = ringbuf.next() { let event: &event_t = unsafe { &*(event.as_ptr() as *const _) }; let event = match Event::try_from(event) { - Ok(event) => Arc::new(event), + Ok(event) if event.is_from_container() => Arc::new(event), + Ok(event) if event.is_file_event() => Arc::new(event), + Ok(_) => continue, Err(e) => { error!("Failed to parse event: '{e}'"); debug!("Event: {event:?}"); diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 745b426d..70cfc0de 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -25,8 +25,7 @@ fn timestamp_to_proto(ts: u64) -> prost_types::Timestamp { pub struct Event { timestamp: u64, hostname: &'static str, - process: Process, - file: FileData, + data: EventData, } impl Event { @@ -42,6 +41,42 @@ impl Event { .duration_since(UNIX_EPOCH) .unwrap() .as_nanos() as _; + let data = match event_type { + file_activity_type_t::FILE_ACTIVITY_OPEN + | file_activity_type_t::FILE_ACTIVITY_CREATION + | file_activity_type_t::FILE_ACTIVITY_UNLINK => { + Event::new_file_event(event_type, filename, host_file, process) + } + file_activity_type_t::PROCESS_EXEC => EventData::ProcessData(process), + invalid => unreachable!("Invalid event type: {invalid:?}"), + }; + + Ok(Event { + timestamp, + hostname, + data, + }) + } + + pub fn is_file_event(&self) -> bool { + matches!(self.data, EventData::FileData { .. }) + } + + pub fn is_from_container(&self) -> bool { + let p = match &self.data { + EventData::FileData { process, .. } => process, + EventData::ProcessData(process) => process, + }; + p.is_from_container() + } + + #[cfg(test)] + fn new_file_event( + event_type: file_activity_type_t, + filename: PathBuf, + host_file: PathBuf, + process: Process, + ) -> EventData { let inner = BaseFileData { filename, host_file, @@ -52,13 +87,7 @@ impl Event { file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), invalid => unreachable!("Invalid event type: {invalid:?}"), }; - - Ok(Event { - timestamp, - hostname, - process, - file, - }) + EventData::FileData { process, file } } } @@ -68,35 +97,97 @@ impl TryFrom<&event_t> for Event { fn try_from(value: &event_t) -> Result { let process = Process::try_from(value.process)?; let timestamp = host_info::get_boot_time() + value.timestamp; - let file = FileData::new(value.type_, value.filename, value.host_file)?; + + let data = match value.type_ { + file_activity_type_t::FILE_ACTIVITY_OPEN + | file_activity_type_t::FILE_ACTIVITY_CREATION + | file_activity_type_t::FILE_ACTIVITY_UNLINK => { + let file = FileData::new(value.type_, value.filename, value.host_file)?; + EventData::FileData { process, file } + } + file_activity_type_t::PROCESS_EXEC => EventData::ProcessData(process), + invalid => unreachable!("Invalid event type: {invalid:?}"), + }; Ok(Event { timestamp, hostname: host_info::get_hostname(), - process, - file, + data, }) } } -impl From for fact_api::FileActivity { - fn from(value: Event) -> Self { - let file = fact_api::file_activity::File::from(value.file); +impl TryFrom for fact_api::sensor::FileActivity { + type Error = anyhow::Error; + + fn try_from(value: Event) -> Result { + let (process, file) = match value.data { + EventData::FileData { process, file } => (process, file), + EventData::ProcessData(_) => anyhow::bail!("Unexpected process event on file pipeline"), + }; + let file = fact_api::sensor::file_activity::File::from(file); let timestamp = timestamp_to_proto(value.timestamp); - let process = fact_api::ProcessSignal::from(value.process); + let process = fact_api::sensor::ProcessSignal::from(process); - Self { + Ok(Self { file: Some(file), timestamp: Some(timestamp), process: Some(process), - } + }) + } +} + +impl TryFrom for fact_api::sensor::SignalStreamMessage { + type Error = anyhow::Error; + + fn try_from(value: Event) -> Result { + let process = match value.data { + EventData::FileData { .. } => { + anyhow::bail!("Unexpected file event on process pipeline") + } + EventData::ProcessData(p) => p, + }; + let signal = fact_api::storage::ProcessSignal::from(process); + let signal = fact_api::v1::signal::Signal::ProcessSignal(signal); + let signal = fact_api::v1::Signal { + signal: Some(signal), + }; + let msg = fact_api::sensor::signal_stream_message::Msg::Signal(signal); + + Ok(Self { msg: Some(msg) }) } } #[cfg(test)] impl PartialEq for Event { fn eq(&self, other: &Self) -> bool { - self.hostname == other.hostname && self.process == other.process && self.file == other.file + self.hostname == other.hostname && self.data == other.data + } +} + +#[derive(Debug, Clone, Serialize)] +pub enum EventData { + FileData { process: Process, file: FileData }, + ProcessData(Process), +} + +#[cfg(test)] +impl PartialEq for EventData { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + EventData::FileData { + process: s_proc, + file: s_file, + }, + EventData::FileData { + process: o_proc, + file: o_file, + }, + ) => s_proc == o_proc && s_file == o_file, + (EventData::ProcessData(s_proc), EventData::ProcessData(o_proc)) => s_proc == o_proc, + _ => false, + } } } @@ -125,23 +216,23 @@ impl FileData { } } -impl From for fact_api::file_activity::File { +impl From for fact_api::sensor::file_activity::File { fn from(event: FileData) -> Self { match event { FileData::Open(event) => { - let activity = Some(fact_api::FileActivityBase::from(event)); - let f_act = fact_api::FileOpen { activity }; - fact_api::file_activity::File::Open(f_act) + let activity = Some(fact_api::sensor::FileActivityBase::from(event)); + let f_act = fact_api::sensor::FileOpen { activity }; + fact_api::sensor::file_activity::File::Open(f_act) } FileData::Creation(event) => { - let activity = Some(fact_api::FileActivityBase::from(event)); - let f_act = fact_api::FileCreation { activity }; - fact_api::file_activity::File::Creation(f_act) + let activity = Some(fact_api::sensor::FileActivityBase::from(event)); + let f_act = fact_api::sensor::FileCreation { activity }; + fact_api::sensor::file_activity::File::Creation(f_act) } FileData::Unlink(event) => { - let activity = Some(fact_api::FileActivityBase::from(event)); - let f_act = fact_api::FileUnlink { activity }; - fact_api::file_activity::File::Unlink(f_act) + let activity = Some(fact_api::sensor::FileActivityBase::from(event)); + let f_act = fact_api::sensor::FileUnlink { activity }; + fact_api::sensor::file_activity::File::Unlink(f_act) } } } @@ -187,9 +278,9 @@ impl PartialEq for BaseFileData { } } -impl From for fact_api::FileActivityBase { +impl From for fact_api::sensor::FileActivityBase { fn from(value: BaseFileData) -> Self { - fact_api::FileActivityBase { + fact_api::sensor::FileActivityBase { path: value.filename.to_string_lossy().to_string(), host_path: value.host_file.to_string_lossy().to_string(), } diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 67e1056d..b01b336d 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::host_info; -use super::slice_to_string; +use super::{slice_to_string, timestamp_to_proto}; #[derive(Debug, Clone, Default, Serialize)] pub struct Lineage { @@ -34,7 +34,17 @@ impl TryFrom<&lineage_t> for Lineage { } } -impl From for fact_api::process_signal::LineageInfo { +impl From for fact_api::sensor::process_signal::LineageInfo { + fn from(value: Lineage) -> Self { + let Lineage { uid, exe_path } = value; + Self { + parent_uid: uid, + parent_exec_file_path: exe_path, + } + } +} + +impl From for fact_api::storage::process_signal::LineageInfo { fn from(value: Lineage) -> Self { let Lineage { uid, exe_path } = value; Self { @@ -57,9 +67,14 @@ pub struct Process { pid: u32, in_root_mount_ns: bool, lineage: Vec, + start_time: Option, } impl Process { + pub fn is_from_container(&self) -> bool { + self.container_id.is_some() + } + /// Create a representation of the current process as best as /// possible. #[cfg(test)] @@ -96,6 +111,7 @@ impl Process { pid, in_root_mount_ns, lineage: vec![], + start_time: None, } } @@ -146,6 +162,11 @@ impl TryFrom for Process { let memory_cgroup = unsafe { CStr::from_ptr(value.memory_cgroup.as_ptr()) }.to_str()?; let container_id = Process::extract_container_id(memory_cgroup); let in_root_mount_ns = value.in_root_mount_ns != 0; + let start_time = if value.start_time != 0 { + Some(host_info::get_boot_time() + value.start_time) + } else { + None + }; let lineage = value.lineage[..value.lineage_len as usize] .iter() @@ -180,11 +201,12 @@ impl TryFrom for Process { pid: value.pid, in_root_mount_ns, lineage, + start_time, }) } } -impl From for fact_api::ProcessSignal { +impl From for fact_api::sensor::ProcessSignal { fn from(value: Process) -> Self { let Process { comm, @@ -198,6 +220,7 @@ impl From for fact_api::ProcessSignal { pid, in_root_mount_ns, lineage, + start_time: _, } = value; let container_id = container_id.unwrap_or("".to_string()); @@ -220,7 +243,7 @@ impl From for fact_api::ProcessSignal { scraped: false, lineage_info: lineage .into_iter() - .map(fact_api::process_signal::LineageInfo::from) + .map(fact_api::sensor::process_signal::LineageInfo::from) .collect(), login_uid, username: username.to_owned(), @@ -228,3 +251,48 @@ impl From for fact_api::ProcessSignal { } } } + +impl From for fact_api::storage::ProcessSignal { + fn from(value: Process) -> Self { + let Process { + comm, + args, + exe_path, + container_id, + uid, + username: _, + gid, + login_uid: _, + pid, + in_root_mount_ns: _, + lineage, + start_time, + } = value; + + let container_id = container_id.unwrap_or("".to_string()); + + let args = args + .into_iter() + .reduce(|acc, i| acc + " " + &i) + .unwrap_or("".to_owned()); + + #[allow(deprecated)] + Self { + id: Uuid::new_v4().to_string(), + container_id, + time: start_time.map(timestamp_to_proto), + name: comm, + args, + exec_file_path: exe_path, + pid, + uid, + gid, + scraped: false, + lineage: Vec::new(), + lineage_info: lineage + .into_iter() + .map(fact_api::storage::process_signal::LineageInfo::from) + .collect(), + } + } +} diff --git a/fact/src/output/grpc.rs b/fact/src/output/grpc.rs index 0cebb735..41fe0e18 100644 --- a/fact/src/output/grpc.rs +++ b/fact/src/output/grpc.rs @@ -1,7 +1,10 @@ use std::{fs::read_to_string, path::Path, sync::Arc, time::Duration}; use anyhow::bail; -use fact_api::file_activity_service_client::FileActivityServiceClient; +use fact_api::sensor::{ + file_activity_service_client::FileActivityServiceClient, + signal_service_client::SignalServiceClient, +}; use log::{debug, info, warn}; use tokio::{ sync::{broadcast, watch}, @@ -127,16 +130,40 @@ impl Client { }; info!("Successfully connected to gRPC server"); - let mut client = - FileActivityServiceClient::with_interceptor(channel, UserAgentInterceptor {}); + let mut sfa_client = FileActivityServiceClient::with_interceptor( + channel.clone(), + UserAgentInterceptor {}, + ); + let mut proc_client = + SignalServiceClient::with_interceptor(channel, UserAgentInterceptor {}); let metrics = self.metrics.clone(); - let rx = + let sfa_rx = + BroadcastStream::new(self.rx.resubscribe()).filter_map(move |event| match event { + Ok(event) => { + metrics.added(); + let event = Arc::unwrap_or_clone(event); + match event.try_into() { + Ok(event) => Some(event), + Err(_) => None, + } + } + Err(BroadcastStreamRecvError::Lagged(n)) => { + warn!("gRPC stream lagged, dropped {n} events"); + metrics.dropped_n(n); + None + } + }); + let metrics = self.metrics.clone(); + let proc_rx = BroadcastStream::new(self.rx.resubscribe()).filter_map(move |event| match event { Ok(event) => { metrics.added(); let event = Arc::unwrap_or_clone(event); - Some(event.into()) + match event.try_into() { + Ok(event) => Some(event), + Err(_) => None, + } } Err(BroadcastStreamRecvError::Lagged(n)) => { warn!("gRPC stream lagged, dropped {n} events"); @@ -146,7 +173,13 @@ impl Client { }); tokio::select! { - res = client.communicate(rx) => { + res = sfa_client.communicate(sfa_rx) => { + match res { + Ok(_) => info!("gRPC stream ended"), + Err(e) => warn!("gRPC stream error: {e}"), + } + } + res = proc_client.push_signals(proc_rx) => { match res { Ok(_) => info!("gRPC stream ended"), Err(e) => warn!("gRPC stream error: {e}"),