Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(filtering): whitelist current executable #129

Merged
merged 3 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bpf-common/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ pub enum ProgramError {
ProgramLoadError {
program: String,
#[source]
program_error: aya::programs::ProgramError,
program_error: Box<aya::programs::ProgramError>,
},
#[error("failed program attach {program}")]
ProgramAttachError {
program: String,
#[source]
program_error: aya::programs::ProgramError,
program_error: Box<aya::programs::ProgramError>,
},
#[error(transparent)]
MapError(#[from] aya::maps::MapError),
Expand Down Expand Up @@ -231,11 +231,11 @@ impl ProgramType {
fn attach(&self, bpf: &mut Bpf, btf: &Btf) -> Result<(), ProgramError> {
let load_err = |program_error| ProgramError::ProgramLoadError {
program: self.to_string(),
program_error,
program_error: Box::new(program_error),
};
let attach_err = |program_error| ProgramError::ProgramAttachError {
program: self.to_string(),
program_error,
program_error: Box::new(program_error),
};
match self {
ProgramType::TracePoint(section, tracepoint) => {
Expand Down
2 changes: 2 additions & 0 deletions modules/process-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ all other modules.
|`targets_children`|image list|List of processes to track (extended to children)|
|`whitelist`|image list|List of processes to ignore|
|`whitelist_children`|image list|List of processes to ignore (extended to children)|
|`ignore_self`|bool|Add the Pulsar executable to whitelist_children|

Default configuration:

Expand All @@ -34,6 +35,7 @@ targets=
targets_children=
whitelist=
whitelist_children=
ignore_self=true
```

For example, to limit Pulsar analisys to SSH connections with:
Expand Down
2 changes: 2 additions & 0 deletions modules/process-monitor/src/filtering/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(crate) struct Config {
pub(crate) pid_targets: Vec<PidRule>,
pub(crate) targets: Vec<Rule>,
pub(crate) whitelist: Vec<Rule>,
pub(crate) ignore_self: bool,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -60,6 +61,7 @@ impl TryFrom<&ModuleConfig> for Config {
pid_targets,
targets,
whitelist,
ignore_self: config.with_default("ignore_self", true)?,
})
}
}
Expand Down
40 changes: 24 additions & 16 deletions modules/process-monitor/src/filtering/initializer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::time::Duration;
use std::{os::unix::prelude::OsStringExt, time::Duration};

use anyhow::Result;
use anyhow::{Context, Result};
use bpf_common::{aya::Bpf, Pid};
use pulsar_core::{
pdk::process_tracker::{ProcessTrackerHandle, TrackerUpdate},
Expand All @@ -9,9 +9,9 @@ use pulsar_core::{
use tokio::sync::mpsc;

use super::{
config::Config,
config::{Config, Rule},
maps::InterestMap,
maps::{PolicyDecision, RuleMap},
maps::{Image, PolicyDecision, RuleMap},
process_tree::{ProcessData, ProcessTree, PID_0},
};

Expand All @@ -31,10 +31,18 @@ const INIT_TIMEOUT: Duration = Duration::from_millis(100);
/// because of unitialized entries.
pub(crate) async fn setup_events_filter(
bpf: &mut Bpf,
config: Config,
mut config: Config,
process_tracker: &ProcessTrackerHandle,
rx_processes: &mut mpsc::UnboundedReceiver<TrackerUpdate>,
) -> Result<()> {
// Add a rule to ignore the pulsar executable itself
if config.ignore_self {
match whitelist_for_current_process() {
Ok(rule) => config.whitelist.push(rule),
Err(err) => log::error!("Failed to add current process to whitelist: {:?}", err),
}
}

// setup targets map
let mut target_map = RuleMap::target(bpf)?;
target_map.clear()?;
Expand Down Expand Up @@ -95,23 +103,19 @@ struct Initializer {
interest_map: InterestMap,
cache: std::collections::HashMap<Pid, PolicyDecision>,
config: Config,
my_pid: Pid,
}

impl Initializer {
fn new(bpf: &mut Bpf, config: Config) -> Result<Self> {
// clear whitelist map
let mut interest_map = InterestMap::load(bpf)?;
interest_map.clear()?;

let cache = Default::default();
let my_pid = Pid::from_raw(std::process::id() as i32);

Ok(Self {
interest_map,
cache,
config,
my_pid,
})
}

Expand Down Expand Up @@ -163,13 +167,6 @@ impl Initializer {
decision.children_interesting = true;
}
};
// make sure to ignore pulsard
if process.pid == self.my_pid {
decision = PolicyDecision {
interesting: false,
children_interesting: false,
};
}
if decision.interesting {
log::debug!("tracking {} {}", process.pid, process.image);
}
Expand All @@ -178,3 +175,14 @@ impl Initializer {
Ok(())
}
}

/// Return a rule which whitelists the current executable.
/// This is needed to avoid loops where pulsar events generate further events.
fn whitelist_for_current_process() -> Result<Rule> {
let pulsar_exec = std::fs::read_link("/proc/self/exe")
.context("Failed to read current process executable name")?;
Ok(Rule {
image: Image::try_from(pulsar_exec.into_os_string().into_vec())?,
with_children: true,
})
}
37 changes: 27 additions & 10 deletions modules/process-monitor/src/filtering/maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,39 @@ pub(crate) struct Image(pub(crate) [u8; MAX_IMAGE_LEN]);
// We must explicitly mark Image as a plain old data which can be safely memcopied by aya.
unsafe impl bpf_common::aya::Pod for Image {}

#[derive(thiserror::Error, Debug)]
pub(crate) enum InvalidImage {
#[error("process image coming from string must be ascii")]
NotAscii,
#[error("process image must be smaller than {MAX_IMAGE_LEN}")]
TooLong,
}

impl TryFrom<Vec<u8>> for Image {
type Error = InvalidImage;

fn try_from(mut data: Vec<u8>) -> Result<Self, Self::Error> {
if data.len() > MAX_IMAGE_LEN {
return Err(InvalidImage::TooLong);
}
data.resize(MAX_IMAGE_LEN, 0);
let mut image_array = [0; MAX_IMAGE_LEN];
image_array.clone_from_slice(&data[..]);
Ok(Image(image_array))
}
}

impl FromStr for Image {
type Err = String;
type Err = InvalidImage;

fn from_str(image: &str) -> Result<Self, Self::Err> {
if !image.is_ascii() {
Err("process image must be ascii".to_string())
Err(InvalidImage::NotAscii)
} else if image.len() >= MAX_IMAGE_LEN {
Err(format!(
"process image must be smaller than {MAX_IMAGE_LEN}"
))
Err(InvalidImage::TooLong)
} else {
let mut image_vec: Vec<u8> = image.bytes().collect();
image_vec.resize(MAX_IMAGE_LEN, 0);
let mut image_array = [0; MAX_IMAGE_LEN];
image_array.clone_from_slice(&image_vec[..]);
Ok(Image(image_array))
let data: Vec<u8> = image.bytes().collect();
Image::try_from(data)
}
}
}
Expand Down