use crate::clients::SupplychainClients;
use crate::supplychain::Failure;
use crate::walk::total_of;
use scm::RawRequest;
use scm::airs::agentguard::models::FiniteNumber;
use serde_json::{Map, Number, Value};
pub const OPERATION: &str = "airs.agentguard.list_scans";
#[derive(Clone, Copy, Debug)]
pub struct ScanQuery<'a> {
pub skip: u64,
pub limit: u32,
pub start: Option<&'a str>,
pub end: Option<&'a str>,
}
fn query_number(value: u64) -> String {
let number = FiniteNumber::from(value).as_f64().unwrap_or(0.0);
ryu_js::Buffer::new().format_finite(number).to_owned()
}
impl ScanQuery<'_> {
pub fn pairs(&self) -> Vec<(String, String)> {
let mut pairs = vec![
("skip".to_owned(), query_number(self.skip)),
("limit".to_owned(), query_number(u64::from(self.limit))),
("isBackgroundRefresh".to_owned(), "false".to_owned()),
];
if let Some(start) = self.start {
pairs.push(("start_time".to_owned(), start.to_owned()));
}
if let Some(end) = self.end {
pairs.push(("end_time".to_owned(), end.to_owned()));
}
pairs
}
}
pub async fn send(
clients: &SupplychainClients,
query: &ScanQuery<'_>,
options: &scm::RequestOptions,
) -> scm::Result<Value> {
let request = RawRequest::read("/v1/scans").query(query.pairs());
let answer = crate::debug::named(
OPERATION,
clients
.agentguard
.data_client()
.send_optional_json::<Value>(request, Some(options)),
)
.await?
.ok_or_else(|| scm::Error::new(scm::ErrorKind::Decode))?;
if within_bounds(&answer, &clients.validation) {
Ok(answer)
} else {
Err(scm::Error::new(scm::ErrorKind::Limit))
}
}
pub fn within_bounds(answer: &Value, limits: &scm::validation::ValidationLimits) -> bool {
let mut stack = vec![(answer, 0usize)];
let mut nodes = 0usize;
let mut bytes = 0usize;
while let Some((value, depth)) = stack.pop() {
nodes = nodes.saturating_add(1);
if nodes > limits.max_nodes || depth > limits.max_depth {
return false;
}
match value {
Value::String(text) => bytes = bytes.saturating_add(text.len()),
Value::Array(values) => stack.extend(values.iter().map(|value| (value, depth + 1))),
Value::Object(values) => {
for (key, value) in values {
bytes = bytes.saturating_add(key.len());
stack.push((value, depth + 1));
}
}
Value::Null | Value::Bool(_) | Value::Number(_) => {}
}
if bytes > limits.max_input_bytes {
return false;
}
}
true
}
#[derive(Clone, Debug, PartialEq)]
pub struct Summary {
pub vulnerability_count: Option<Number>,
pub attack_chain_count: Option<Number>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Scan {
pub uuid: String,
pub artifact_type: Option<String>,
pub status: Option<String>,
pub eval_outcome: Option<String>,
pub created_at: Option<String>,
pub summary: Option<Summary>,
pub is_batch: Option<bool>,
pub parent_uuid: Option<String>,
pub children: Option<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ScanPage {
pub scans: Vec<Scan>,
pub total: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Refused {
Page,
Identity,
Member,
}
impl From<Refused> for Failure {
fn from(_: Refused) -> Self {
Failure::Contract
}
}
fn nullable<'a, T>(
scan: &'a Map<String, Value>,
key: &str,
read: impl Fn(&'a Value) -> Option<T>,
) -> Result<Option<T>, Refused> {
match scan.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => read(value).map(Some).ok_or(Refused::Member),
}
}
fn text(scan: &Map<String, Value>, key: &str) -> Result<Option<String>, Refused> {
nullable(scan, key, |value| value.as_str().map(str::to_owned))
}
fn summary_count(summary: &Map<String, Value>, key: &str) -> Result<Option<Number>, Refused> {
nullable(summary, key, |value| match value {
Value::Number(number) => total_of(number.as_f64()).map(|_| number.clone()),
_ => None,
})
}
pub fn scan(value: &Value) -> Result<Scan, Refused> {
let scan = value.as_object().ok_or(Refused::Identity)?;
let uuid = match scan.get("uuid") {
Some(Value::String(uuid)) => uuid.clone(),
_ => return Err(Refused::Identity),
};
let summary = match nullable(scan, "summary", Value::as_object)? {
None => None,
Some(summary) => Some(Summary {
vulnerability_count: summary_count(summary, "vulnerability_count")?,
attack_chain_count: summary_count(summary, "attack_chain_count")?,
}),
};
Ok(Scan {
uuid,
artifact_type: text(scan, "artifact_type")?,
status: text(scan, "status")?,
eval_outcome: text(scan, "eval_outcome")?,
created_at: text(scan, "created_at")?,
summary,
is_batch: nullable(scan, "is_batch", Value::as_bool)?,
parent_uuid: text(scan, "parent_uuid")?,
children: nullable(scan, "child_scan_uuids", |value| {
value.as_array().map(Vec::len)
})?,
})
}
pub fn project(answer: &Value) -> Result<ScanPage, Refused> {
let page = answer.as_object().ok_or(Refused::Page)?;
let total = page
.get("pagination")
.and_then(Value::as_object)
.and_then(|pagination| pagination.get("total_items"))
.and_then(Value::as_number)
.and_then(|number| total_of(number.as_f64()))
.ok_or(Refused::Page)?;
let scans = page
.get("scans")
.and_then(Value::as_array)
.ok_or(Refused::Page)?
.iter()
.map(scan)
.collect::<Result<Vec<Scan>, Refused>>()?;
Ok(ScanPage { scans, total })
}
#[cfg(test)]
#[path = "scans_tests.rs"]
mod tests;