初始化 Panda RPKI v0.1.0 开源候选版本
This commit is contained in:
336
src/output/audit.rs
Normal file
336
src/output/audit.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::validation::policy::Policy;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditObjectKind {
|
||||
Manifest,
|
||||
Crl,
|
||||
Certificate,
|
||||
RouterCertificate,
|
||||
Roa,
|
||||
Aspa,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditObjectResult {
|
||||
Ok,
|
||||
Skipped,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ObjectAuditEntry {
|
||||
pub rsync_uri: String,
|
||||
pub sha256_hex: String,
|
||||
pub kind: AuditObjectKind,
|
||||
pub result: AuditObjectResult,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditWarning {
|
||||
pub message: String,
|
||||
pub category: String,
|
||||
pub rfc_refs: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryAuditManifest {
|
||||
pub schema_version: u32,
|
||||
pub status: String,
|
||||
pub events_path: String,
|
||||
pub events_count: u64,
|
||||
pub events_sha256: String,
|
||||
pub writer_version: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ValidationEventCounts {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub warnings: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vrps: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspas: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ValidationEvent {
|
||||
pub schema_version: u32,
|
||||
pub seq: u64,
|
||||
pub event_type: String,
|
||||
pub validation_time: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pp_node_id: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pp_manifest_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pp_rsync_base_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_sync_phase: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_terminal_state: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sha256: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_type: Option<AuditObjectKind>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<AuditObjectResult>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub counts: Option<ValidationEventCounts>,
|
||||
}
|
||||
|
||||
impl From<&crate::output::report::Warning> for AuditWarning {
|
||||
fn from(w: &crate::output::report::Warning) -> Self {
|
||||
Self {
|
||||
message: w.message.clone(),
|
||||
category: w.category.as_str().to_string(),
|
||||
rfc_refs: w.rfc_refs.iter().map(|r| r.0.to_string()).collect(),
|
||||
context: w.context.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct PublicationPointAudit {
|
||||
/// Monotonic node ID assigned by the traversal engine.
|
||||
///
|
||||
/// Present when running via the Stage2 tree engine; may be absent in ad-hoc runs.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub node_id: Option<u64>,
|
||||
/// Parent node ID in the traversal tree.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_node_id: Option<u64>,
|
||||
/// Provenance metadata for non-root nodes (how this CA instance was discovered).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub discovered_from: Option<DiscoveredFrom>,
|
||||
|
||||
pub rsync_base_uri: String,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
|
||||
pub source: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_sync_source: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_sync_phase: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_sync_duration_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repo_sync_error: Option<String>,
|
||||
pub repo_terminal_state: String,
|
||||
pub this_update_rfc3339_utc: String,
|
||||
pub next_update_rfc3339_utc: String,
|
||||
pub verified_at_rfc3339_utc: String,
|
||||
|
||||
pub warnings: Vec<AuditWarning>,
|
||||
pub objects: Vec<ObjectAuditEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct DiscoveredFrom {
|
||||
pub parent_manifest_rsync_uri: String,
|
||||
pub child_ca_certificate_rsync_uri: String,
|
||||
pub child_ca_certificate_sha256_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct TreeSummary {
|
||||
pub instances_processed: usize,
|
||||
pub instances_failed: usize,
|
||||
pub warnings: Vec<AuditWarning>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditRunMeta {
|
||||
pub validation_time_rfc3339_utc: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditDownloadKind {
|
||||
RrdpNotification,
|
||||
RrdpSnapshot,
|
||||
RrdpDelta,
|
||||
Rsync,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadObjectsStat {
|
||||
pub objects_count: u64,
|
||||
pub objects_bytes_total: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadEvent {
|
||||
pub kind: AuditDownloadKind,
|
||||
pub uri: String,
|
||||
pub started_at_rfc3339_utc: String,
|
||||
pub finished_at_rfc3339_utc: String,
|
||||
pub duration_ms: u64,
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects: Option<AuditDownloadObjectsStat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadKindStats {
|
||||
pub ok_total: u64,
|
||||
pub fail_total: u64,
|
||||
pub duration_ms_total: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes_total: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects_count_total: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects_bytes_total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadStats {
|
||||
pub events_total: u64,
|
||||
/// Statistics keyed by serialized `AuditDownloadKind` string (e.g. "rrdp_snapshot").
|
||||
pub by_kind: std::collections::BTreeMap<String, AuditDownloadKindStats>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn publication_point_audit_serializes_object_audit_only() {
|
||||
let audit = PublicationPointAudit {
|
||||
objects: vec![ObjectAuditEntry {
|
||||
rsync_uri: "rsync://example.test/repo/fresh.roa".to_string(),
|
||||
sha256_hex: "11".repeat(32),
|
||||
kind: AuditObjectKind::Roa,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
}],
|
||||
..PublicationPointAudit::default()
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&audit).expect("serialize audit");
|
||||
assert!(value.get("objects").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warning_audit_keeps_the_warning_category() {
|
||||
let warning = crate::output::report::Warning::new("BER-compatible CMS accepted")
|
||||
.with_category(crate::output::report::WarningCategory::BerCompatibleCmsEncoding);
|
||||
|
||||
let audit_warning = AuditWarning::from(&warning);
|
||||
assert_eq!(audit_warning.category, "ber_compatible_cms_encoding");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditRepoSyncStateStat {
|
||||
pub count: u64,
|
||||
pub duration_ms_total: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditRepoSyncStats {
|
||||
pub publication_points_total: u64,
|
||||
pub by_phase: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
|
||||
pub by_terminal_state: std::collections::BTreeMap<String, AuditRepoSyncStateStat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditReportV1 {
|
||||
pub format_version: u32,
|
||||
pub meta: AuditRunMeta,
|
||||
pub policy: Policy,
|
||||
pub tree: TreeSummary,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
|
||||
pub vrps: Vec<VrpOutput>,
|
||||
pub aspas: Vec<AspaOutput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditReportV2 {
|
||||
pub format_version: u32,
|
||||
pub meta: AuditRunMeta,
|
||||
pub policy: Policy,
|
||||
pub tree: TreeSummary,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
|
||||
pub vrps: Vec<VrpOutput>,
|
||||
pub aspas: Vec<AspaOutput>,
|
||||
|
||||
pub downloads: Vec<AuditDownloadEvent>,
|
||||
pub download_stats: AuditDownloadStats,
|
||||
pub repo_sync_stats: AuditRepoSyncStats,
|
||||
#[serde(rename = "queryAudit", skip_serializing_if = "Option::is_none")]
|
||||
pub query_audit: Option<QueryAuditManifest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct VrpOutput {
|
||||
pub asn: u32,
|
||||
pub prefix: String,
|
||||
pub max_length: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AspaOutput {
|
||||
pub customer_as_id: u32,
|
||||
pub provider_as_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
pub fn sha256_hex_from_32(bytes: &[u8; 32]) -> String {
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
hex::encode(digest)
|
||||
}
|
||||
|
||||
pub fn format_roa_ip_prefix(p: &crate::model::roa::IpPrefix) -> String {
|
||||
let addr = p.addr_bytes();
|
||||
match p.afi {
|
||||
crate::model::roa::RoaAfi::Ipv4 => {
|
||||
format!(
|
||||
"{}.{}.{}.{}/{}",
|
||||
addr[0], addr[1], addr[2], addr[3], p.prefix_len
|
||||
)
|
||||
}
|
||||
crate::model::roa::RoaAfi::Ipv6 => {
|
||||
let mut parts = Vec::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
let hi = addr[i * 2] as u16;
|
||||
let lo = addr[i * 2 + 1] as u16;
|
||||
parts.push(format!("{:x}", (hi << 8) | lo));
|
||||
}
|
||||
format!("{}/{}", parts.join(":"), p.prefix_len)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user