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..0d19afe0 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -16,10 +16,12 @@ __always_inline static void submit_event(struct metrics_by_hook_t* m, file_activ event->timestamp = bpf_ktime_get_boot_ns(); 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..ef4bed07 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -91,3 +91,14 @@ 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(); + + m->bprm_check.total++; + + submit_event(&m->bprm_check, PROCESS_ACTIVITY_EXEC, "", NULL); + + return 0; +} diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index f32ade10..3357788e 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -37,6 +37,7 @@ typedef enum file_activity_type_t { FILE_ACTIVITY_OPEN = 0, FILE_ACTIVITY_CREATION, FILE_ACTIVITY_UNLINK, + PROCESS_ACTIVITY_EXEC, } file_activity_type_t; struct event_t { @@ -73,4 +74,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..30342bd1 100644 --- a/fact/src/bpf.rs +++ b/fact/src/bpf.rs @@ -14,7 +14,11 @@ use tokio::{ task::JoinHandle, }; -use crate::{event::Event, host_info, metrics::EventCounter}; +use crate::{ + event::{Activity, Event}, + host_info, + metrics::EventCounter, +}; use fact_ebpf::{event_t, metrics_t, path_prefix_t, LPM_SIZE_MAX}; @@ -139,7 +143,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 +179,7 @@ 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) => event, Err(e) => { error!("Failed to parse event: '{e}'"); debug!("Event: {event:?}"); @@ -183,6 +188,14 @@ impl Bpf { } }; + if matches!(event.activity, Activity::Process(_)) + && event.process.container_id.is_none() + { + event_counter.dropped(); + continue; + } + + let event = Arc::new(event); event_counter.added(); if self.tx.send(event).is_err() { info!("No BPF consumers left, stopping..."); diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index 745b426d..1ceb2a1d 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -21,12 +21,54 @@ fn timestamp_to_proto(ts: u64) -> prost_types::Timestamp { prost_types::Timestamp { seconds, nanos } } +#[derive(Debug, Clone, Serialize, PartialEq)] +pub enum ProcessActivity { + Exec, +} + +#[derive(Debug, Clone, Serialize)] +pub enum Activity { + File(FileData), + Process(ProcessActivity), +} + +#[cfg(test)] +impl PartialEq for Activity { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::File(l), Self::File(r)) => l == r, + (Self::Process(l), Self::Process(r)) => l == r, + _ => false, + } + } +} + +impl Activity { + pub fn new( + event_type: file_activity_type_t, + filename: [c_char; PATH_MAX as usize], + host_file: [c_char; PATH_MAX as usize], + ) -> anyhow::Result { + let activity = match event_type { + file_activity_type_t::FILE_ACTIVITY_OPEN + | file_activity_type_t::FILE_ACTIVITY_CREATION + | file_activity_type_t::FILE_ACTIVITY_UNLINK => { + Activity::File(FileData::new(event_type, filename, host_file)?) + } + file_activity_type_t::PROCESS_ACTIVITY_EXEC => Activity::Process(ProcessActivity::Exec), + invalid => unreachable!("Invalid event type: {invalid:?}"), + }; + + Ok(activity) + } +} + #[derive(Debug, Clone, Serialize)] pub struct Event { timestamp: u64, hostname: &'static str, - process: Process, - file: FileData, + pub process: Process, + pub activity: Activity, } impl Event { @@ -46,10 +88,13 @@ impl Event { filename, host_file, }; - let file = match event_type { - file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), - file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner), - file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), + let activity = match event_type { + file_activity_type_t::FILE_ACTIVITY_OPEN => Activity::File(FileData::Open(inner)), + file_activity_type_t::FILE_ACTIVITY_CREATION => { + Activity::File(FileData::Creation(inner)) + } + file_activity_type_t::FILE_ACTIVITY_UNLINK => Activity::File(FileData::Unlink(inner)), + file_activity_type_t::PROCESS_ACTIVITY_EXEC => Activity::Process(ProcessActivity::Exec), invalid => unreachable!("Invalid event type: {invalid:?}"), }; @@ -57,7 +102,7 @@ impl Event { timestamp, hostname, process, - file, + activity, }) } } @@ -68,25 +113,60 @@ 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 activity = Activity::new(value.type_, value.filename, value.host_file)?; Ok(Event { timestamp, hostname: host_info::get_hostname(), process, - file, + activity, }) } } -impl From for fact_api::FileActivity { +impl From for fact_api::v1::Signal { + fn from(value: Event) -> Self { + let process_signal = fact_api::storage::ProcessSignal::from(value.process); + fact_api::v1::Signal { + signal: Some(fact_api::v1::signal::Signal::ProcessSignal(process_signal)), + } + } +} + +impl From for fact_api::sensor::FileActivity { fn from(value: Event) -> Self { - let file = fact_api::file_activity::File::from(value.file); + let file = match value.activity { + Activity::File(file) => Some(fact_api::sensor::file_activity::File::from(file)), + Activity::Process(_) => None, + }; let timestamp = timestamp_to_proto(value.timestamp); - let process = fact_api::ProcessSignal::from(value.process); + let process_signal = fact_api::storage::ProcessSignal::from(value.process.clone()); + let process = fact_api::sensor::ProcessSignal { + id: process_signal.id, + container_id: process_signal.container_id, + creation_time: process_signal.time, + name: process_signal.name, + args: process_signal.args, + exec_file_path: process_signal.exec_file_path, + pid: process_signal.pid, + gid: process_signal.gid, + uid: process_signal.uid, + username: value.process.username.to_string(), + login_uid: value.process.login_uid, + in_root_mount_ns: value.process.in_root_mount_ns, + scraped: process_signal.scraped, + lineage_info: process_signal + .lineage_info + .into_iter() + .map(|l| fact_api::sensor::process_signal::LineageInfo { + parent_uid: l.parent_uid, + parent_exec_file_path: l.parent_exec_file_path, + }) + .collect(), + }; Self { - file: Some(file), + file, timestamp: Some(timestamp), process: Some(process), } @@ -96,7 +176,9 @@ impl From for fact_api::FileActivity { #[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.process == other.process + && self.activity == other.activity } } @@ -118,30 +200,30 @@ impl FileData { file_activity_type_t::FILE_ACTIVITY_OPEN => FileData::Open(inner), file_activity_type_t::FILE_ACTIVITY_CREATION => FileData::Creation(inner), file_activity_type_t::FILE_ACTIVITY_UNLINK => FileData::Unlink(inner), - invalid => unreachable!("Invalid event type: {invalid:?}"), + invalid => unreachable!("Invalid file event type: {invalid:?}"), }; Ok(file) } } -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 +269,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..6c153d18 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -34,7 +34,7 @@ impl TryFrom<&lineage_t> for Lineage { } } -impl From for fact_api::process_signal::LineageInfo { +impl From for fact_api::storage::process_signal::LineageInfo { fn from(value: Lineage) -> Self { let Lineage { uid, exe_path } = value; Self { @@ -49,13 +49,13 @@ pub struct Process { comm: String, args: Vec, exe_path: String, - container_id: Option, + pub container_id: Option, uid: u32, - username: &'static str, + pub username: &'static str, gid: u32, - login_uid: u32, + pub login_uid: u32, pid: u32, - in_root_mount_ns: bool, + pub in_root_mount_ns: bool, lineage: Vec, } @@ -136,7 +136,6 @@ impl PartialEq for Process { && self.in_root_mount_ns == other.in_root_mount_ns } } - impl TryFrom for Process { type Error = anyhow::Error; @@ -184,7 +183,7 @@ impl TryFrom for Process { } } -impl From for fact_api::ProcessSignal { +impl From for fact_api::storage::ProcessSignal { fn from(value: Process) -> Self { let Process { comm, @@ -192,11 +191,11 @@ impl From for fact_api::ProcessSignal { exe_path, container_id, uid, - username, + username: _, gid, - login_uid, + login_uid: _, pid, - in_root_mount_ns, + in_root_mount_ns: _, lineage, } = value; @@ -207,10 +206,11 @@ impl From for fact_api::ProcessSignal { .reduce(|acc, i| acc + " " + &i) .unwrap_or("".to_owned()); + #[allow(deprecated)] Self { id: Uuid::new_v4().to_string(), container_id, - creation_time: None, + time: None, name: comm, args, exec_file_path: exe_path, @@ -218,13 +218,11 @@ impl From for fact_api::ProcessSignal { uid, gid, scraped: false, + lineage: vec![], lineage_info: lineage .into_iter() - .map(fact_api::process_signal::LineageInfo::from) + .map(fact_api::storage::process_signal::LineageInfo::from) .collect(), - login_uid, - username: username.to_owned(), - in_root_mount_ns, } } } diff --git a/fact/src/output/grpc.rs b/fact/src/output/grpc.rs index 0cebb735..3d68e0ce 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, SignalStreamMessage, +}; use log::{debug, info, warn}; use tokio::{ sync::{broadcast, watch}, @@ -52,6 +55,15 @@ impl Interceptor for UserAgentInterceptor { } } +impl From for SignalStreamMessage { + fn from(value: Event) -> Self { + let signal = fact_api::v1::Signal::from(value); + SignalStreamMessage { + msg: Some(fact_api::sensor::signal_stream_message::Msg::Signal(signal)), + } + } +} + pub struct Client { rx: broadcast::Receiver>, running: watch::Receiver, @@ -127,29 +139,59 @@ 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 signal_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) => { + if !matches!(event.activity, crate::event::Activity::File(_)) { + return None; + } + metrics.added(); + let event = Arc::unwrap_or_clone(event); + Some(event.into()) + } + Err(BroadcastStreamRecvError::Lagged(n)) => { + warn!("gRPC sfa stream lagged, dropped {n} events"); + metrics.dropped_n(n); + None + } + }); + let metrics = self.metrics.clone(); + let signal_rx = BroadcastStream::new(self.rx.resubscribe()).filter_map(move |event| match event { Ok(event) => { + if !matches!(event.activity, crate::event::Activity::Process(_)) { + return None; + } metrics.added(); let event = Arc::unwrap_or_clone(event); Some(event.into()) } Err(BroadcastStreamRecvError::Lagged(n)) => { - warn!("gRPC stream lagged, dropped {n} events"); + warn!("gRPC signal stream lagged, dropped {n} events"); metrics.dropped_n(n); None } }); tokio::select! { - res = client.communicate(rx) => { + res = sfa_client.communicate(sfa_rx) => { + match res { + Ok(_) => info!("gRPC sfa stream ended"), + Err(e) => warn!("gRPC sfa stream error: {e}"), + } + } + res = signal_client.push_signals(signal_rx) => { match res { - Ok(_) => info!("gRPC stream ended"), - Err(e) => warn!("gRPC stream error: {e}"), + Ok(_) => info!("gRPC signal stream ended"), + Err(e) => warn!("gRPC signal stream error: {e}"), } } _ = self.config.changed() => return Ok(true),