use super::Invocation;
use super::redteam::{read, value};
use super::report_command::{self as rc, ParentCheck, ReportDestination, SourceBudget, Verdict};
use crate::clients::WALK_BUDGET;
use crate::error::CliError;
use crate::json::OValue;
use crate::output::Format;
use crate::redteam::views;
use crate::report::{self, ReadError};
use scm::airs::red_team::{
AdapterListOptions, AttackListOptions, CustomAttacksReportListOptions, RedTeamClient,
RedTeamScanListOptions, TargetListOptions,
};
fn total_items(answer: &OValue) -> Option<OValue> {
answer
.get("pagination")
.and_then(|pagination| pagination.get("total_items"))
.filter(|total| views::present(Some(total)))
.cloned()
}
pub async fn job_report(
invocation: &mut Invocation,
job_id: &str,
attacks: bool,
severity: Option<&str>,
limit: u32,
format: Format,
) -> Result<(), CliError> {
let connected = invocation.connect_red_team(None).await?;
let client = &connected.client;
let answer = read(invocation, client.scans().get(job_id, None)).await?;
let mut job = views::job(&value(&answer)?);
views::withhold(&mut job, &invocation.secrets);
let kind = views::text(&job, "jobType").unwrap_or_default().to_owned();
let (mut report, mut list) = match kind.as_str() {
"CUSTOM" => {
let answer = read(
invocation,
client.custom_attack_reports().get_report(job_id, None),
)
.await?;
let report = views::custom_report(&value(&answer)?);
let list = if attacks {
let mut options = CustomAttacksReportListOptions::default();
options.limit = Some(limit);
let answer = read(
invocation,
client
.custom_attack_reports()
.list_custom_attacks(job_id, &options, None),
)
.await?;
let answer = value(&answer)?;
Some((
attack_rows(&answer, views::custom_attack),
total_items(&answer),
))
} else {
None
};
(report, list)
}
"DYNAMIC" => {
let answer = read(
invocation,
client.reports().get_dynamic_report(job_id, None),
)
.await?;
(views::dynamic_report(&value(&answer)?), None)
}
_ => {
let answer = read(invocation, client.reports().get_static_report(job_id, None)).await?;
let report = views::static_report(&value(&answer)?);
let list = if attacks {
let mut options = AttackListOptions::default();
options.limit = Some(limit);
options.severity = severity.map(str::to_owned);
let answer = read(
invocation,
client.reports().list_attacks(job_id, &options, None),
)
.await?;
let answer = value(&answer)?;
Some((attack_rows(&answer, views::attack), total_items(&answer)))
} else {
None
};
(report, list)
}
};
views::withhold(&mut report, &invocation.secrets);
if let Some((rows, _)) = &mut list {
for row in rows.iter_mut() {
views::withhold(row, &invocation.secrets);
}
}
let out = &mut invocation.output;
match format {
Format::Json | Format::Table => {
let mut doc = vec![
("job".to_owned(), job.clone()),
("report".to_owned(), report.clone()),
];
if let Some((rows, total)) = &list {
let mut attacks = vec![("items".to_owned(), OValue::Array(rows.clone()))];
if let Some(total) = total {
attacks.push(("totalItems".to_owned(), total.clone()));
}
doc.push(("attacks".to_owned(), OValue::Object(attacks)));
}
let doc = OValue::Object(doc);
if format == Format::Json {
out.json(&doc)
} else {
views::detail_table(out, &doc);
Ok(())
}
}
Format::Pretty => {
views::banner(out);
views::scan_status_layout(out, &job);
match kind.as_str() {
"CUSTOM" => {
views::custom_report_layout(out, &report);
if let Some((rows, _)) = &list {
views::custom_attack_list_layout(out, rows);
}
}
"DYNAMIC" => views::dynamic_report_layout(out, &report),
_ => {
views::static_report_layout(out, &report);
if let Some((rows, total)) = &list {
let total = total.as_ref().and_then(|total| match total {
OValue::Number(number) => number.as_f64(),
_ => None,
});
let footnote = views::attack_footnote(severity, total, &report);
views::attack_list_layout(out, rows, footnote.as_deref());
}
}
}
Ok(())
}
}
}
fn attack_rows(answer: &OValue, normalise: fn(&OValue) -> OValue) -> Vec<OValue> {
answer
.get("data")
.and_then(OValue::as_array)
.unwrap_or_default()
.iter()
.map(normalise)
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnvReportPlan {
pub destination: ReportDestination,
pub title: String,
pub max_pages: u32,
pub strict: bool,
}
pub fn default_name(now: std::time::SystemTime) -> String {
format!(
"airs-redteam-report-{}-{}.html",
crate::debug::iso_timestamp(now).replace(':', "-"),
&uuid::Uuid::new_v4().simple().to_string()[..8]
)
}
struct SdkReader<'a> {
client: &'a RedTeamClient,
budget: SourceBudget,
}
fn read_options() -> scm::Result<scm::RequestOptions> {
scm::RequestOptions::with_timeout(WALK_BUDGET).and_then(|options| options.with_retries(0))
}
impl report::Reader for SdkReader<'_> {
async fn overview(&self) -> Result<OValue, ReadError> {
self.budget
.read("overview", async |options| {
self.client.get_dashboard_overview(Some(&options)).await
})
.await
}
async fn statistics(&self) -> Result<OValue, ReadError> {
self.budget
.read("statistics", async |options| {
self.client
.get_scan_statistics(&Default::default(), Some(&options))
.await
})
.await
}
async fn targets(&self, skip: u64) -> Result<OValue, ReadError> {
let mut page = TargetListOptions::default();
page.skip = Some(skip);
page.limit = Some(report::PAGE_SIZE);
self.budget
.read("targets", async |options| {
self.client.targets().list(&page, Some(&options)).await
})
.await
}
async fn quota(&self) -> Result<OValue, ReadError> {
self.budget
.read("quota", async |options| {
self.client.get_quota_summary(Some(&options)).await
})
.await
}
async fn scans(&self, skip: u64) -> Result<OValue, ReadError> {
let mut page = RedTeamScanListOptions::default();
page.skip = Some(skip);
page.limit = Some(report::PAGE_SIZE);
self.budget
.read("scans", async |options| {
self.client.scans().list(&page, Some(&options)).await
})
.await
}
async fn channel_stats(&self) -> Result<OValue, ReadError> {
self.budget
.read("broker", async |options| {
self.client
.network_broker()
.get_channel_stats(Some(&options))
.await
})
.await
}
async fn adapters(&self, skip: u64) -> Result<OValue, ReadError> {
let mut page = AdapterListOptions::default();
page.skip = Some(skip);
page.limit = Some(report::PAGE_SIZE);
page.include_target_count = Some(true);
self.budget
.read("adapters", async |options| {
self.client.adapters().list(&page, Some(&options)).await
})
.await
}
}
pub async fn environment_report(
invocation: &mut Invocation,
plan: &EnvReportPlan,
) -> Result<(), CliError> {
let result = generate(invocation, plan).await;
rc::conclude(invocation, result)
}
async fn generate(invocation: &mut Invocation, plan: &EnvReportPlan) -> Result<i32, CliError> {
let path = rc::check_destination(&plan.destination, ParentCheck::AtWrite)?;
let prepared = invocation
.red_team_unauthenticated()
.map_err(rc::before_report)?;
invocation
.output
.status("Collecting Red Team statistics and current configuration (read-only)...");
let reader = SdkReader {
client: prepared.client(),
budget: SourceBudget::new(read_options),
};
let options = report::Options {
title: &plan.title,
max_pages: plan.max_pages,
now: &rc::now_ms,
};
let collected = report::collect(&reader, &options)
.await
.map_err(|_| rc::no_report())?;
let html = report::html::render(&collected);
let verdict = Verdict {
incomplete: collected.incomplete(),
health_unknown: collected.health == report::Health::Unknown,
strict: plan.strict,
};
rc::publish(invocation, path.as_deref(), &html, verdict)
}