420 lines
14 KiB
Rust
420 lines
14 KiB
Rust
use std::io::BufWriter;
|
|
use std::path::Path;
|
|
|
|
use serde::Serialize;
|
|
use serde::ser::SerializeSeq;
|
|
use sha2::Digest;
|
|
|
|
use crate::audit::{
|
|
AspaOutput, AuditRunMeta, AuditWarning, QueryAuditManifest, ValidationEvent,
|
|
ValidationEventCounts, VrpOutput,
|
|
};
|
|
use crate::ccr::canonical_vrp_prefix;
|
|
|
|
use super::{PostValidationShared, RunStageTiming};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(super) enum ReportJsonFormat {
|
|
Pretty,
|
|
Compact,
|
|
}
|
|
|
|
pub(super) fn write_json<T: Serialize>(
|
|
path: &Path,
|
|
report: &T,
|
|
format: ReportJsonFormat,
|
|
) -> Result<(), String> {
|
|
let f = std::fs::File::create(path)
|
|
.map_err(|e| format!("create report file failed: {}: {e}", path.display()))?;
|
|
let writer = BufWriter::new(f);
|
|
match format {
|
|
ReportJsonFormat::Pretty => serde_json::to_writer_pretty(writer, report),
|
|
ReportJsonFormat::Compact => serde_json::to_writer(writer, report),
|
|
}
|
|
.map_err(|e| format!("write report json failed: {e}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(super) struct ReportJsonWriteTiming {
|
|
pub(super) build_ms: u64,
|
|
pub(super) write_ms: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct BorrowedAuditReportV2<'a> {
|
|
format_version: u32,
|
|
meta: AuditRunMeta,
|
|
policy: &'a crate::policy::Policy,
|
|
tree: BorrowedTreeSummary<'a>,
|
|
publication_points: &'a [crate::audit::PublicationPointAudit],
|
|
vrps: VrpReportSequence<'a>,
|
|
aspas: AspaReportSequence<'a>,
|
|
downloads: &'a [crate::audit::AuditDownloadEvent],
|
|
download_stats: &'a crate::audit::AuditDownloadStats,
|
|
repo_sync_stats: crate::audit::AuditRepoSyncStats,
|
|
#[serde(rename = "queryAudit", skip_serializing_if = "Option::is_none")]
|
|
query_audit: Option<QueryAuditManifest>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct BorrowedTreeSummary<'a> {
|
|
instances_processed: usize,
|
|
instances_failed: usize,
|
|
warnings: WarningReportSequence<'a>,
|
|
}
|
|
|
|
struct WarningReportSequence<'a>(&'a [crate::report::Warning]);
|
|
|
|
impl Serialize for WarningReportSequence<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
|
for warning in self.0 {
|
|
seq.serialize_element(&AuditWarning::from(warning))?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
struct VrpReportSequence<'a>(&'a [crate::validation::objects::Vrp]);
|
|
|
|
impl Serialize for VrpReportSequence<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
|
for vrp in self.0 {
|
|
seq.serialize_element(&VrpOutput {
|
|
asn: vrp.asn,
|
|
prefix: crate::audit::format_roa_ip_prefix(&vrp.prefix),
|
|
max_length: vrp.max_length,
|
|
})?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
struct AspaReportSequence<'a>(&'a [crate::validation::objects::AspaAttestation]);
|
|
|
|
impl Serialize for AspaReportSequence<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
|
for aspa in self.0 {
|
|
seq.serialize_element(&AspaOutput {
|
|
customer_as_id: aspa.customer_as_id,
|
|
provider_as_ids: aspa.provider_as_ids.clone(),
|
|
})?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
pub(super) fn write_report_json_from_shared(
|
|
path: &Path,
|
|
policy: &crate::policy::Policy,
|
|
validation_time: time::OffsetDateTime,
|
|
shared: &PostValidationShared,
|
|
format: ReportJsonFormat,
|
|
) -> Result<ReportJsonWriteTiming, String> {
|
|
use time::format_description::well_known::Rfc3339;
|
|
|
|
let build_started = std::time::Instant::now();
|
|
let validation_time_rfc3339_utc = validation_time
|
|
.to_offset(time::UtcOffset::UTC)
|
|
.format(&Rfc3339)
|
|
.expect("format validation_time");
|
|
let repo_sync_stats = super::build_repo_sync_stats(shared.publication_points.as_ref());
|
|
let query_audit = write_validation_events_sidecar(path, &validation_time_rfc3339_utc, shared)?;
|
|
let report = BorrowedAuditReportV2 {
|
|
format_version: 2,
|
|
meta: AuditRunMeta {
|
|
validation_time_rfc3339_utc,
|
|
},
|
|
policy,
|
|
tree: BorrowedTreeSummary {
|
|
instances_processed: shared.instances_processed,
|
|
instances_failed: shared.instances_failed,
|
|
warnings: WarningReportSequence(shared.tree_warnings.as_ref()),
|
|
},
|
|
publication_points: shared.publication_points.as_ref(),
|
|
vrps: VrpReportSequence(shared.vrps.as_ref()),
|
|
aspas: AspaReportSequence(shared.aspas.as_ref()),
|
|
downloads: shared.downloads.as_ref(),
|
|
download_stats: &shared.download_stats,
|
|
repo_sync_stats,
|
|
query_audit: Some(query_audit),
|
|
};
|
|
let build_ms = build_started.elapsed().as_millis() as u64;
|
|
|
|
let write_started = std::time::Instant::now();
|
|
write_json(path, &report, format)?;
|
|
Ok(ReportJsonWriteTiming {
|
|
build_ms,
|
|
write_ms: write_started.elapsed().as_millis() as u64,
|
|
})
|
|
}
|
|
|
|
fn write_validation_events_sidecar(
|
|
report_path: &Path,
|
|
validation_time: &str,
|
|
shared: &PostValidationShared,
|
|
) -> Result<QueryAuditManifest, String> {
|
|
let events_path = report_path.with_file_name("validation-events.jsonl");
|
|
if let Some(parent) = events_path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.map_err(|e| format!("create validation events parent failed: {e}"))?;
|
|
}
|
|
let mut writer = BufWriter::new(std::fs::File::create(&events_path).map_err(|e| {
|
|
format!(
|
|
"create validation events failed: {}: {e}",
|
|
events_path.display()
|
|
)
|
|
})?);
|
|
let mut seq = 0u64;
|
|
let mut hasher = sha2::Sha256::new();
|
|
emit_validation_events(validation_time, shared, &mut seq, &mut |event| {
|
|
let mut line = serde_json::to_vec(&event)
|
|
.map_err(|e| format!("serialize validation event failed: {e}"))?;
|
|
line.push(b'\n');
|
|
std::io::Write::write_all(&mut writer, &line)
|
|
.map_err(|e| format!("write validation event failed: {e}"))?;
|
|
hasher.update(&line);
|
|
Ok(())
|
|
})?;
|
|
std::io::Write::flush(&mut writer)
|
|
.map_err(|e| format!("flush validation events failed: {e}"))?;
|
|
let events_count = seq;
|
|
let events_sha256 = hex::encode(hasher.finalize());
|
|
Ok(QueryAuditManifest {
|
|
schema_version: 1,
|
|
status: "complete".to_string(),
|
|
events_path: events_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.unwrap_or("validation-events.jsonl")
|
|
.to_string(),
|
|
events_count,
|
|
events_sha256,
|
|
writer_version: 1,
|
|
error: None,
|
|
})
|
|
}
|
|
|
|
fn emit_validation_events(
|
|
validation_time: &str,
|
|
shared: &PostValidationShared,
|
|
seq: &mut u64,
|
|
emit: &mut impl FnMut(ValidationEvent) -> Result<(), String>,
|
|
) -> Result<(), String> {
|
|
emit(next_event(seq, "run_summary", validation_time, |event| {
|
|
event.counts = Some(ValidationEventCounts {
|
|
objects: Some(
|
|
shared
|
|
.publication_points
|
|
.iter()
|
|
.map(|pp| pp.objects.len() as u64)
|
|
.sum(),
|
|
),
|
|
warnings: Some(
|
|
(shared.tree_warnings.len()
|
|
+ shared
|
|
.publication_points
|
|
.iter()
|
|
.map(|pp| pp.warnings.len())
|
|
.sum::<usize>()) as u64,
|
|
),
|
|
vrps: Some(shared.vrps.len() as u64),
|
|
aspas: Some(shared.aspas.len() as u64),
|
|
});
|
|
}))?;
|
|
for pp in shared.publication_points.iter() {
|
|
emit(next_event(
|
|
seq,
|
|
"publication_point",
|
|
validation_time,
|
|
|event| {
|
|
event.pp_node_id = pp.node_id;
|
|
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
|
|
event.pp_rsync_base_uri = Some(pp.rsync_base_uri.clone());
|
|
event.repo_sync_phase = pp.repo_sync_phase.clone();
|
|
event.repo_terminal_state = Some(pp.repo_terminal_state.clone());
|
|
event.counts = Some(ValidationEventCounts {
|
|
objects: Some(pp.objects.len() as u64),
|
|
warnings: Some(pp.warnings.len() as u64),
|
|
vrps: None,
|
|
aspas: None,
|
|
});
|
|
},
|
|
))?;
|
|
for object in &pp.objects {
|
|
emit(next_event(seq, "object", validation_time, |event| {
|
|
event.pp_node_id = pp.node_id;
|
|
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
|
|
event.object_uri = Some(object.rsync_uri.clone());
|
|
event.sha256 = Some(object.sha256_hex.clone());
|
|
event.object_type = Some(object.kind.clone());
|
|
event.result = Some(object.result.clone());
|
|
event.reason = object.detail.clone();
|
|
}))?;
|
|
}
|
|
for warning in &pp.warnings {
|
|
emit(next_event(seq, "warning", validation_time, |event| {
|
|
event.pp_node_id = pp.node_id;
|
|
event.pp_manifest_uri = Some(pp.manifest_rsync_uri.clone());
|
|
event.reason = Some(warning.message.clone());
|
|
}))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn next_event(
|
|
seq: &mut u64,
|
|
event_type: &str,
|
|
validation_time: &str,
|
|
fill: impl FnOnce(&mut ValidationEvent),
|
|
) -> ValidationEvent {
|
|
*seq += 1;
|
|
let mut event = ValidationEvent {
|
|
schema_version: 1,
|
|
seq: *seq,
|
|
event_type: event_type.to_string(),
|
|
validation_time: validation_time.to_string(),
|
|
pp_node_id: None,
|
|
pp_manifest_uri: None,
|
|
pp_rsync_base_uri: None,
|
|
repo_sync_phase: None,
|
|
repo_terminal_state: None,
|
|
object_uri: None,
|
|
sha256: None,
|
|
object_type: None,
|
|
result: None,
|
|
reason: None,
|
|
counts: None,
|
|
};
|
|
fill(&mut event);
|
|
event
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(super) struct CompareViewTaskOutput {
|
|
pub(super) build_ms: Option<u64>,
|
|
pub(super) write_ms: Option<u64>,
|
|
}
|
|
|
|
pub(super) fn run_compare_view_task(
|
|
shared: &PostValidationShared,
|
|
vrps_csv_out_path: Option<&Path>,
|
|
vaps_csv_out_path: Option<&Path>,
|
|
trust_anchor: &str,
|
|
) -> Result<CompareViewTaskOutput, String> {
|
|
let mut build_ms = None;
|
|
let mut write_ms = None;
|
|
if let (Some(vrps_path), Some(vaps_path)) = (vrps_csv_out_path, vaps_csv_out_path) {
|
|
let started = std::time::Instant::now();
|
|
build_ms = Some(0);
|
|
write_direct_vrp_csv(vrps_path, shared.vrps.as_ref(), trust_anchor)?;
|
|
write_direct_vap_csv(vaps_path, shared.aspas.as_ref(), trust_anchor)?;
|
|
write_ms = Some(started.elapsed().as_millis() as u64);
|
|
eprintln!(
|
|
"wrote compare views: vrps={} vaps={}",
|
|
vrps_path.display(),
|
|
vaps_path.display()
|
|
);
|
|
}
|
|
Ok(CompareViewTaskOutput { build_ms, write_ms })
|
|
}
|
|
|
|
fn write_direct_vrp_csv(
|
|
path: &Path,
|
|
vrps: &[crate::validation::objects::Vrp],
|
|
trust_anchor: &str,
|
|
) -> Result<(), String> {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
|
|
}
|
|
let file = std::fs::File::create(path)
|
|
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
|
|
let mut writer = BufWriter::new(file);
|
|
use std::io::Write;
|
|
let trust_anchor = trust_anchor.to_ascii_lowercase();
|
|
writeln!(writer, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?;
|
|
for vrp in vrps {
|
|
writeln!(
|
|
writer,
|
|
"AS{},{},{},{}",
|
|
vrp.asn,
|
|
canonical_vrp_prefix(&vrp.prefix),
|
|
vrp.max_length,
|
|
trust_anchor
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_direct_vap_csv(
|
|
path: &Path,
|
|
aspas: &[crate::validation::objects::AspaAttestation],
|
|
trust_anchor: &str,
|
|
) -> Result<(), String> {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
|
|
}
|
|
let file = std::fs::File::create(path)
|
|
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
|
|
let mut writer = BufWriter::new(file);
|
|
use std::io::Write;
|
|
let trust_anchor = trust_anchor.to_ascii_lowercase();
|
|
writeln!(writer, "Customer ASN,Providers,Trust Anchor").map_err(|e| e.to_string())?;
|
|
for aspa in aspas {
|
|
let mut providers = aspa.provider_as_ids.clone();
|
|
providers.sort_unstable();
|
|
providers.dedup();
|
|
let providers = providers
|
|
.into_iter()
|
|
.map(|asn| format!("AS{asn}"))
|
|
.collect::<Vec<_>>()
|
|
.join(";");
|
|
writeln!(
|
|
writer,
|
|
"AS{},{},{}",
|
|
aspa.customer_as_id, providers, trust_anchor
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn write_stage_timing(
|
|
report_json_path: Option<&Path>,
|
|
stage_timing: &RunStageTiming,
|
|
) -> Result<(), String> {
|
|
if let Some(path) = report_json_path
|
|
&& let Some(parent) = path.parent()
|
|
{
|
|
let stage_timing_path = parent.join("stage-timing.json");
|
|
std::fs::write(
|
|
&stage_timing_path,
|
|
serde_json::to_vec_pretty(stage_timing).map_err(|e| e.to_string())?,
|
|
)
|
|
.map_err(|e| {
|
|
format!(
|
|
"write stage timing failed: {}: {e}",
|
|
stage_timing_path.display()
|
|
)
|
|
})?;
|
|
eprintln!("analysis: wrote {}", stage_timing_path.display());
|
|
}
|
|
Ok(())
|
|
}
|