diff --git a/src/cli/client.rs b/src/cli/client.rs index a0ca70b9..1101ef8d 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -229,6 +229,18 @@ impl KrillClient { Ok(ApiResponse::BgpAnalysisRoas(report.into())) } + CaCommand::BgpAnalysisSuggest(handle, resources) => { + let uri = format!("api/v1/cas/{}/routes/analysis/suggest", handle); + + let suggestions = if let Some(resources) = resources { + self.post_json_with_response(&uri, resources).await? + } else { + self.get_json(&uri).await? + }; + + Ok(ApiResponse::BgpAnalysisSuggestions(suggestions)) + } + CaCommand::Show(handle) => { let uri = format!("api/v1/cas/{}", handle); let ca_info = self.get_json(&uri).await?; diff --git a/src/cli/options.rs b/src/cli/options.rs index 583fb2ab..84ba2f53 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -611,6 +611,33 @@ impl Options { app.subcommand(sub) } + fn make_cas_routes_bgp_suggestions_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("suggest").about("Show ROA suggestions."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + sub = sub + .arg( + Arg::with_name("ipv4") + .short("4") + .long("ipv4") + .value_name("IPv4 resources") + .help("Scope to these IPv4 resources") + .required(false), + ) + .arg( + Arg::with_name("ipv6") + .short("6") + .long("ipv6") + .value_name("IPv6 resources") + .help("Scope to these IPv6 resources") + .required(false), + ); + + app.subcommand(sub) + } + fn make_cas_routes_bgp_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { let mut sub = SubCommand::with_name("bgp").about("Show current authorizations in relation to known announcements."); @@ -618,6 +645,7 @@ impl Options { sub = Self::make_cas_routes_bgp_full_sc(sub); sub = Self::make_cas_routes_bgp_announcements_sc(sub); sub = Self::make_cas_routes_bgp_roas_sc(sub); + sub = Self::make_cas_routes_bgp_suggestions_sc(sub); app.subcommand(sub) } @@ -1321,6 +1349,24 @@ impl Options { )) } + fn parse_matches_cas_routes_bgp_suggest(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let v4 = matches.value_of("ipv4").unwrap_or(""); + let v6 = matches.value_of("ipv6").unwrap_or(""); + + let resources = ResourceSet::from_strs("", v4, v6) + .map_err(|e| Error::GeneralArgumentError(format!("Could not parse IP resources: {}", e)))?; + + let resources = if resources.is_empty() { None } else { Some(resources) }; + + Ok(Options::make( + general_args, + Command::CertAuth(CaCommand::BgpAnalysisSuggest(my_ca, resources)), + )) + } + fn parse_matches_cas_routes_bgp(matches: &ArgMatches) -> Result { if let Some(m) = matches.subcommand_matches("full") { Self::parse_matches_cas_routes_bgp_full(m) @@ -1328,6 +1374,8 @@ impl Options { Self::parse_matches_cas_routes_bgp_announcements(m) } else if let Some(m) = matches.subcommand_matches("roas") { Self::parse_matches_cas_routes_bgp_roas(m) + } else if let Some(m) = matches.subcommand_matches("suggest") { + Self::parse_matches_cas_routes_bgp_suggest(m) } else { Err(Error::UnrecognisedSubCommand) } @@ -1722,6 +1770,9 @@ pub enum CaCommand { #[display(fmt = "Show ROA centric summary of ROA vs BGP analysis for ca: '{}'", _0)] BgpAnalysisRoas(Handle), + #[display(fmt = "Show ROA suggestions based on BGP analysis for ca: '{}'", _0)] + BgpAnalysisSuggest(Handle, Option), + // Show details for this CA #[display(fmt = "Show details for ca: '{}'", _0)] Show(Handle), diff --git a/src/cli/report.rs b/src/cli/report.rs index b31e6aa2..72aef392 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -10,7 +10,7 @@ use crate::commons::api::{ CertAuthList, ChildCaInfo, CommandHistory, CurrentObjects, CurrentRepoState, ParentCaContact, PublisherDetails, PublisherList, RepositoryContact, RoaDefinition, ServerInfo, StoredEffect, }; -use crate::commons::bgp::{AnnouncementReport, BgpAnalysisReport, RoaReport}; +use crate::commons::bgp::{AnnouncementReport, BgpAnalysisReport, BgpAnalysisSuggestion, RoaReport}; use crate::commons::eventsourcing::WithStorableDetails; use crate::commons::remote::api::ClientInfo; use crate::commons::remote::rfc8183; @@ -34,6 +34,7 @@ pub enum ApiResponse { BgpAnalysisFull(BgpAnalysisReport), BgpAnalysisAnnouncements(AnnouncementReport), BgpAnalysisRoas(RoaReport), + BgpAnalysisSuggestions(BgpAnalysisSuggestion), ParentCaContact(ParentCaContact), @@ -78,6 +79,7 @@ impl ApiResponse { ApiResponse::BgpAnalysisFull(table) => Ok(Some(table.report(fmt)?)), ApiResponse::BgpAnalysisAnnouncements(summary) => Ok(Some(summary.report(fmt)?)), ApiResponse::BgpAnalysisRoas(summary) => Ok(Some(summary.report(fmt)?)), + ApiResponse::BgpAnalysisSuggestions(suggestions) => Ok(Some(suggestions.report(fmt)?)), ApiResponse::ParentCaContact(contact) => Ok(Some(contact.report(fmt)?)), ApiResponse::ChildInfo(info) => Ok(Some(info.report(fmt)?)), ApiResponse::PublisherList(list) => Ok(Some(list.report(fmt)?)), @@ -430,6 +432,12 @@ impl Report for RoaReport { } } +impl Report for BgpAnalysisSuggestion { + fn text(&self) -> Result { + Ok(self.to_string()) + } +} + impl Report for CaRepoDetails { fn text(&self) -> Result { let mut res = String::new(); diff --git a/src/commons/api/roas.rs b/src/commons/api/roas.rs index d0ab029c..7c7d7ebe 100644 --- a/src/commons/api/roas.rs +++ b/src/commons/api/roas.rs @@ -278,6 +278,10 @@ pub struct RoaDefinitionUpdates { } impl RoaDefinitionUpdates { + pub fn is_empty(&self) -> bool { + self.added.is_empty() && self.removed.is_empty() + } + pub fn new(added: HashSet, removed: HashSet) -> Self { RoaDefinitionUpdates { added, removed } } diff --git a/src/commons/bgp/analyser.rs b/src/commons/bgp/analyser.rs index c2973167..17f267fc 100644 --- a/src/commons/bgp/analyser.rs +++ b/src/commons/bgp/analyser.rs @@ -10,7 +10,8 @@ use rpki::x509::Time; use crate::commons::api::{ResourceSet, RoaDefinition, TypedPrefix}; use crate::commons::bgp::{ make_roa_tree, make_validated_announcement_tree, Announcement, AnnouncementValidity, Announcements, - BgpAnalysisEntry, BgpAnalysisReport, IpRange, RisDumpError, RisDumpLoader, ValidatedAnnouncement, + BgpAnalysisEntry, BgpAnalysisReport, BgpAnalysisState, BgpAnalysisSuggestion, IpRange, RisDumpError, RisDumpLoader, + ValidatedAnnouncement, }; use crate::constants::{BGP_RIS_REFRESH_MINUTES, KRILL_ENV_TEST_ANN}; @@ -66,13 +67,19 @@ impl BgpAnalyser { let seen = self.seen.read().unwrap(); let mut entries = vec![]; + let roas: Vec = roas + .iter() + .filter(|roa| scope.contains_roa_address(&roa.as_roa_ip_address())) + .cloned() + .collect(); + if seen.last_updated().is_none() { // nothing to analyse, just push all ROAs as 'no announcement info' for roa in roas { - entries.push(BgpAnalysisEntry::roa_no_announcement_info(*roa)); + entries.push(BgpAnalysisEntry::roa_no_announcement_info(roa)); } } else { - let roa_tree = make_roa_tree(roas); + let roa_tree = make_roa_tree(roas.as_ref()); let (v4_scope, v6_scope) = IpRange::for_resource_set(&scope); @@ -97,7 +104,7 @@ impl BgpAnalyser { let covered = validated_tree.matching_or_more_specific(&roa.prefix()); if covered.is_empty() { - entries.push(BgpAnalysisEntry::roa_unseen(*roa)) + entries.push(BgpAnalysisEntry::roa_unseen(roa)) } else { let authorizes: Vec = covered .iter() @@ -140,16 +147,16 @@ impl BgpAnalyser { .collect(); if authorizes.is_empty() && disallows.is_empty() { - entries.push(BgpAnalysisEntry::roa_unseen(*roa)) + entries.push(BgpAnalysisEntry::roa_unseen(roa)) } else if !authorizes_excess.is_empty() { entries.push(BgpAnalysisEntry::roa_too_permissive( - *roa, + roa, authorizes, disallows, authorizes_excess, )) } else { - entries.push(BgpAnalysisEntry::roa_seen(*roa, authorizes, disallows)) + entries.push(BgpAnalysisEntry::roa_seen(roa, authorizes, disallows)) } } } @@ -185,6 +192,33 @@ impl BgpAnalyser { BgpAnalysisReport::new(entries) } + pub fn suggest(&self, roas: &[RoaDefinition], scope: &ResourceSet) -> BgpAnalysisSuggestion { + let mut suggestion = BgpAnalysisSuggestion::default(); + + // perform analysis + for entry in self.analyse(roas, scope).into_entries() { + match entry.state() { + BgpAnalysisState::RoaUnseen => suggestion.add_stale(entry.into_definition()), + BgpAnalysisState::RoaTooPermissive => { + let replace_with = entry + .authorizes() + .iter() + .map(|auth| RoaDefinition::from(*auth)) + .collect(); + suggestion.add_too_permissive(entry.into_definition(), replace_with); + } + BgpAnalysisState::AnnouncementNotFound => suggestion.add_not_found(entry.into_definition()), + BgpAnalysisState::AnnouncementInvalidAsn => suggestion.add_invalid_asn(entry.into_definition()), + BgpAnalysisState::AnnouncementInvalidLength => suggestion.add_invalid_length(entry.into_definition()), + BgpAnalysisState::RoaSeen => suggestion.add_keep(entry.into_definition()), + BgpAnalysisState::RoaNoAnnouncementInfo => suggestion.add_keep(entry.into_definition()), + _ => {} + } + } + + suggestion + } + fn test_announcements() -> Vec { use crate::test::announcement; @@ -306,4 +340,48 @@ mod tests { assert_eq!(roas_no_info.as_slice(), &[&roa1, &roa2, &roa3]); } + + #[test] + fn make_bgp_analysis_suggestion() { + let roa_too_permissive = definition("10.0.0.0/22-23 => 64496"); + let roa_disallowing = definition("10.0.4.0/24 => 0"); + let roa_unseen_completely = definition("10.0.3.0/24 => 64497"); + let roa_authorizing_single = definition("192.168.1.0/24 => 64497"); + + let analyser = BgpAnalyser::with_test_announcements(); + + let scope = ResourceSet::from_strs("", "10.0.0.0/22", "").unwrap(); + let suggestion_resource_subset = analyser.suggest( + &[ + roa_too_permissive, + roa_disallowing, + roa_unseen_completely, + roa_authorizing_single, + ], + &scope, + ); + + let expected: BgpAnalysisSuggestion = serde_json::from_str(include_str!( + "../../../test-resources/bgp/expected_suggestion_some_roas.json" + )) + .unwrap(); + assert_eq!(suggestion_resource_subset, expected); + + let scope = ResourceSet::from_strs("", "10.0.0.0/8,192.168.0.0/16", "").unwrap(); + let suggestion_all_roas_in_scope = analyser.suggest( + &[ + roa_too_permissive, + roa_disallowing, + roa_unseen_completely, + roa_authorizing_single, + ], + &scope, + ); + let expected: BgpAnalysisSuggestion = serde_json::from_str(include_str!( + "../../../test-resources/bgp/expected_suggestion_all_roas.json" + )) + .unwrap(); + + assert_eq!(suggestion_all_roas_in_scope, expected); + } } diff --git a/src/commons/bgp/report.rs b/src/commons/bgp/report.rs index 38f99939..e84f4326 100644 --- a/src/commons/bgp/report.rs +++ b/src/commons/bgp/report.rs @@ -1,10 +1,168 @@ use std::cmp::Ordering; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt; -use crate::commons::api::{BgpStats, RoaDefinition}; +use crate::commons::api::{BgpStats, RoaDefinition, RoaDefinitionUpdates}; use crate::commons::bgp::Announcement; +//------------ BgpAnalysisSuggestion --------------------------------------- + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct BgpAnalysisSuggestion { + stale: Vec, + not_found: Vec, + invalid_asn: Vec, + invalid_length: Vec, + too_permissive: Vec, + keep: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct ReplacementRoaSuggestion { + current: RoaDefinition, + new: Vec, +} + +impl From for RoaDefinitionUpdates { + fn from(suggestion: BgpAnalysisSuggestion) -> Self { + let (stale, not_found, invalid_asn, invalid_length, too_permissive) = ( + suggestion.stale, + suggestion.not_found, + suggestion.invalid_asn, + suggestion.invalid_length, + suggestion.too_permissive, + ); + + let mut added = HashSet::new(); + let mut removed = HashSet::new(); + + for auth in not_found + .into_iter() + .chain(invalid_asn.into_iter()) + .chain(invalid_length.into_iter()) + { + added.insert(auth); + } + + for auth in stale.into_iter() { + removed.insert(auth); + } + + for suggestion in too_permissive.into_iter() { + removed.insert(suggestion.current); + for auth in suggestion.new.into_iter() { + added.insert(auth); + } + } + + RoaDefinitionUpdates::new(added, removed) + } +} + +impl Default for BgpAnalysisSuggestion { + fn default() -> Self { + BgpAnalysisSuggestion { + stale: vec![], + not_found: vec![], + invalid_asn: vec![], + invalid_length: vec![], + too_permissive: vec![], + keep: vec![], + } + } +} + +impl BgpAnalysisSuggestion { + pub fn add_stale(&mut self, authorization: RoaDefinition) { + self.stale.push(authorization) + } + + pub fn add_too_permissive(&mut self, current: RoaDefinition, new: Vec) { + let replacement = ReplacementRoaSuggestion { current, new }; + self.too_permissive.push(replacement) + } + + pub fn add_not_found(&mut self, authorization: RoaDefinition) { + self.not_found.push(authorization) + } + + pub fn add_invalid_asn(&mut self, authorization: RoaDefinition) { + self.invalid_asn.push(authorization) + } + + pub fn add_invalid_length(&mut self, authorization: RoaDefinition) { + self.invalid_length.push(authorization) + } + + pub fn add_keep(&mut self, authorization: RoaDefinition) { + self.keep.push(authorization) + } +} + +#[allow(clippy::cognitive_complexity)] +impl fmt::Display for BgpAnalysisSuggestion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if !self.stale.is_empty() { + writeln!(f, "Remove the following stale entries:")?; + for auth in &self.stale { + writeln!(f, " {}", auth)?; + } + writeln!(f)?; + } + + if !self.too_permissive.is_empty() { + writeln!(f, "Replace the following too permissive entries:")?; + for entry in &self.too_permissive { + writeln!(f, " Remove: {}", entry.current)?; + for replace in &entry.new { + writeln!(f, " Add: {}", replace)?; + } + writeln!(f)?; + } + } + + if !self.not_found.is_empty() { + writeln!(f, "Authorize these announcements which are currently not covered:")?; + for auth in &self.not_found { + writeln!(f, " {}", auth)?; + } + writeln!(f)?; + } + + if !self.invalid_length.is_empty() { + writeln!( + f, + "Authorize these announcements which are currently invalid because they are too specific:" + )?; + for auth in &self.invalid_length { + writeln!(f, " {}", auth)?; + } + writeln!(f)?; + } + + if !self.invalid_asn.is_empty() { + writeln!( + f, + "Authorize these announcements which are currently invalid because they are not allowed for these ASNs:" + )?; + for auth in &self.invalid_asn { + writeln!(f, " {}", auth)?; + } + writeln!(f)?; + } + + if !self.keep.is_empty() { + writeln!(f, "Keep the following entries:")?; + for auth in &self.keep { + writeln!(f, " {}", auth)?; + } + writeln!(f)?; + } + + Ok(()) + } +} + //------------ BgpAnalysisReport ------------------------------------------- #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -20,6 +178,10 @@ impl BgpAnalysisReport { &self.0 } + pub fn into_entries(self) -> Vec { + self.0 + } + pub fn matching_defs(&self, state: BgpAnalysisState) -> Vec<&RoaDefinition> { self.matching_entries(state) .into_iter() @@ -208,6 +370,10 @@ impl BgpAnalysisEntry { &self.definition } + pub fn into_definition(self) -> RoaDefinition { + self.definition + } + pub fn state(&self) -> BgpAnalysisState { self.state } diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index 45f8fc2e..aafa511e 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -589,10 +589,7 @@ async fn api_ca_routes(req: Request, path: &mut RequestPath, ca: Handle) -> Rout Method::POST => ca_routes_update(req, ca).await, _ => render_unknown_method(), }, - Some("analysis") => match *req.method() { - Method::GET => ca_routes_analysis(req, path, ca).await, - _ => render_unknown_method(), - }, + Some("analysis") => ca_routes_analysis(req, path, ca).await, _ => render_unknown_method(), } } @@ -1078,6 +1075,19 @@ async fn ca_routes_show(req: Request, handle: Handle) -> RoutingResult { async fn ca_routes_analysis(req: Request, path: &mut RequestPath, handle: Handle) -> RoutingResult { match path.next() { Some("full") => render_json_res(req.state().read().await.ca_routes_bgp_analysis(&handle)), + Some("suggest") => match *req.method() { + Method::GET => render_json_res(req.state().read().await.ca_routes_bgp_suggest(&handle, None)), + Method::POST => { + let server = req.state().clone(); + match req.json().await { + Err(e) => render_error(e), + Ok(resources) => { + render_json_res(server.read().await.ca_routes_bgp_suggest(&handle, Some(resources))) + } + } + } + _ => render_unknown_method(), + }, _ => render_unknown_method(), } } diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index 9f8fcb69..d989d0bd 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -14,10 +14,10 @@ use crate::commons::api::{ AddChildRequest, AllCertAuthIssues, CaCommandDetails, CaRepoDetails, CertAuthInfo, CertAuthInit, CertAuthIssues, CertAuthList, CertAuthStats, ChildCaInfo, ChildHandle, CommandHistory, CommandHistoryCriteria, CurrentRepoState, Handle, ListReply, ParentCaContact, ParentCaReq, ParentHandle, PublishDelta, PublisherDetails, PublisherHandle, - RepoInfo, RepositoryContact, RepositoryUpdate, RoaDefinition, RoaDefinitionUpdates, ServerInfo, TaCertDetails, - UpdateChildRequest, + RepoInfo, RepositoryContact, RepositoryUpdate, ResourceSet, RoaDefinition, RoaDefinitionUpdates, ServerInfo, + TaCertDetails, UpdateChildRequest, }; -use crate::commons::bgp::{BgpAnalyser, BgpAnalysisReport}; +use crate::commons::bgp::{BgpAnalyser, BgpAnalysisReport, BgpAnalysisSuggestion}; use crate::commons::error::Error; use crate::commons::eventsourcing::CommandKey; use crate::commons::remote::rfc8183; @@ -621,6 +621,22 @@ impl KrillServer { let resources = ca.all_resources(); Ok(self.bgp_analyser.analyse(definitions.as_slice(), &resources)) } + + pub fn ca_routes_bgp_suggest( + &self, + handle: &Handle, + scope: Option, + ) -> KrillResult { + let ca = self.caserver.get_ca(handle)?; + let definitions = ca.roa_definitions(); + let mut resources = ca.all_resources(); + + if let Some(scope) = scope { + resources = resources.intersection(&scope); + } + + Ok(self.bgp_analyser.suggest(definitions.as_slice(), &resources)) + } } /// # Handle publication requests diff --git a/src/test.rs b/src/test.rs index 49b130a0..8e6996ff 100644 --- a/src/test.rs +++ b/src/test.rs @@ -25,11 +25,11 @@ use crate::commons::api::{ ResourceClassKeysInfo, ResourceClassName, ResourceSet, RoaDefinition, RoaDefinitionUpdates, TypedPrefix, UpdateChildRequest, }; -use crate::commons::bgp::Announcement; +use crate::commons::bgp::{Announcement, BgpAnalysisSuggestion}; use crate::commons::remote::rfc8183; use crate::commons::remote::rfc8183::ChildRequest; use crate::commons::util::httpclient; -use crate::constants::KRILL_ENV_TEST_UNIT_DATA; +use crate::constants::{KRILL_ENV_TEST_ANN, KRILL_ENV_TEST_UNIT_DATA}; use crate::daemon::ca::{ta_handle, ResourceTaggedAttestation, RtaRequest, SignSupport}; use crate::daemon::http::server; @@ -62,6 +62,7 @@ pub async fn start_krill() -> PathBuf { let data_dir = sub_dir(&dir); env::set_var(KRILL_ENV_TEST_UNIT_DATA, data_dir.to_string_lossy().to_string()); + env::set_var(KRILL_ENV_TEST_ANN, "1"); tokio::spawn(server::start()); @@ -93,6 +94,7 @@ pub async fn init_child(handle: &Handle) { krill_admin(Command::CertAuth(CaCommand::Init(CertAuthInit::new(handle.clone())))).await; } +// We use embedded when not testing RFC 8181 - so that the CMS signing/verification overhead can be reduced. pub async fn init_child_with_embedded_repo(handle: &Handle) { krill_admin(Command::CertAuth(CaCommand::Init(CertAuthInit::new(handle.clone())))).await; krill_admin(Command::CertAuth(CaCommand::RepoUpdate( @@ -125,6 +127,7 @@ pub async fn child_request(handle: &Handle) -> rfc8183::ChildRequest { } } +// We use embedded when not testing RFC 6492 - so that the CMS signing/verification overhead can be reduced. pub async fn add_child_to_ta_embedded(handle: &Handle, resources: ResourceSet) -> ParentCaContact { let auth = ChildAuthRequest::Embedded; let req = AddChildRequest::new(handle.clone(), resources, auth); @@ -236,6 +239,13 @@ pub async fn ca_route_authorizations_update_expect_error(handle: &Handle, update .await; } +pub async fn ca_route_authorizations_suggestions(handle: &Handle) -> BgpAnalysisSuggestion { + match krill_admin(Command::CertAuth(CaCommand::BgpAnalysisSuggest(handle.clone(), None))).await { + ApiResponse::BgpAnalysisSuggestions(suggestion) => suggestion, + _ => panic!("Expected ROA suggestion"), + } +} + pub async fn ca_details(handle: &Handle) -> CertAuthInfo { match krill_admin(Command::CertAuth(CaCommand::Show(handle.clone()))).await { ApiResponse::CertAuthInfo(inf) => inf, diff --git a/test-resources/bgp/expected_suggestion_all_roas.json b/test-resources/bgp/expected_suggestion_all_roas.json new file mode 100644 index 00000000..76997f64 --- /dev/null +++ b/test-resources/bgp/expected_suggestion_all_roas.json @@ -0,0 +1,63 @@ +{ + "stale": [ + { + "asn": 64497, + "prefix": "10.0.3.0/24" + }, + { + "asn": 0, + "prefix": "10.0.4.0/24" + } + ], + "not_found": [ + { + "asn": 64497, + "prefix": "10.0.0.0/21" + }, + { + "asn": 64496, + "prefix": "192.168.0.0/24" + }, + { + "asn": 64497, + "prefix": "192.168.0.0/24" + } + ], + "invalid_asn": [ + { + "asn": 64497, + "prefix": "10.0.0.0/22" + } + ], + "invalid_length": [ + { + "asn": 64496, + "prefix": "10.0.0.0/24" + } + ], + "too_permissive": [ + { + "current": { + "asn": 64496, + "prefix": "10.0.0.0/22", + "max_length": 23 + }, + "new": [ + { + "asn": 64496, + "prefix": "10.0.0.0/22" + }, + { + "asn": 64496, + "prefix": "10.0.2.0/23" + } + ] + } + ], + "keep": [ + { + "asn": 64497, + "prefix": "192.168.1.0/24" + } + ] +} \ No newline at end of file diff --git a/test-resources/bgp/expected_suggestion_some_roas.json b/test-resources/bgp/expected_suggestion_some_roas.json new file mode 100644 index 00000000..07a11d6b --- /dev/null +++ b/test-resources/bgp/expected_suggestion_some_roas.json @@ -0,0 +1,41 @@ +{ + "stale": [ + { + "asn": 64497, + "prefix": "10.0.3.0/24" + } + ], + "not_found": [], + "invalid_asn": [ + { + "asn": 64497, + "prefix": "10.0.0.0/22" + } + ], + "invalid_length": [ + { + "asn": 64496, + "prefix": "10.0.0.0/24" + } + ], + "too_permissive": [ + { + "current": { + "asn": 64496, + "prefix": "10.0.0.0/22", + "max_length": 23 + }, + "new": [ + { + "asn": 64496, + "prefix": "10.0.0.0/22" + }, + { + "asn": 64496, + "prefix": "10.0.2.0/23" + } + ] + } + ], + "keep": [] +} \ No newline at end of file diff --git a/tests/ca_roas.rs b/tests/ca_roas.rs index 22237d99..cf24e552 100644 --- a/tests/ca_roas.rs +++ b/tests/ca_roas.rs @@ -129,5 +129,17 @@ async fn ca_roas() { ca_route_authorizations_update(&child, updates).await; assert!(will_publish_objects(&child, &[crl_file, mft_file, route1_file],).await); + // Get ROA suggestions, expect that we can submit the suggestions as an update, and then we + // should see no more suggestions. + let suggestion = ca_route_authorizations_suggestions(&child).await; + let updates: RoaDefinitionUpdates = suggestion.into(); + assert!(!updates.is_empty()); + + ca_route_authorizations_update(&child, updates).await; + + let remaining_suggestion = ca_route_authorizations_suggestions(&child).await; + let updates: RoaDefinitionUpdates = remaining_suggestion.into(); + assert!(updates.is_empty()); + let _ = fs::remove_dir_all(dir); }