#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::collectors::command::{run_output, CommandTimeout};
use crate::collectors::CollectMode;
use crate::error::Result;
use std::env;
#[derive(Debug, Clone)]
pub struct SessionInfo {
pub username: String,
pub home_dir: String,
pub shell: String,
pub current_dir: String,
pub terminal: String,
pub last_login: Option<String>,
pub last_login_ip: Option<String>,
}
pub fn collect(mode: CollectMode) -> Result<SessionInfo> {
let username = get_username();
let home_dir = dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let shell = get_shell();
let current_dir = env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "Unknown".to_string());
let terminal = get_terminal();
let should_skip_last_login = mode == CollectMode::Fast && should_skip_last_login_on_platform();
let (last_login, last_login_ip) = if should_skip_last_login {
(None, None)
} else {
let (login, ip) = get_last_login(&username);
(Some(login), ip)
};
Ok(SessionInfo {
username,
home_dir,
shell,
current_dir,
terminal,
last_login,
last_login_ip,
})
}
fn should_skip_last_login_on_platform() -> bool {
#[cfg(target_os = "windows")]
{
true
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn get_username() -> String {
#[cfg(unix)]
{
uzers::get_current_username()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "Unknown".to_string()))
}
#[cfg(windows)]
{
env::var("USERNAME").unwrap_or_else(|_| "Unknown".to_string())
}
#[cfg(not(any(unix, windows)))]
{
"Unknown".to_string()
}
}
fn get_shell() -> String {
#[cfg(unix)]
{
env::var("SHELL").unwrap_or_else(|_| "Unknown".to_string())
}
#[cfg(windows)]
{
env::var("COMSPEC").unwrap_or_else(|_| {
env::var("PSModulePath")
.map(|_| "PowerShell".to_string())
.unwrap_or_else(|_| "cmd.exe".to_string())
})
}
#[cfg(not(any(unix, windows)))]
{
"Unknown".to_string()
}
}
fn get_terminal() -> String {
if let Ok(term_program) = env::var("TERM_PROGRAM") {
return term_program;
}
if let Ok(wt_session) = env::var("WT_SESSION") {
if !wt_session.is_empty() {
return "Windows Terminal".to_string();
}
}
if let Ok(term) = env::var("TERM") {
return term;
}
#[cfg(windows)]
{
if env::var("ConEmuANSI").is_ok() {
return "ConEmu".to_string();
}
"Console".to_string()
}
#[cfg(not(windows))]
{
"Unknown".to_string()
}
}
fn get_last_login(username: &str) -> (String, Option<String>) {
#[cfg(target_os = "linux")]
{
get_last_login_linux(username)
}
#[cfg(target_os = "macos")]
{
get_last_login_macos(username)
}
#[cfg(target_os = "windows")]
{
get_last_login_windows(username)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = username;
("Login tracking unavailable".to_string(), None)
}
}
#[cfg(target_os = "linux")]
fn get_last_login_linux(username: &str) -> (String, Option<String>) {
if let Some(output) = run_output("lastlog2", ["--user", username], CommandTimeout::Normal) {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().nth(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let date_time = parts[1..4].join(" ");
let ip = parts.get(4).map(|s| s.to_string());
return (date_time, ip);
}
}
}
}
if let Some(output) = run_output("lastlog", ["-u", username], CommandTimeout::Normal) {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().nth(1) {
if line.contains("Never logged in") {
return ("Never logged in".to_string(), None);
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 {
let from = parts.get(2).map(|s| s.to_string());
let date_time = parts[3..].join(" ");
return (date_time, from);
}
}
}
}
if let Some(output) = run_output("last", ["-F", "-1", "-w", username], CommandTimeout::Normal) {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().next() {
if !line.contains("wtmp begins") && !line.is_empty() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 {
let from = parts.get(2).and_then(|s| {
if s.starts_with(':') || s.starts_with("pts") || s.starts_with("tty") {
None
} else {
Some(s.to_string())
}
});
let date_time = parts[3..7.min(parts.len())].join(" ");
return (date_time, from);
}
}
}
}
}
("Login tracking unavailable".to_string(), None)
}
#[cfg(target_os = "macos")]
fn get_last_login_macos(username: &str) -> (String, Option<String>) {
if let Some(output) = run_output("last", ["-1", username], CommandTimeout::Normal) {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().next() {
if !line.contains("wtmp begins") && !line.is_empty() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 {
let from = parts.get(2).and_then(|s| {
if s.starts_with(':')
|| s.starts_with("console")
|| s.starts_with("tty")
{
None
} else {
Some(s.to_string())
}
});
let date_time = parts[3..7.min(parts.len())].join(" ");
return (date_time, from);
}
}
}
}
}
("Login tracking unavailable".to_string(), None)
}
#[cfg(target_os = "windows")]
fn get_last_login_windows(_username: &str) -> (String, Option<String>) {
if let Some(when) = wts_query_session_connect_time() {
return (when, None);
}
if let Some(boot_time_str) = boot_time_local_string() {
return (boot_time_str, None);
}
("Login tracking unavailable".to_string(), None)
}
#[cfg(target_os = "windows")]
fn boot_time_local_string() -> Option<String> {
let uptime_ms: u64 = unsafe { winapi::um::sysinfoapi::GetTickCount64() };
let now = chrono::Local::now();
let boot = now - chrono::Duration::milliseconds(uptime_ms as i64);
Some(boot.format("%a %b %-d %H:%M").to_string())
}
#[cfg(target_os = "windows")]
const WTS_CURRENT_SESSION: u32 = 0xFFFF_FFFF;
#[cfg(target_os = "windows")]
const WTS_CONNECT_TIME: u32 = 14; #[cfg(target_os = "windows")]
const WTS_LOGON_TIME: u32 = 17;
#[cfg(target_os = "windows")]
#[link(name = "wtsapi32")]
extern "system" {
fn WTSQuerySessionInformationW(
hServer: *mut std::ffi::c_void,
SessionId: u32,
WTSInfoClass: u32,
ppBuffer: *mut *mut u16,
pBytesReturned: *mut u32,
) -> i32;
fn WTSFreeMemory(pMemory: *mut std::ffi::c_void);
}
#[cfg(target_os = "windows")]
fn wts_query_session_connect_time() -> Option<String> {
let filetime = wts_query_filetime(WTS_LOGON_TIME)
.filter(|&ft| ft > 0)
.or_else(|| wts_query_filetime(WTS_CONNECT_TIME).filter(|&ft| ft > 0))?;
filetime_to_local_string(filetime)
}
#[cfg(target_os = "windows")]
fn wts_query_filetime(info_class: u32) -> Option<i64> {
let mut buffer_ptr: *mut u16 = std::ptr::null_mut();
let mut bytes_returned: u32 = 0;
let ok = unsafe {
WTSQuerySessionInformationW(
std::ptr::null_mut(),
WTS_CURRENT_SESSION,
info_class,
&mut buffer_ptr,
&mut bytes_returned,
)
};
if ok == 0 || buffer_ptr.is_null() || (bytes_returned as usize) < std::mem::size_of::<i64>() {
if !buffer_ptr.is_null() {
unsafe { WTSFreeMemory(buffer_ptr as *mut _) };
}
return None;
}
let mut raw = [0u8; 8];
unsafe {
std::ptr::copy_nonoverlapping(buffer_ptr as *const u8, raw.as_mut_ptr(), raw.len());
}
let filetime = i64::from_le_bytes(raw);
unsafe { WTSFreeMemory(buffer_ptr as *mut _) };
Some(filetime)
}
#[cfg(target_os = "windows")]
fn filetime_to_local_string(filetime: i64) -> Option<String> {
const FILETIME_UNIX_EPOCH_DIFF_SECS: i64 = 11_644_473_600;
let unix_secs = filetime / 10_000_000 - FILETIME_UNIX_EPOCH_DIFF_SECS;
let unix_nsecs = ((filetime % 10_000_000) * 100) as u32;
let dt_utc = chrono::DateTime::<chrono::Utc>::from_timestamp(unix_secs, unix_nsecs)?;
let dt_local = dt_utc.with_timezone(&chrono::Local);
Some(dt_local.format("%a %b %-d %H:%M").to_string())
}