//! System information builtins (hostname, uname, whoami, id)
//!
//! These builtins return configurable virtual values to prevent
//! information disclosure about the host system.
//!
//! Security rationale: Real system information could be used for:
//! - Fingerprinting the host for targeted attacks
//! - Identifying the environment for escape attempts
//! - Correlating activity across tenants
use async_trait::async_trait;
use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
/// Default virtual hostname.
/// Using a clearly fake name prevents confusion with real hosts.
pub const DEFAULT_HOSTNAME: &str = "bashkit-sandbox";
/// Default virtual username.
pub const DEFAULT_USERNAME: &str = "sandbox";
/// Hardcoded virtual user ID.
pub const SANDBOX_UID: u32 = 1000;
/// Hardcoded virtual group ID.
pub const SANDBOX_GID: u32 = 1000;
/// The hostname builtin - returns configurable virtual hostname.
///
/// Real hostname is never exposed to prevent host fingerprinting.
pub struct Hostname {
hostname: String,
}
impl Hostname {
/// Create a new Hostname builtin with default hostname.
pub fn new() -> Self {
Self {
hostname: DEFAULT_HOSTNAME.to_string(),
}
}
/// Create a new Hostname builtin with custom hostname.
pub fn with_hostname(hostname: impl Into<String>) -> Self {
Self {
hostname: hostname.into(),
}
}
}
impl Default for Hostname {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Builtin for Hostname {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: hostname\nDisplay the virtual hostname.\n\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("hostname (bashkit) 0.1"),
) {
return Ok(r);
}
// Ignore any attempts to set hostname
if !ctx.args.is_empty() {
return Ok(ExecResult::err(
"hostname: cannot set hostname in virtual mode\n",
1,
));
}
Ok(ExecResult::ok(format!("{}\n", self.hostname)))
}
}
/// The uname builtin - returns configurable system information.
///
/// Prevents disclosure of:
/// - Kernel version (could reveal vulnerabilities)
/// - Architecture (could inform exploit selection)
/// - Host machine name
pub struct Uname {
hostname: String,
}
impl Uname {
/// Create a new Uname builtin with default hostname.
pub fn new() -> Self {
Self {
hostname: DEFAULT_HOSTNAME.to_string(),
}
}
/// Create a new Uname builtin with custom hostname.
pub fn with_hostname(hostname: impl Into<String>) -> Self {
Self {
hostname: hostname.into(),
}
}
}
impl Default for Uname {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Builtin for Uname {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: uname [OPTION]...\nPrint virtual system information.\n\n -a, --all\t\t\tprint all information\n -s, --kernel-name\t\tprint the kernel name\n -n, --nodename\t\tprint the network node hostname\n -r, --kernel-release\t\tprint the kernel release\n -v, --kernel-version\t\tprint the kernel version\n -m, --machine\t\t\tprint the machine hardware name\n -o, --operating-system\tprint the operating system\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("uname (bashkit) 0.1"),
) {
return Ok(r);
}
let mut show_all = false;
let mut show_kernel = false;
let mut show_nodename = false;
let mut show_release = false;
let mut show_version = false;
let mut show_machine = false;
let mut show_os = false;
for arg in ctx.args {
match arg.as_str() {
"-a" | "--all" => show_all = true,
"-s" | "--kernel-name" => show_kernel = true,
"-n" | "--nodename" => show_nodename = true,
"-r" | "--kernel-release" => show_release = true,
"-v" | "--kernel-version" => show_version = true,
"-m" | "--machine" => show_machine = true,
"-o" | "--operating-system" => show_os = true,
_ => {}
}
}
// Default to kernel name if no options
if !show_all
&& !show_kernel
&& !show_nodename
&& !show_release
&& !show_version
&& !show_machine
&& !show_os
{
show_kernel = true;
}
let mut parts = Vec::new();
if show_all || show_kernel {
parts.push("Linux".to_string());
}
if show_all || show_nodename {
parts.push(self.hostname.clone());
}
if show_all || show_release {
parts.push("5.15.0-sandbox".to_string());
}
if show_all || show_version {
parts.push("#1 SMP PREEMPT sandbox".to_string());
}
if show_all || show_machine {
parts.push("x86_64".to_string());
}
if show_all || show_os {
parts.push("GNU/Linux".to_string());
}
Ok(ExecResult::ok(format!("{}\n", parts.join(" "))))
}
}
/// The whoami builtin - returns configurable virtual username.
pub struct Whoami {
username: String,
}
impl Whoami {
/// Create a new Whoami builtin with default username.
pub fn new() -> Self {
Self {
username: DEFAULT_USERNAME.to_string(),
}
}
/// Create a new Whoami builtin with custom username.
pub fn with_username(username: impl Into<String>) -> Self {
Self {
username: username.into(),
}
}
}
impl Default for Whoami {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Builtin for Whoami {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: whoami\nPrint the user name associated with the current effective user ID.\n\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("whoami (bashkit) 0.1"),
) {
return Ok(r);
}
Ok(ExecResult::ok(format!("{}\n", self.username)))
}
}
/// The id builtin - returns configurable virtual user/group IDs.
pub struct Id {
username: String,
}
impl Id {
/// Create a new Id builtin with default username.
pub fn new() -> Self {
Self {
username: DEFAULT_USERNAME.to_string(),
}
}
/// Create a new Id builtin with custom username.
pub fn with_username(username: impl Into<String>) -> Self {
Self {
username: username.into(),
}
}
}
impl Default for Id {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Builtin for Id {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: id [OPTION]...\nPrint virtual user and group information.\n\n -u, --user\tprint only the effective user ID\n -g, --group\tprint only the effective group ID\n -n, --name\tprint a name instead of a number (with -u or -g)\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("id (bashkit) 0.1"),
) {
return Ok(r);
}
// Check for specific flags
for arg in ctx.args {
match arg.as_str() {
"-u" | "--user" => {
return Ok(ExecResult::ok(format!("{}\n", SANDBOX_UID)));
}
"-g" | "--group" => {
return Ok(ExecResult::ok(format!("{}\n", SANDBOX_GID)));
}
"-n" | "--name" => {
// -n is usually combined with -u or -g
continue;
}
_ => {}
}
}
// Check for -un or -gn combinations
let args_str: String = ctx.args.iter().map(|s| s.as_str()).collect();
if args_str.contains('u') && args_str.contains('n') {
return Ok(ExecResult::ok(format!("{}\n", self.username)));
}
if args_str.contains('g') && args_str.contains('n') {
return Ok(ExecResult::ok(format!("{}\n", self.username)));
}
// Default output format
Ok(ExecResult::ok(format!(
"uid={}({}) gid={}({}) groups={}({})\n",
SANDBOX_UID, self.username, SANDBOX_GID, self.username, SANDBOX_GID, self.username
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::InMemoryFs;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
async fn run_builtin<B: Builtin>(builtin: &B, args: &[&str]) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
builtin.execute(ctx).await.unwrap()
}
#[tokio::test]
async fn test_hostname_returns_sandbox() {
let result = run_builtin(&Hostname::new(), &[]).await;
assert_eq!(result.stdout, "bashkit-sandbox\n");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_hostname_custom() {
let result = run_builtin(&Hostname::with_hostname("my-host"), &[]).await;
assert_eq!(result.stdout, "my-host\n");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_hostname_cannot_set() {
let result = run_builtin(&Hostname::new(), &["evil.com"]).await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("cannot set"));
}
#[tokio::test]
async fn test_uname_default() {
let result = run_builtin(&Uname::new(), &[]).await;
assert_eq!(result.stdout, "Linux\n");
}
#[tokio::test]
async fn test_uname_all() {
let result = run_builtin(&Uname::new(), &["-a"]).await;
assert!(result.stdout.contains("Linux"));
assert!(result.stdout.contains("bashkit-sandbox"));
assert!(result.stdout.contains("x86_64"));
}
#[tokio::test]
async fn test_uname_custom_hostname() {
let result = run_builtin(&Uname::with_hostname("custom-host"), &["-n"]).await;
assert_eq!(result.stdout, "custom-host\n");
}
#[tokio::test]
async fn test_uname_nodename() {
let result = run_builtin(&Uname::new(), &["-n"]).await;
assert_eq!(result.stdout, "bashkit-sandbox\n");
}
#[tokio::test]
async fn test_whoami() {
let result = run_builtin(&Whoami::new(), &[]).await;
assert_eq!(result.stdout, "sandbox\n");
}
#[tokio::test]
async fn test_whoami_custom() {
let result = run_builtin(&Whoami::with_username("alice"), &[]).await;
assert_eq!(result.stdout, "alice\n");
}
#[tokio::test]
async fn test_id_default() {
let result = run_builtin(&Id::new(), &[]).await;
assert!(result.stdout.contains("uid=1000"));
assert!(result.stdout.contains("gid=1000"));
assert!(result.stdout.contains("sandbox"));
}
#[tokio::test]
async fn test_id_custom_username() {
let result = run_builtin(&Id::with_username("bob"), &[]).await;
assert!(result.stdout.contains("uid=1000(bob)"));
assert!(result.stdout.contains("gid=1000(bob)"));
}
#[tokio::test]
async fn test_id_user() {
let result = run_builtin(&Id::new(), &["-u"]).await;
assert_eq!(result.stdout, "1000\n");
}
#[tokio::test]
async fn test_id_group() {
let result = run_builtin(&Id::new(), &["-g"]).await;
assert_eq!(result.stdout, "1000\n");
}
}