use crate::events::{Event, EventKind};
use crate::monitor::process::PidSet;
use anyhow::Result;
use std::collections::HashSet;
use std::ffi::OsString;
use std::time::Duration;
use sysinfo::{Pid, ProcessesToUpdate, System};
use tokio::sync::mpsc;
use tokio::time;
pub async fn run(tx: mpsc::Sender<Event>, pids: PidSet) -> Result<()> {
let mut system = System::new_all();
let mut scanned_pids: HashSet<u32> = HashSet::new();
let mut emitted_vars: HashSet<String> = HashSet::new();
loop {
if tx.is_closed() {
return Ok(());
}
let tracked_pids = {
let guard = pids.read().await;
guard.clone()
};
if tracked_pids.is_empty() {
scanned_pids.clear();
time::sleep(Duration::from_millis(500)).await;
continue;
}
system.refresh_processes(ProcessesToUpdate::All, true);
for pid in &tracked_pids {
if !scanned_pids.insert(*pid) {
continue;
}
let Some(process) = system.process(Pid::from_u32(*pid)) else {
continue;
};
let env_vars: Vec<(String, bool)> = process
.environ()
.iter()
.filter_map(parse_env_entry)
.filter(|(name, _)| is_sensitive_env_name(name))
.collect();
for (name, high_value) in env_vars {
if !emitted_vars.insert(name.clone()) {
continue;
}
let risk = if high_value { 15 } else { 8 };
let event = Event::with_risk(
EventKind::EnvVarRead {
name,
sensitive: true,
},
risk,
);
if tx.send(event).await.is_err() {
return Ok(());
}
}
}
time::sleep(Duration::from_millis(500)).await;
}
}
fn parse_env_entry(entry: &OsString) -> Option<(String, bool)> {
let text = entry.to_string_lossy();
let (name, value) = text.split_once('=')?;
if name.is_empty() {
return None;
}
let high_value = value.len() >= 16
&& !value.contains('/')
&& !value.contains('\\')
&& value != "your_key_here"
&& value != "changeme"
&& value != "secret"
&& value != "placeholder";
Some((name.to_string(), high_value))
}
fn is_sensitive_env_name(name: &str) -> bool {
let u = name.to_ascii_uppercase();
if u.ends_with("_API_KEY")
|| u.ends_with("_SECRET_KEY")
|| u.ends_with("_AUTH_TOKEN")
|| u.ends_with("_ACCESS_TOKEN")
|| u.ends_with("_PRIVATE_KEY")
|| u.ends_with("_CLIENT_SECRET")
{
return true;
}
let exact = [
"DATABASE_URL",
"DB_PASSWORD",
"DB_PASS",
"REDIS_URL",
"REDIS_PASSWORD",
"SECRET_KEY",
"JWT_SECRET",
"SESSION_SECRET",
"ENCRYPTION_KEY",
"MASTER_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_API_KEY",
"AZURE_CLIENT_SECRET",
"AZURE_STORAGE_CONNECTION_STRING",
"GITHUB_TOKEN",
"GITHUB_PAT",
"GITLAB_TOKEN",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GEMINI_API_KEY",
"HUGGINGFACE_TOKEN",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"PAYPAL_SECRET",
"SLACK_BOT_TOKEN",
"SLACK_SIGNING_SECRET",
"TWILIO_AUTH_TOKEN",
"SENDGRID_API_KEY",
"MAILGUN_API_KEY",
"NPM_TOKEN",
"PYPI_TOKEN",
"DOCKER_PASSWORD",
];
if exact.contains(&u.as_str()) {
return true;
}
let keywords = [
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"PRIVATE",
"CREDENTIAL",
];
keywords.iter().any(|k| u.contains(k))
}