use anyhow::Result;
use chrono::Utc;
use serde::Serialize;
use std::fs::{create_dir_all, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
#[derive(Debug, Serialize)]
pub struct Entry<'a> {
pub ts: String,
pub akm_version: &'a str,
pub command: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
pub keys: Vec<String>,
pub input_mode: Option<&'a str>,
pub child_command: Option<String>,
pub injected_keys: Option<Vec<String>>,
pub push_target: Option<&'a str>,
pub cwd: Option<String>,
pub ppid: i32,
pub parent_exe: Option<String>,
pub status: &'a str,
}
pub fn log_path() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join(".akm").join("audit.log")
} else {
PathBuf::from("/tmp/akm-audit.log")
}
}
pub fn append(entry: &Entry) -> Result<()> {
let path = log_path();
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(parent) {
let mut perms = meta.permissions();
if perms.mode() & 0o077 != 0 {
perms.set_mode(0o700);
let _ = std::fs::set_permissions(parent, perms);
}
}
}
}
let mut opts = OpenOptions::new();
opts.create(true).append(true).mode(0o600);
let mut f = opts.open(&path)?;
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = f.metadata() {
let mut perms = meta.permissions();
if perms.mode() & 0o077 != 0 {
perms.set_mode(0o600);
let _ = std::fs::set_permissions(&path, perms);
}
}
}
let mut line = serde_json::to_vec(entry)?;
line.push(b'\n');
f.write_all(&line)?;
Ok(())
}
pub fn now() -> String {
Utc::now().to_rfc3339()
}
pub fn last_set_map() -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let Ok(file) = File::open(log_path()) else {
return map;
};
for line in BufReader::new(file).lines() {
let Ok(line) = line else {
break;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&line) else {
continue;
};
let cmd = v.get("command").and_then(|c| c.as_str()).unwrap_or("");
if cmd != "add" && cmd != "import" {
continue;
}
if v.get("status").and_then(|s| s.as_str()) != Some("ok") {
continue;
}
let Some(ts) = v.get("ts").and_then(|t| t.as_str()) else {
continue;
};
if let Some(keys) = v.get("keys").and_then(|k| k.as_array()) {
for k in keys.iter().filter_map(|k| k.as_str()) {
map.insert(k.to_string(), ts.to_string());
}
}
}
map
}
pub fn ppid() -> i32 {
unsafe { libc::getppid() }
}
pub fn parent_exe(ppid: i32) -> Option<String> {
let mut buf = vec![0u8; 4096];
let n = unsafe {
extern "C" {
fn proc_pidpath(pid: libc::c_int, buf: *mut libc::c_void, bufsize: u32) -> i32;
}
proc_pidpath(ppid, buf.as_mut_ptr() as *mut _, buf.len() as u32)
};
if n <= 0 {
return None;
}
buf.truncate(n as usize);
String::from_utf8(buf).ok()
}
pub fn cwd_string() -> Option<String> {
std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.to_string()))
}
pub const AKM_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn new_run_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let pid = std::process::id();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{pid}-{nanos}")
}
pub fn entry_base(command: &'static str, status: &'static str) -> Entry<'static> {
Entry {
ts: now(),
akm_version: AKM_VERSION,
command,
run_id: None,
keys: Vec::new(),
input_mode: None,
child_command: None,
injected_keys: None,
push_target: None,
cwd: cwd_string(),
ppid: ppid(),
parent_exe: parent_exe(ppid()),
status,
}
}