use crate::parse::{Token, WordSet};
use crate::verdict::{SafetyLevel, Verdict};
static ROUTE_SAFE_FLAGS: WordSet = WordSet::new(&["--help", "--version", "-4", "-6", "-V", "-h", "-n", "-v"]);
static ROUTE_SAFE_SUBCMDS: WordSet = WordSet::new(&["get", "monitor", "print", "show"]);
fn is_safe_route(tokens: &[Token]) -> Verdict {
let mut i = 1;
while i < tokens.len() {
let t = &tokens[i];
if ROUTE_SAFE_FLAGS.contains(t) {
i += 1;
continue;
}
if ROUTE_SAFE_SUBCMDS.contains(t) {
return Verdict::Allowed(SafetyLevel::Inert);
}
return Verdict::Denied;
}
Verdict::Allowed(SafetyLevel::Inert)
}
pub(in crate::handlers::coreutils) fn dispatch(cmd: &str, tokens: &[Token]) -> Option<Verdict> {
match cmd {
"route" => Some(is_safe_route(tokens)),
_ => None,
}
}
pub(in crate::handlers::coreutils) fn command_docs() -> Vec<crate::docs::CommandDoc> {
vec![crate::docs::CommandDoc::handler(
"route",
"https://man7.org/linux/man-pages/man8/route.8.html",
"- Allowed subcommands: get, monitor, print, show\n- Allowed flags: -4, -6, -n, -v\n- Bare invocation allowed",
"net",
)]
}
#[cfg(test)]
mod tests {
use crate::is_safe_command;
fn check(cmd: &str) -> bool {
is_safe_command(cmd)
}
safe! {
route_bare: "route",
route_n: "route -n",
route_print: "route print",
route_get: "route get 8.8.8.8",
route_show: "route show",
route_monitor: "route monitor",
route_dash_v: "route -v",
route_n_get: "route -n get 8.8.8.8",
route_4_get: "route -4 get 8.8.8.8",
}
denied! {
route_add_denied: "route add default 192.168.1.1",
route_del_denied: "route del default",
route_delete_denied: "route delete default",
route_change_denied: "route change default 192.168.1.1",
route_flush_denied: "route flush",
route_replace_denied: "route replace default via 192.168.1.1",
route_n_add_denied: "route -n add default 192.168.1.1",
}
use proptest::prelude::*;
const SAFE_FLAGS: &[&str] = &["--help", "--version", "-4", "-6", "-V", "-h", "-n", "-v"];
const SAFE_SUBCMDS: &[&str] = &["get", "monitor", "print", "show"];
proptest! {
#[test]
fn is_safe_route_matches_the_specified_reference(
flags in proptest::collection::vec(proptest::sample::select(SAFE_FLAGS), 0..8),
extra in proptest::collection::vec("[a-zA-Z0-9._:/-]{0,6}", 0..6),
) {
let mut tokens: Vec<crate::parse::Token> = Vec::with_capacity(1 + flags.len() + 1 + extra.len());
tokens.push(crate::parse::Token::from_raw("route".to_string()));
for f in &flags {
tokens.push(crate::parse::Token::from_raw((*f).to_string()));
}
for e in &extra {
tokens.push(crate::parse::Token::from_raw(e.clone()));
}
let actual = super::is_safe_route(&tokens);
let mut allowed = true;
let mut decided = false;
for t in tokens.iter().skip(1) {
if SAFE_FLAGS.contains(&t.as_str()) { continue; }
allowed = SAFE_SUBCMDS.contains(&t.as_str());
decided = true;
break;
}
let reference = if decided { if allowed { super::Verdict::Allowed(super::SafetyLevel::Inert) } else { super::Verdict::Denied } } else { super::Verdict::Allowed(super::SafetyLevel::Inert) };
prop_assert_eq!(actual, reference, "flags={:?} extra={:?}", flags, extra);
}
#[test]
fn trailing_tokens_after_a_safe_subcommand_are_inert(
flags in proptest::collection::vec(proptest::sample::select(SAFE_FLAGS), 0..6),
sub in proptest::sample::select(SAFE_SUBCMDS),
suffix in proptest::collection::vec("[a-zA-Z0-9._:/-]{1,8}", 0..10),
) {
let mut tokens: Vec<crate::parse::Token> = Vec::with_capacity(1 + flags.len() + 1 + suffix.len());
tokens.push(crate::parse::Token::from_raw("route".to_string()));
for f in &flags {
tokens.push(crate::parse::Token::from_raw((*f).to_string()));
}
tokens.push(crate::parse::Token::from_raw((*sub).to_string()));
for s in &suffix {
tokens.push(crate::parse::Token::from_raw(s.clone()));
}
prop_assert_eq!(
super::is_safe_route(&tokens),
super::Verdict::Allowed(super::SafetyLevel::Inert),
"flags={:?} sub={} suffix={:?}",
flags, sub, suffix,
);
}
#[test]
fn any_unknown_token_before_any_subcommand_denies(
flags in proptest::collection::vec(proptest::sample::select(SAFE_FLAGS), 0..6),
hostile in proptest::sample::select(&["add", "del", "delete", "change", "flush", "replace", "evil", "set", "unset", "frob"][..]),
more in proptest::collection::vec(proptest::sample::select(SAFE_SUBCMDS), 0..3),
) {
let mut tokens: Vec<crate::parse::Token> = Vec::with_capacity(1 + flags.len() + 1 + more.len());
tokens.push(crate::parse::Token::from_raw("route".to_string()));
for f in &flags {
tokens.push(crate::parse::Token::from_raw((*f).to_string()));
}
tokens.push(crate::parse::Token::from_raw((*hostile).to_string()));
for m in &more {
tokens.push(crate::parse::Token::from_raw((*m).to_string()));
}
prop_assert_eq!(
super::is_safe_route(&tokens),
super::Verdict::Denied,
"flags={:?} hostile={} later_subs={:?}",
flags, hostile, more,
);
}
#[test]
fn command_docs_is_a_single_route_entry(
_ in proptest::num::u8::ANY,
) {
let docs = super::command_docs();
prop_assert_eq!(docs.len(), 1, "expected exactly one CommandDoc entry, got {}", docs.len());
let doc = &docs[0];
prop_assert_eq!(&doc.name, "route");
prop_assert_eq!(&doc.category, "net");
prop_assert!(!doc.url.is_empty(), "url must be non-empty");
prop_assert!(doc.url.starts_with("https://"), "url must be https, got {}", doc.url);
prop_assert!(!doc.description.is_empty(), "description must be non-empty");
prop_assert!(doc.aliases.is_empty(), "aliases must be empty");
prop_assert!(doc.examples.is_empty(), "examples must be empty");
}
}
}