mirror of
https://github.com/nushell/nushell
synced 2025-01-12 05:09:04 +00:00
ls
/ps
: add switches to show SELinux security contexts
# User-Facing Changes A new optional compile-time `selinux` feature (requires `libselinux`) enables `ls --context` and `ps --context` to show the security contexts of file entries and processes.
This commit is contained in:
parent
ebce62629e
commit
ee513e4630
10 changed files with 157 additions and 3 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -67,7 +67,7 @@ jobs:
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
uses: actions-rust-lang/setup-rust-toolchain@v1.10.1
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: cargo test --workspace --profile ci --exclude nu_plugin_*
|
run: cargo test --workspace --profile ci --features selinux --exclude nu_plugin_*
|
||||||
- name: Check for clean repo
|
- name: Check for clean repo
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -379,6 +379,8 @@ dependencies = [
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -3416,6 +3418,7 @@ dependencies = [
|
||||||
"rstest",
|
"rstest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
|
"selinux",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
@ -5138,6 +5141,16 @@ dependencies = [
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn 2.0.90",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "print-positions"
|
name = "print-positions"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
@ -5643,6 +5656,12 @@ dependencies = [
|
||||||
"syn 2.0.87",
|
"syn 2.0.87",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reference-counted-singleton"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5daffa8f5ca827e146485577fa9dba9bd9c6921e06e954ab8f6408c10f753086"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
|
@ -6184,6 +6203,32 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selinux"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0139b2436c81305eb6bda33af151851f75bd62783817b25f44daa371119c30b5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"reference-counted-singleton",
|
||||||
|
"selinux-sys",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selinux-sys"
|
||||||
|
version = "0.6.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5e6e2b8e07a8ff45c90f8e3611bf10c4da7a28d73a26f9ede04f927da234f52"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"dunce",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
|
@ -148,6 +148,7 @@ rstest = { version = "0.23", default-features = false }
|
||||||
rusqlite = "0.31"
|
rusqlite = "0.31"
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
scopeguard = { version = "1.2.0" }
|
scopeguard = { version = "1.2.0" }
|
||||||
|
selinux = "0.4.6"
|
||||||
serde = { version = "1.0" }
|
serde = { version = "1.0" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
|
@ -293,6 +294,8 @@ trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"]
|
||||||
# SQLite commands for nushell
|
# SQLite commands for nushell
|
||||||
sqlite = ["nu-command/sqlite", "nu-cmd-lang/sqlite"]
|
sqlite = ["nu-command/sqlite", "nu-cmd-lang/sqlite"]
|
||||||
|
|
||||||
|
selinux = ["nu-command/selinux"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s" # Optimize for size
|
opt-level = "s" # Optimize for size
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
|
@ -1073,10 +1073,11 @@ fn flag_completions() {
|
||||||
// Test completions for the 'ls' flags
|
// Test completions for the 'ls' flags
|
||||||
let suggestions = completer.complete("ls -", 4);
|
let suggestions = completer.complete("ls -", 4);
|
||||||
|
|
||||||
assert_eq!(18, suggestions.len());
|
assert_eq!(20, suggestions.len());
|
||||||
|
|
||||||
let expected: Vec<String> = vec![
|
let expected: Vec<String> = vec![
|
||||||
"--all".into(),
|
"--all".into(),
|
||||||
|
"--context".into(),
|
||||||
"--directory".into(),
|
"--directory".into(),
|
||||||
"--du".into(),
|
"--du".into(),
|
||||||
"--full-paths".into(),
|
"--full-paths".into(),
|
||||||
|
@ -1094,6 +1095,7 @@ fn flag_completions() {
|
||||||
"-m".into(),
|
"-m".into(),
|
||||||
"-s".into(),
|
"-s".into(),
|
||||||
"-t".into(),
|
"-t".into(),
|
||||||
|
"-Z".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Match results
|
// Match results
|
||||||
|
|
|
@ -81,6 +81,7 @@ roxmltree = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
||||||
rmp = { workspace = true }
|
rmp = { workspace = true }
|
||||||
scopeguard = { workspace = true }
|
scopeguard = { workspace = true }
|
||||||
|
selinux = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||||
serde_urlencoded = { workspace = true }
|
serde_urlencoded = { workspace = true }
|
||||||
|
|
|
@ -31,6 +31,7 @@ struct Args {
|
||||||
directory: bool,
|
directory: bool,
|
||||||
use_mime_type: bool,
|
use_mime_type: bool,
|
||||||
use_threads: bool,
|
use_threads: bool,
|
||||||
|
security_context: bool,
|
||||||
call_span: Span,
|
call_span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ impl Command for Ls {
|
||||||
.switch("all", "Show hidden files", Some('a'))
|
.switch("all", "Show hidden files", Some('a'))
|
||||||
.switch(
|
.switch(
|
||||||
"long",
|
"long",
|
||||||
"Get all available columns for each entry (slower; columns are platform-dependent)",
|
"Get almost all available columns for each entry (slower; columns are platform-dependent)",
|
||||||
Some('l'),
|
Some('l'),
|
||||||
)
|
)
|
||||||
.switch(
|
.switch(
|
||||||
|
@ -79,6 +80,14 @@ impl Command for Ls {
|
||||||
)
|
)
|
||||||
.switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
|
.switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
|
||||||
.switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t'))
|
.switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t'))
|
||||||
|
.switch(
|
||||||
|
"context",
|
||||||
|
match cfg!(feature = "selinux") {
|
||||||
|
true => "Get the SELinux security context for each entry, if available",
|
||||||
|
false => "Get the SELinux security context for each entry (disabled)"
|
||||||
|
},
|
||||||
|
Some('Z'),
|
||||||
|
)
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +106,7 @@ impl Command for Ls {
|
||||||
let directory = call.has_flag(engine_state, stack, "directory")?;
|
let directory = call.has_flag(engine_state, stack, "directory")?;
|
||||||
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
|
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
|
||||||
let use_threads = call.has_flag(engine_state, stack, "threads")?;
|
let use_threads = call.has_flag(engine_state, stack, "threads")?;
|
||||||
|
let security_context = call.has_flag(engine_state, stack, "context")?;
|
||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
@ -110,6 +120,7 @@ impl Command for Ls {
|
||||||
directory,
|
directory,
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
use_threads,
|
use_threads,
|
||||||
|
security_context,
|
||||||
call_span,
|
call_span,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -252,6 +263,7 @@ fn ls_for_one_pattern(
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
use_threads,
|
use_threads,
|
||||||
call_span,
|
call_span,
|
||||||
|
security_context,
|
||||||
} = args;
|
} = args;
|
||||||
let pattern_arg = {
|
let pattern_arg = {
|
||||||
if let Some(path) = pattern_arg {
|
if let Some(path) = pattern_arg {
|
||||||
|
@ -439,6 +451,7 @@ fn ls_for_one_pattern(
|
||||||
&signals_clone,
|
&signals_clone,
|
||||||
use_mime_type,
|
use_mime_type,
|
||||||
args.full_paths,
|
args.full_paths,
|
||||||
|
security_context,
|
||||||
);
|
);
|
||||||
match entry {
|
match entry {
|
||||||
Ok(value) => Some(value),
|
Ok(value) => Some(value),
|
||||||
|
@ -557,6 +570,7 @@ pub(crate) fn dir_entry_dict(
|
||||||
signals: &Signals,
|
signals: &Signals,
|
||||||
use_mime_type: bool,
|
use_mime_type: bool,
|
||||||
full_symlink_target: bool,
|
full_symlink_target: bool,
|
||||||
|
security_context: bool,
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if metadata.is_none() {
|
if metadata.is_none() {
|
||||||
|
@ -613,6 +627,13 @@ pub(crate) fn dir_entry_dict(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if security_context {
|
||||||
|
record.push(
|
||||||
|
"security_context",
|
||||||
|
security_context_value(filename, span).unwrap_or(Value::nothing(span)), // TODO: consider report_shell_warning
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if long {
|
if long {
|
||||||
if let Some(md) = metadata {
|
if let Some(md) = metadata {
|
||||||
record.push("readonly", Value::bool(md.permissions().readonly(), span));
|
record.push("readonly", Value::bool(md.permissions().readonly(), span));
|
||||||
|
@ -765,6 +786,28 @@ fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn security_context_value(_path: &Path, span: Span) -> Result<Value, ShellError> {
|
||||||
|
#[cfg(not(all(feature = "selinux", target_os = "linux")))]
|
||||||
|
return Ok(Value::nothing(span));
|
||||||
|
|
||||||
|
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
use selinux;
|
||||||
|
match selinux::SecurityContext::of_path(_path, false, false)
|
||||||
|
.map_err(|e| ShellError::IOError { msg: e.to_string() })?
|
||||||
|
{
|
||||||
|
Some(con) => {
|
||||||
|
let bytes = con.as_bytes();
|
||||||
|
Ok(Value::string(
|
||||||
|
String::from_utf8_lossy(&bytes[0..bytes.len().saturating_sub(1)]),
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
None => Ok(Value::nothing(span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #[cfg(windows)] is just to make Clippy happy, remove if you ever want to use this on other platforms
|
// #[cfg(windows)] is just to make Clippy happy, remove if you ever want to use this on other platforms
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {
|
fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {
|
||||||
|
|
|
@ -24,6 +24,14 @@ impl Command for Ps {
|
||||||
"list all available columns for each entry",
|
"list all available columns for each entry",
|
||||||
Some('l'),
|
Some('l'),
|
||||||
)
|
)
|
||||||
|
.switch(
|
||||||
|
"context",
|
||||||
|
match cfg!(feature = "selinux") {
|
||||||
|
true => "get the security context for each entry, if available",
|
||||||
|
false => "get the security context for each entry (disabled)",
|
||||||
|
},
|
||||||
|
Some('Z'),
|
||||||
|
)
|
||||||
.filter()
|
.filter()
|
||||||
.category(Category::System)
|
.category(Category::System)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +93,7 @@ fn run_ps(
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
let span = call.head;
|
let span = call.head;
|
||||||
let long = call.has_flag(engine_state, stack, "long")?;
|
let long = call.has_flag(engine_state, stack, "long")?;
|
||||||
|
let security_context = call.has_flag(engine_state, stack, "context")?;
|
||||||
|
|
||||||
for proc in nu_system::collect_proc(Duration::from_millis(100), false) {
|
for proc in nu_system::collect_proc(Duration::from_millis(100), false) {
|
||||||
let mut record = Record::new();
|
let mut record = Record::new();
|
||||||
|
@ -103,6 +112,13 @@ fn run_ps(
|
||||||
record.push("mem", Value::filesize(proc.mem_size() as i64, span));
|
record.push("mem", Value::filesize(proc.mem_size() as i64, span));
|
||||||
record.push("virtual", Value::filesize(proc.virtual_size() as i64, span));
|
record.push("virtual", Value::filesize(proc.virtual_size() as i64, span));
|
||||||
|
|
||||||
|
if security_context {
|
||||||
|
record.push(
|
||||||
|
"security_context",
|
||||||
|
security_context_value(proc.pid(), span).unwrap_or(Value::nothing(span)), // TODO: consider report_shell_warning
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if long {
|
if long {
|
||||||
record.push("command", Value::string(proc.command(), span));
|
record.push("command", Value::string(proc.command(), span));
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
@ -189,3 +205,19 @@ fn run_ps(
|
||||||
|
|
||||||
Ok(output.into_pipeline_data(span, engine_state.signals().clone()))
|
Ok(output.into_pipeline_data(span, engine_state.signals().clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn security_context_value(_pid: i32, span: Span) -> Result<Value, ShellError> {
|
||||||
|
#[cfg(not(all(feature = "selinux", target_os = "linux")))]
|
||||||
|
return Ok(Value::nothing(span));
|
||||||
|
|
||||||
|
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
use selinux;
|
||||||
|
let con = selinux::SecurityContext::of_process(_pid, false)
|
||||||
|
.map_err(|e| ShellError::IOError { msg: e.to_string() })?;
|
||||||
|
Ok(Value::string(
|
||||||
|
String::from_utf8_lossy(&con.as_bytes()).trim_ascii_end(),
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -862,3 +862,17 @@ fn consistent_list_order() {
|
||||||
assert_eq!(no_arg.out, with_arg.out);
|
assert_eq!(no_arg.out, with_arg.out);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||||
|
#[test]
|
||||||
|
fn returns_correct_security_context() {
|
||||||
|
use nu_test_support::nu_with_std;
|
||||||
|
|
||||||
|
let input = "
|
||||||
|
use std assert
|
||||||
|
^ls -Z / | lines | each { |e| $e | str trim | split column ' ' 'coreutils_scontext' 'name' | first } \
|
||||||
|
| join (ls -Z / | each { default '?' security_context }) name \
|
||||||
|
| each { |e| assert equal $e.security_context $e.coreutils_scontext $'For entry ($e.name) expected ($e.coreutils_scontext), got ($e.security_context)' }
|
||||||
|
";
|
||||||
|
assert_eq!(nu_with_std!(input).err, "");
|
||||||
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ mod path;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod prepend;
|
mod prepend;
|
||||||
mod print;
|
mod print;
|
||||||
|
mod ps;
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
mod query;
|
mod query;
|
||||||
mod random;
|
mod random;
|
||||||
|
|
13
crates/nu-command/tests/commands/ps.rs
Normal file
13
crates/nu-command/tests/commands/ps.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#[cfg(all(feature = "selinux", target_os = "linux"))]
|
||||||
|
#[test]
|
||||||
|
fn returns_correct_security_context() {
|
||||||
|
use nu_test_support::nu_with_std;
|
||||||
|
|
||||||
|
let input = "
|
||||||
|
use std assert
|
||||||
|
^ps -o pid=,label= | lines | each { str trim | split column ' ' 'pid' 'procps_scontext' } | flatten \
|
||||||
|
| join (ps -Z | each { default '-' security_context }) pid \
|
||||||
|
| each { |e| assert equal $e.security_context $e.procps_scontext $'For process ($e.pid) expected ($e.procps_scontext), got ($e.security_context)' }
|
||||||
|
";
|
||||||
|
assert_eq!(nu_with_std!(input).err, "");
|
||||||
|
}
|
Loading…
Reference in a new issue