pub mod args;
pub mod backup;
pub mod secrets;
pub mod views;
pub use crate::walk::ListParams;
use crate::error::CliError;
use crate::pagination;
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
const FILTER_LIMIT: usize = 4096;
pub const MAX_STATUS_VALUES: usize = 252;
pub fn check_uuid(field: &str, value: &str) -> Result<(), CliError> {
if pagination::is_uuid(value) {
Ok(())
} else {
Err(CliError::usage(format!(
"AISEC_USER_REQUEST_PAYLOAD_ERROR:Invalid {field}: {value}"
)))
}
}
pub fn check_filter(flag: &str, value: &str) -> Result<(), CliError> {
if value.len() <= FILTER_LIMIT && !value.chars().any(char::is_control) {
Ok(())
} else {
Err(CliError::usage(format!(
"{flag} must be at most {FILTER_LIMIT} bytes and contain no control characters"
)))
}
}
pub fn check_serial_numbers(value: &str) -> Result<(), CliError> {
if value.split(',').any(|serial| !serial.trim().is_empty()) {
Ok(())
} else {
Err(CliError::usage(
"--serial-numbers needs at least one serial number",
))
}
}
pub fn filter(flag: &str, value: Option<&str>) -> Result<Option<String>, CliError> {
match value {
Some(value) => {
check_filter(flag, value)?;
Ok(Some(value.to_owned()))
}
None => Ok(None),
}
}
pub fn query_filter(flag: &str, value: Option<&str>) -> Result<Option<String>, CliError> {
Ok(filter(flag, value)?.filter(|value| !value.is_empty()))
}
fn decimal(value: &str) -> Option<u64> {
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
value.parse::<u64>().ok()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LimitText {
Contract,
Typescript,
}
pub fn positive(flag: &str, value: &str, text: LimitText) -> Result<u32, CliError> {
decimal(value)
.filter(|n| *n >= 1)
.and_then(|n| u32::try_from(n).ok())
.ok_or_else(|| match text {
LimitText::Contract => CliError::usage(format!("{flag} must be a positive integer")),
LimitText::Typescript => CliError::usage(format!(
"{flag}: expected a positive integer, got \"{value}\""
)),
})
}
pub fn offset(value: &str) -> Result<u64, CliError> {
decimal(value)
.filter(|n| *n <= MAX_SAFE_INTEGER)
.ok_or_else(|| CliError::usage("--offset must be a non-negative integer"))
}
pub fn max(value: &str) -> Result<u64, CliError> {
decimal(value)
.filter(|n| *n <= MAX_SAFE_INTEGER)
.ok_or_else(|| CliError::usage("--max must be a non-negative integer"))
}
pub fn list_params(
limit: Option<&str>,
offset_value: Option<&str>,
all: bool,
max_value: Option<&str>,
text: LimitText,
) -> Result<ListParams, CliError> {
let limit = limit
.map(|value| positive("--limit", value, text))
.transpose()?;
let offset = offset_value.map(offset).transpose()?;
let max = max_value
.map(max)
.transpose()?
.unwrap_or(pagination::DEFAULT_MAX);
if all && offset_value.is_some() {
return Err(CliError::usage("--all cannot be combined with --offset"));
}
Ok(ListParams {
limit,
offset,
all,
max,
})
}
pub fn statuses(values: &[String]) -> Result<Vec<String>, CliError> {
if values.len() > MAX_STATUS_VALUES {
return Err(CliError::usage(format!(
"--status accepts at most {MAX_STATUS_VALUES} values"
)));
}
for value in values {
if value.is_empty() {
return Err(CliError::usage("--status values must not be empty"));
}
check_filter("--status", value)?;
}
Ok(values.to_vec())
}
pub const BANNER_TITLE: &str = "Prisma AIRS — AI Red Team";
pub const BANNER_SUBTITLE: &str = "Adversarial scan operations";
#[cfg(test)]
mod tests {
use super::*;
fn usage(result: Result<impl std::fmt::Debug, CliError>) -> String {
match result {
Err(CliError::Usage(message)) => message,
other => panic!("expected a usage error, got {other:?}"),
}
}
#[test]
fn uuid_checks_use_the_typescript_text() {
assert!(check_uuid("job id", "00000000-0000-4000-8000-000000000101").is_ok());
assert_eq!(
usage(check_uuid("job id", "not-a-uuid")),
"AISEC_USER_REQUEST_PAYLOAD_ERROR:Invalid job id: not-a-uuid"
);
assert_eq!(
usage(check_uuid("target uuid", "")),
"AISEC_USER_REQUEST_PAYLOAD_ERROR:Invalid target uuid: "
);
}
#[test]
fn serial_number_lists_name_at_least_one() {
for good in ["SN1", "SN1,SN2", " SN1 ", ",SN1,", "SN1,,SN2"] {
assert!(check_serial_numbers(good).is_ok(), "{good:?}");
}
for empty in ["", " ", ",", " , ,", ",,,", "\u{a0}"] {
assert_eq!(
usage(check_serial_numbers(empty)),
"--serial-numbers needs at least one serial number",
"{empty:?}"
);
}
}
#[test]
fn filters_numbers_and_list_flags() {
assert!(check_filter("--status", "RUNNING").is_ok());
assert_eq!(
usage(check_filter("--search", "a\nb")),
"--search must be at most 4096 bytes and contain no control characters"
);
assert!(check_filter("--search", &"x".repeat(4096)).is_ok());
assert!(check_filter("--search", &"x".repeat(4097)).is_err());
for bad in ["0", "-1", "abc", "1.5", "2.5x", "", "4294967296"] {
assert_eq!(
usage(positive("--limit", bad, LimitText::Contract)),
"--limit must be a positive integer",
"{bad}"
);
assert_eq!(
usage(positive("--depth", bad, LimitText::Typescript)),
format!("--depth: expected a positive integer, got \"{bad}\""),
);
}
assert_eq!(
positive("--limit", "4294967295", LimitText::Contract).unwrap(),
u32::MAX
);
assert_eq!(offset("9007199254740991").unwrap(), MAX_SAFE_INTEGER);
assert!(offset("9007199254740992").is_err());
assert_eq!(
usage(list_params(
None,
Some("0"),
true,
None,
LimitText::Contract
)),
"--all cannot be combined with --offset"
);
assert_eq!(
usage(list_params(
Some("x"),
Some("y"),
true,
Some("z"),
LimitText::Contract
)),
"--limit must be a positive integer"
);
assert_eq!(
usage(list_params(
None,
Some("y"),
true,
Some("z"),
LimitText::Contract
)),
"--offset must be a non-negative integer"
);
assert_eq!(
usage(list_params(
None,
None,
false,
Some("z"),
LimitText::Contract
)),
"--max must be a non-negative integer"
);
let params = list_params(Some("5"), None, true, Some("0"), LimitText::Contract).unwrap();
assert_eq!(
params,
ListParams {
limit: Some(5),
offset: None,
all: true,
max: 0
}
);
assert_eq!(
usage(statuses(&["ONLINE".into(), String::new()])),
"--status values must not be empty"
);
assert_eq!(
usage(statuses(&vec!["A".to_owned(); 253])),
"--status accepts at most 252 values"
);
assert_eq!(statuses(&vec!["A".to_owned(); 252]).unwrap().len(), 252);
}
}