1 use std::{cmp::Ordering, collections::HashMap, fmt};
2 
3 use crate::commons::{
4     api::{BgpStats, RoaDefinition, RoaDefinitionUpdates},
5     bgp::Announcement,
6 };
7 
8 //------------ BgpAnalysisAdvice -------------------------------------------
9 
10 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
11 pub struct BgpAnalysisAdvice {
12     effect: BgpAnalysisReport,
13     suggestion: BgpAnalysisSuggestion,
14 }
15 
16 impl BgpAnalysisAdvice {
new(effect: BgpAnalysisReport, suggestion: BgpAnalysisSuggestion) -> Self17     pub fn new(effect: BgpAnalysisReport, suggestion: BgpAnalysisSuggestion) -> Self {
18         BgpAnalysisAdvice { effect, suggestion }
19     }
20 
effect(&self) -> &BgpAnalysisReport21     pub fn effect(&self) -> &BgpAnalysisReport {
22         &self.effect
23     }
24 
suggestion(&self) -> &BgpAnalysisSuggestion25     pub fn suggestion(&self) -> &BgpAnalysisSuggestion {
26         &self.suggestion
27     }
28 }
29 
30 impl fmt::Display for BgpAnalysisAdvice {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result31     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32         writeln!(f, "Unsafe update, please review")?;
33         writeln!(f)?;
34         writeln!(f, "Effect would leave the following invalids:")?;
35 
36         let invalid_asns = self.effect().matching_defs(BgpAnalysisState::AnnouncementInvalidAsn);
37         if !invalid_asns.is_empty() {
38             writeln!(f)?;
39             writeln!(f, "  Announcements from invalid ASNs:")?;
40             for invalid in invalid_asns {
41                 writeln!(f, "    {}\n", invalid)?;
42             }
43         }
44 
45         let invalid_length = self.effect().matching_defs(BgpAnalysisState::AnnouncementInvalidLength);
46         if !invalid_length.is_empty() {
47             writeln!(f)?;
48             writeln!(f, "  Announcements too specific for their ASNs:\n")?;
49             for invalid in invalid_length {
50                 writeln!(f, "    {}", invalid)?;
51             }
52         }
53 
54         writeln!(f)?;
55         writeln!(f, "You may want to consider this alternative:")?;
56         writeln!(f, "{}", self.suggestion())?;
57 
58         Ok(())
59     }
60 }
61 
62 //------------ BgpAnalysisSuggestion ---------------------------------------
63 
64 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
65 pub struct BgpAnalysisSuggestion {
66     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
67     stale: Vec<RoaDefinition>,
68 
69     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
70     not_found: Vec<Announcement>,
71 
72     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
73     invalid_asn: Vec<Announcement>,
74 
75     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
76     invalid_length: Vec<Announcement>,
77 
78     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
79     too_permissive: Vec<ReplacementRoaSuggestion>,
80 
81     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
82     disallowing: Vec<RoaDefinition>,
83 
84     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
85     redundant: Vec<RoaDefinition>,
86 
87     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
88     not_held: Vec<RoaDefinition>,
89 
90     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
91     as0_redundant: Vec<RoaDefinition>,
92 
93     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
94     keep: Vec<RoaDefinition>,
95 
96     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
97     keep_disallowing: Vec<Announcement>,
98 }
99 
100 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
101 pub struct ReplacementRoaSuggestion {
102     current: RoaDefinition,
103     new: Vec<RoaDefinition>,
104 }
105 
106 impl From<BgpAnalysisSuggestion> for RoaDefinitionUpdates {
from(suggestion: BgpAnalysisSuggestion) -> Self107     fn from(suggestion: BgpAnalysisSuggestion) -> Self {
108         let (stale, not_found, invalid_asn, invalid_length, too_permissive, as0_redundant, redundant) = (
109             suggestion.stale,
110             suggestion.not_found,
111             suggestion.invalid_asn,
112             suggestion.invalid_length,
113             suggestion.too_permissive,
114             suggestion.as0_redundant,
115             suggestion.redundant,
116         );
117 
118         let mut added: Vec<RoaDefinition> = vec![];
119         let mut removed: Vec<RoaDefinition> = vec![];
120 
121         for auth in not_found
122             .into_iter()
123             .chain(invalid_asn.into_iter())
124             .chain(invalid_length.into_iter())
125         {
126             added.push(auth.into());
127         }
128 
129         for auth in stale.into_iter() {
130             removed.push(auth);
131         }
132 
133         for suggestion in too_permissive.into_iter() {
134             removed.push(suggestion.current);
135             for auth in suggestion.new.into_iter() {
136                 added.push(auth);
137             }
138         }
139 
140         for auth in as0_redundant.into_iter() {
141             removed.push(auth);
142         }
143 
144         for auth in redundant.into_iter() {
145             removed.push(auth);
146         }
147 
148         RoaDefinitionUpdates::new(added, removed)
149     }
150 }
151 
152 impl Default for BgpAnalysisSuggestion {
default() -> Self153     fn default() -> Self {
154         BgpAnalysisSuggestion {
155             stale: vec![],
156             not_found: vec![],
157             invalid_asn: vec![],
158             invalid_length: vec![],
159             too_permissive: vec![],
160             disallowing: vec![],
161             redundant: vec![],
162             not_held: vec![],
163             keep: vec![],
164             as0_redundant: vec![],
165             keep_disallowing: vec![],
166         }
167     }
168 }
169 
170 impl BgpAnalysisSuggestion {
add_stale(&mut self, authorization: RoaDefinition)171     pub fn add_stale(&mut self, authorization: RoaDefinition) {
172         self.stale.push(authorization);
173     }
174 
add_too_permissive(&mut self, current: RoaDefinition, new: Vec<RoaDefinition>)175     pub fn add_too_permissive(&mut self, current: RoaDefinition, new: Vec<RoaDefinition>) {
176         let replacement = ReplacementRoaSuggestion { current, new };
177         self.too_permissive.push(replacement);
178     }
179 
add_not_found(&mut self, announcement: Announcement)180     pub fn add_not_found(&mut self, announcement: Announcement) {
181         self.not_found.push(announcement);
182     }
183 
add_invalid_asn(&mut self, announcement: Announcement)184     pub fn add_invalid_asn(&mut self, announcement: Announcement) {
185         self.invalid_asn.push(announcement);
186     }
187 
add_invalid_length(&mut self, announcement: Announcement)188     pub fn add_invalid_length(&mut self, announcement: Announcement) {
189         self.invalid_length.push(announcement);
190     }
191 
add_disallowing(&mut self, authorization: RoaDefinition)192     pub fn add_disallowing(&mut self, authorization: RoaDefinition) {
193         self.disallowing.push(authorization);
194     }
195 
add_redundant(&mut self, authorization: RoaDefinition)196     pub fn add_redundant(&mut self, authorization: RoaDefinition) {
197         self.redundant.push(authorization);
198     }
199 
add_not_held(&mut self, authorization: RoaDefinition)200     pub fn add_not_held(&mut self, authorization: RoaDefinition) {
201         self.not_held.push(authorization);
202     }
203 
add_as0_redundant(&mut self, authorization: RoaDefinition)204     pub fn add_as0_redundant(&mut self, authorization: RoaDefinition) {
205         self.as0_redundant.push(authorization);
206     }
207 
add_keep(&mut self, authorization: RoaDefinition)208     pub fn add_keep(&mut self, authorization: RoaDefinition) {
209         self.keep.push(authorization);
210     }
211 
add_keep_disallowing(&mut self, announcement: Announcement)212     pub fn add_keep_disallowing(&mut self, announcement: Announcement) {
213         self.keep_disallowing.push(announcement);
214     }
215 }
216 
217 #[allow(clippy::cognitive_complexity)]
218 impl fmt::Display for BgpAnalysisSuggestion {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result219     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220         if !self.stale.is_empty() {
221             writeln!(f, "Remove the following stale entries:")?;
222             for auth in &self.stale {
223                 writeln!(f, "  {}", auth)?;
224             }
225             writeln!(f)?;
226         }
227 
228         if !self.too_permissive.is_empty() {
229             writeln!(f, "Replace the following too permissive entries:")?;
230             for entry in &self.too_permissive {
231                 writeln!(f, "  Remove: {}", entry.current)?;
232                 for replace in &entry.new {
233                     writeln!(f, "  Add: {}", replace)?;
234                 }
235                 writeln!(f)?;
236             }
237         }
238 
239         if !self.as0_redundant.is_empty() {
240             writeln!(
241                 f,
242                 "Remove the following AS0 ROAs made redundant by ROAs for the same prefix and a real ASN:"
243             )?;
244             for auth in &self.as0_redundant {
245                 writeln!(f, "  {}", auth)?;
246             }
247             writeln!(f)?;
248         }
249 
250         if !self.redundant.is_empty() {
251             writeln!(
252                 f,
253                 "Remove the following ROAs made redundant by a covering ROA using max length:"
254             )?;
255             for auth in &self.redundant {
256                 writeln!(f, "  {}", auth)?;
257             }
258             writeln!(f)?;
259         }
260 
261         if !self.disallowing.is_empty() {
262             writeln!(
263                 f,
264                 "Remove the following ROAs which only disallow announcements (did you use the wrong ASN?), if this is intended you may want to use AS0 instead:"
265             )?;
266             for auth in &self.disallowing {
267                 writeln!(f, "  {}", auth)?;
268             }
269             writeln!(f)?;
270         }
271 
272         if !self.keep.is_empty() {
273             writeln!(f, "Keep the following authorizations:")?;
274             for auth in &self.keep {
275                 writeln!(f, "  {}", auth)?;
276             }
277             writeln!(f)?;
278         }
279 
280         if !self.not_found.is_empty() {
281             writeln!(f, "Authorize these announcements which are currently not covered:")?;
282             for auth in &self.not_found {
283                 writeln!(f, "  {}", auth)?;
284             }
285             writeln!(f)?;
286         }
287 
288         if !self.invalid_length.is_empty() {
289             writeln!(
290                 f,
291                 "Authorize these announcements which are currently invalid because they are too specific:"
292             )?;
293             for auth in &self.invalid_length {
294                 writeln!(f, "  {}", auth)?;
295             }
296             writeln!(f)?;
297         }
298 
299         if !self.invalid_asn.is_empty() {
300             writeln!(
301                 f,
302                 "Authorize these announcements which are currently invalid because they are not allowed for these ASNs:"
303             )?;
304             for auth in &self.invalid_asn {
305                 writeln!(f, "  {}", auth)?;
306             }
307             writeln!(f)?;
308         }
309 
310         Ok(())
311     }
312 }
313 
314 //------------ BgpAnalysisReport -------------------------------------------
315 
316 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
317 pub struct BgpAnalysisReport(Vec<BgpAnalysisEntry>);
318 
319 impl BgpAnalysisReport {
new(mut roas: Vec<BgpAnalysisEntry>) -> Self320     pub fn new(mut roas: Vec<BgpAnalysisEntry>) -> Self {
321         roas.sort();
322         BgpAnalysisReport(roas)
323     }
324 
entries(&self) -> &Vec<BgpAnalysisEntry>325     pub fn entries(&self) -> &Vec<BgpAnalysisEntry> {
326         &self.0
327     }
328 
into_entries(self) -> Vec<BgpAnalysisEntry>329     pub fn into_entries(self) -> Vec<BgpAnalysisEntry> {
330         self.0
331     }
332 
matching_defs(&self, state: BgpAnalysisState) -> Vec<&RoaDefinition>333     pub fn matching_defs(&self, state: BgpAnalysisState) -> Vec<&RoaDefinition> {
334         self.matching_entries(state)
335             .into_iter()
336             .map(|e| &e.definition)
337             .collect()
338     }
339 
matching_entries(&self, state: BgpAnalysisState) -> Vec<&BgpAnalysisEntry>340     pub fn matching_entries(&self, state: BgpAnalysisState) -> Vec<&BgpAnalysisEntry> {
341         self.0.iter().filter(|e| e.state == state).collect()
342     }
343 
contains_invalids(&self) -> bool344     pub fn contains_invalids(&self) -> bool {
345         self.0.iter().any(|el| el.state().is_invalid())
346     }
347 }
348 
349 impl From<BgpAnalysisReport> for BgpStats {
from(r: BgpAnalysisReport) -> BgpStats350     fn from(r: BgpAnalysisReport) -> BgpStats {
351         let mut stats = BgpStats::default();
352         for e in r.0.iter() {
353             match e.state {
354                 BgpAnalysisState::AnnouncementValid => stats.increment_valid(),
355                 BgpAnalysisState::AnnouncementInvalidAsn => stats.increment_invalid_asn(),
356                 BgpAnalysisState::AnnouncementInvalidLength => stats.increment_invalid_length(),
357                 BgpAnalysisState::AnnouncementDisallowed => stats.increment_disallowed(),
358                 BgpAnalysisState::AnnouncementNotFound => stats.increment_not_found(),
359                 BgpAnalysisState::RoaUnseen => {
360                     stats.increment_roas_total();
361                     stats.increment_roas_stale();
362                 }
363                 BgpAnalysisState::RoaTooPermissive => {
364                     stats.increment_roas_total();
365                     stats.increment_roas_too_permissive();
366                 }
367                 BgpAnalysisState::RoaRedundant | BgpAnalysisState::RoaAs0Redundant => {
368                     stats.increment_roas_total();
369                     stats.increment_roas_redundant();
370                 }
371                 BgpAnalysisState::RoaNotHeld => {
372                     stats.increment_roas_total();
373                     stats.increment_roas_not_held();
374                 }
375                 BgpAnalysisState::RoaDisallowing => {
376                     stats.increment_roas_total();
377                     stats.increment_roas_disallowing();
378                 }
379                 BgpAnalysisState::RoaAs0 | BgpAnalysisState::RoaNoAnnouncementInfo | BgpAnalysisState::RoaSeen => {
380                     stats.increment_roas_total()
381                 }
382             }
383         }
384         stats
385     }
386 }
387 
388 #[allow(clippy::cognitive_complexity)]
389 impl fmt::Display for BgpAnalysisReport {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result390     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
391         let entries = self.entries();
392 
393         let mut entry_map: HashMap<BgpAnalysisState, Vec<&BgpAnalysisEntry>> = HashMap::new();
394         for entry in entries.iter() {
395             let state = entry.state();
396             entry_map.entry(state).or_insert_with(Vec::new);
397             entry_map.get_mut(&state).unwrap().push(entry);
398         }
399 
400         if entry_map.contains_key(&BgpAnalysisState::RoaNoAnnouncementInfo) {
401             write!(f, "no BGP announcements known")
402         } else {
403             if let Some(authorizing) = entry_map.get(&BgpAnalysisState::RoaSeen) {
404                 writeln!(f, "Authorizations covering announcements seen:")?;
405                 for roa in authorizing {
406                     writeln!(f)?;
407                     writeln!(f, "\tDefinition: {}", roa.definition)?;
408                     writeln!(f)?;
409                     writeln!(f, "\t\tAuthorizes:")?;
410                     for ann in roa.authorizes.iter() {
411                         writeln!(f, "\t\t{}", ann)?;
412                     }
413 
414                     if !roa.disallows.is_empty() {
415                         writeln!(f)?;
416                         writeln!(f, "\t\tDisallows:")?;
417                         for ann in roa.disallows.iter() {
418                             writeln!(f, "\t\t{}", ann)?;
419                         }
420                     }
421                 }
422                 writeln!(f)?;
423             }
424 
425             if let Some(redundant) = entry_map.get(&BgpAnalysisState::RoaRedundant) {
426                 writeln!(
427                     f,
428                     "Authorizations which are *redundant* - they are already included in full by other authorizations:"
429                 )?;
430                 for roa in redundant {
431                     writeln!(f)?;
432                     writeln!(f, "\tDefinition: {}", roa.definition)?;
433                     writeln!(f)?;
434                     writeln!(f, "\t\tAuthorizes:")?;
435                     for ann in roa.authorizes.iter() {
436                         writeln!(f, "\t\t{}", ann)?;
437                     }
438 
439                     if !roa.disallows.is_empty() {
440                         writeln!(f)?;
441                         writeln!(f, "\t\tDisallows:")?;
442                         for ann in roa.disallows.iter() {
443                             writeln!(f, "\t\t{}", ann)?;
444                         }
445                     }
446 
447                     writeln!(f)?;
448                     writeln!(f, "\t\tMade redundant by:")?;
449                     for redundant_by in roa.made_redundant_by.iter() {
450                         writeln!(f, "\t\t{}", redundant_by)?;
451                     }
452                 }
453                 writeln!(f)?;
454             }
455 
456             if let Some(not_seen) = entry_map.get(&BgpAnalysisState::RoaUnseen) {
457                 writeln!(
458                     f,
459                     "Authorizations for which no announcements are seen (you may wish to remove these):"
460                 )?;
461                 writeln!(f)?;
462                 for roa in not_seen {
463                     writeln!(f, "\tDefinition: {}", roa.definition)?;
464                 }
465                 writeln!(f)?;
466             }
467 
468             if let Some(not_held) = entry_map.get(&BgpAnalysisState::RoaNotHeld) {
469                 writeln!(
470                     f,
471                     "Authorizations for which no ROAs can be made - you do not have the prefix on your certificate(s):"
472                 )?;
473                 writeln!(f)?;
474                 for roa in not_held {
475                     writeln!(f, "\tDefinition: {}", roa.definition)?;
476                 }
477                 writeln!(f)?;
478             }
479 
480             if let Some(disallowing) = entry_map.get(&BgpAnalysisState::RoaDisallowing) {
481                 writeln!(
482                     f,
483                     "Authorizations disallowing announcements seen. You may want to use AS0 ROAs instead:"
484                 )?;
485                 for roa in disallowing {
486                     writeln!(f)?;
487                     writeln!(f, "\tDefinition: {}", roa.definition)?;
488                     writeln!(f)?;
489                     writeln!(f)?;
490                     writeln!(f, "\t\tDisallows:")?;
491                     for ann in roa.disallows.iter() {
492                         writeln!(f, "\t\t{}", ann)?;
493                     }
494                 }
495                 writeln!(f)?;
496             }
497 
498             if let Some(too_permissive) = entry_map.get(&BgpAnalysisState::RoaTooPermissive) {
499                 writeln!(f, "Authorizations which may be too permissive:")?;
500 
501                 for roa in too_permissive {
502                     writeln!(f)?;
503                     writeln!(f, "\tDefinition: {}", roa.definition)?;
504                     writeln!(f)?;
505                     writeln!(f, "\t\tAuthorizes visible announcements:")?;
506                     for ann in roa.authorizes.iter() {
507                         writeln!(f, "\t\t{}", ann)?;
508                     }
509 
510                     if !roa.disallows.is_empty() {
511                         writeln!(f)?;
512                         writeln!(f, "\t\tDisallows:")?;
513                         for ann in roa.disallows.iter() {
514                             writeln!(f, "\t\t{}", ann)?;
515                         }
516                     }
517                 }
518                 writeln!(f)?;
519             }
520 
521             if let Some(as0) = entry_map.get(&BgpAnalysisState::RoaAs0) {
522                 writeln!(f, "AS0 Authorizations disallowing announcements for prefixes")?;
523                 writeln!(f)?;
524                 for roa in as0 {
525                     writeln!(f, "\tDefinition: {}", roa.definition)?;
526                 }
527                 writeln!(f)?;
528             }
529 
530             if let Some(as0_redundant) = entry_map.get(&BgpAnalysisState::RoaAs0Redundant) {
531                 writeln!(
532                     f,
533                     "AS0 Authorization which are made redundant by authorizations for the prefix from real ASNs"
534                 )?;
535                 writeln!(f)?;
536                 for roa in as0_redundant {
537                     writeln!(f, "\tDefinition: {}", roa.definition)?;
538                     writeln!(f)?;
539                     writeln!(f, "\t\tMade redundant by:")?;
540                     for redundant_by in &roa.made_redundant_by {
541                         writeln!(f, "\t\t{}", redundant_by)?;
542                     }
543                     writeln!(f)?;
544                 }
545             }
546 
547             if let Some(valid) = entry_map.get(&BgpAnalysisState::AnnouncementValid) {
548                 writeln!(f, "Announcements which are valid:")?;
549                 writeln!(f)?;
550                 for ann in valid {
551                     writeln!(f, "\tAnnouncement: {}", ann.definition)?;
552                 }
553                 writeln!(f)?;
554             }
555 
556             if let Some(invalid_length) = entry_map.get(&BgpAnalysisState::AnnouncementInvalidLength) {
557                 writeln!(
558                     f,
559                     "Announcements from an authorized ASN, which are too specific (not allowed by max length):"
560                 )?;
561                 for ann in invalid_length {
562                     writeln!(f)?;
563                     writeln!(f, "\tAnnouncement: {}", ann.definition)?;
564                     writeln!(f)?;
565                     writeln!(f, "\t\tDisallowed by authorization(s):")?;
566                     for roa in ann.disallowed_by.iter() {
567                         writeln!(f, "\t\t{}", roa)?;
568                     }
569                 }
570                 writeln!(f)?;
571             }
572 
573             if let Some(invalid_asn) = entry_map.get(&BgpAnalysisState::AnnouncementInvalidAsn) {
574                 writeln!(f, "Announcements from an unauthorized ASN:")?;
575                 for ann in invalid_asn {
576                     writeln!(f)?;
577                     writeln!(f, "\tAnnouncement: {}", ann.definition)?;
578                     writeln!(f)?;
579                     writeln!(f, "\t\tDisallowed by authorization(s):")?;
580                     for roa in ann.disallowed_by.iter() {
581                         writeln!(f, "\t\t{}", roa)?;
582                     }
583                 }
584                 writeln!(f)?;
585             }
586 
587             if let Some(disallowed) = entry_map.get(&BgpAnalysisState::AnnouncementDisallowed) {
588                 writeln!(f, "Announcements disallowed by 'AS0' ROAs:")?;
589                 writeln!(f)?;
590                 for ann in disallowed {
591                     writeln!(f, "\tAnnouncement: {}", ann.definition)?;
592                 }
593                 writeln!(f)?;
594             }
595 
596             if let Some(not_found) = entry_map.get(&BgpAnalysisState::AnnouncementNotFound) {
597                 writeln!(
598                     f,
599                     "Announcements which are 'not found' (not covered by any of your authorizations):"
600                 )?;
601                 writeln!(f)?;
602                 for ann in not_found {
603                     writeln!(f, "\tAnnouncement: {}", ann.definition)?;
604                 }
605                 writeln!(f)?;
606             }
607 
608             Ok(())
609         }
610     }
611 }
612 
613 //------------ BgpAnalysisEntry --------------------------------------------
614 
615 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
616 pub struct BgpAnalysisEntry {
617     #[serde(flatten)]
618     definition: RoaDefinition,
619     state: BgpAnalysisState,
620     #[serde(skip_serializing_if = "Option::is_none")]
621     allowed_by: Option<RoaDefinition>,
622     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
623     disallowed_by: Vec<RoaDefinition>,
624     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
625     made_redundant_by: Vec<RoaDefinition>,
626     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
627     authorizes: Vec<Announcement>,
628     #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
629     disallows: Vec<Announcement>,
630 }
631 
632 impl BgpAnalysisEntry {
definition(&self) -> &RoaDefinition633     pub fn definition(&self) -> &RoaDefinition {
634         &self.definition
635     }
636 
announcement(&self) -> Announcement637     pub fn announcement(&self) -> Announcement {
638         self.definition.into()
639     }
640 
state(&self) -> BgpAnalysisState641     pub fn state(&self) -> BgpAnalysisState {
642         self.state
643     }
644 
allowed_by(&self) -> Option<&RoaDefinition>645     pub fn allowed_by(&self) -> Option<&RoaDefinition> {
646         self.allowed_by.as_ref()
647     }
648 
disallowed_by(&self) -> &Vec<RoaDefinition>649     pub fn disallowed_by(&self) -> &Vec<RoaDefinition> {
650         &self.disallowed_by
651     }
652 
made_redundant_by(&self) -> &Vec<RoaDefinition>653     pub fn made_redundant_by(&self) -> &Vec<RoaDefinition> {
654         &self.made_redundant_by
655     }
656 
authorizes(&self) -> &Vec<Announcement>657     pub fn authorizes(&self) -> &Vec<Announcement> {
658         &self.authorizes
659     }
660 
disallows(&self) -> &Vec<Announcement>661     pub fn disallows(&self) -> &Vec<Announcement> {
662         &self.disallows
663     }
664 
roa_seen( definition: RoaDefinition, mut authorizes: Vec<Announcement>, mut disallows: Vec<Announcement>, ) -> Self665     pub fn roa_seen(
666         definition: RoaDefinition,
667         mut authorizes: Vec<Announcement>,
668         mut disallows: Vec<Announcement>,
669     ) -> Self {
670         authorizes.sort();
671         disallows.sort();
672         BgpAnalysisEntry {
673             definition,
674             state: BgpAnalysisState::RoaSeen,
675             allowed_by: None,
676             disallowed_by: vec![],
677             made_redundant_by: vec![],
678             authorizes,
679             disallows,
680         }
681     }
682 
roa_disallowing(definition: RoaDefinition, mut disallows: Vec<Announcement>) -> Self683     pub fn roa_disallowing(definition: RoaDefinition, mut disallows: Vec<Announcement>) -> Self {
684         disallows.sort();
685         BgpAnalysisEntry {
686             definition,
687             state: BgpAnalysisState::RoaDisallowing,
688             allowed_by: None,
689             disallowed_by: vec![],
690             made_redundant_by: vec![],
691             authorizes: vec![],
692             disallows,
693         }
694     }
695 
roa_as0(definition: RoaDefinition, mut disallows: Vec<Announcement>) -> Self696     pub fn roa_as0(definition: RoaDefinition, mut disallows: Vec<Announcement>) -> Self {
697         disallows.sort();
698         BgpAnalysisEntry {
699             definition,
700             state: BgpAnalysisState::RoaAs0,
701             allowed_by: None,
702             disallowed_by: vec![],
703             made_redundant_by: vec![],
704             authorizes: vec![],
705             disallows,
706         }
707     }
708 
roa_as0_redundant(definition: RoaDefinition, mut made_redundant_by: Vec<RoaDefinition>) -> Self709     pub fn roa_as0_redundant(definition: RoaDefinition, mut made_redundant_by: Vec<RoaDefinition>) -> Self {
710         made_redundant_by.sort();
711         BgpAnalysisEntry {
712             definition,
713             state: BgpAnalysisState::RoaAs0Redundant,
714             allowed_by: None,
715             disallowed_by: vec![],
716             made_redundant_by,
717             authorizes: vec![],
718             disallows: vec![],
719         }
720     }
721 
roa_redundant( definition: RoaDefinition, mut authorizes: Vec<Announcement>, mut disallows: Vec<Announcement>, mut made_redundant_by: Vec<RoaDefinition>, ) -> Self722     pub fn roa_redundant(
723         definition: RoaDefinition,
724         mut authorizes: Vec<Announcement>,
725         mut disallows: Vec<Announcement>,
726         mut made_redundant_by: Vec<RoaDefinition>,
727     ) -> Self {
728         authorizes.sort();
729         disallows.sort();
730         made_redundant_by.sort();
731         BgpAnalysisEntry {
732             definition,
733             state: BgpAnalysisState::RoaRedundant,
734             allowed_by: None,
735             disallowed_by: vec![],
736             made_redundant_by,
737             authorizes,
738             disallows,
739         }
740     }
741 
roa_too_permissive( definition: RoaDefinition, mut authorizes: Vec<Announcement>, mut disallows: Vec<Announcement>, ) -> Self742     pub fn roa_too_permissive(
743         definition: RoaDefinition,
744         mut authorizes: Vec<Announcement>,
745         mut disallows: Vec<Announcement>,
746     ) -> Self {
747         authorizes.sort();
748         disallows.sort();
749         BgpAnalysisEntry {
750             definition,
751             state: BgpAnalysisState::RoaTooPermissive,
752             allowed_by: None,
753             disallowed_by: vec![],
754             made_redundant_by: vec![],
755             authorizes,
756             disallows,
757         }
758     }
759 
roa_unseen(definition: RoaDefinition) -> Self760     pub fn roa_unseen(definition: RoaDefinition) -> Self {
761         BgpAnalysisEntry {
762             definition,
763             state: BgpAnalysisState::RoaUnseen,
764             allowed_by: None,
765             disallowed_by: vec![],
766             made_redundant_by: vec![],
767             authorizes: vec![],
768             disallows: vec![],
769         }
770     }
771 
roa_not_held(definition: RoaDefinition) -> Self772     pub fn roa_not_held(definition: RoaDefinition) -> Self {
773         BgpAnalysisEntry {
774             definition,
775             state: BgpAnalysisState::RoaNotHeld,
776             allowed_by: None,
777             disallowed_by: vec![],
778             made_redundant_by: vec![],
779             authorizes: vec![],
780             disallows: vec![],
781         }
782     }
783 
roa_no_announcement_info(definition: RoaDefinition) -> Self784     pub fn roa_no_announcement_info(definition: RoaDefinition) -> Self {
785         BgpAnalysisEntry {
786             definition,
787             state: BgpAnalysisState::RoaNoAnnouncementInfo,
788             allowed_by: None,
789             disallowed_by: vec![],
790             made_redundant_by: vec![],
791             authorizes: vec![],
792             disallows: vec![],
793         }
794     }
795 
announcement_valid(announcement: Announcement, allowed_by: RoaDefinition) -> Self796     pub fn announcement_valid(announcement: Announcement, allowed_by: RoaDefinition) -> Self {
797         BgpAnalysisEntry {
798             definition: RoaDefinition::from(announcement),
799             state: BgpAnalysisState::AnnouncementValid,
800             allowed_by: Some(allowed_by),
801             disallowed_by: vec![],
802             made_redundant_by: vec![],
803             authorizes: vec![],
804             disallows: vec![],
805         }
806     }
807 
announcement_invalid_asn(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self808     pub fn announcement_invalid_asn(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self {
809         Self::announcement_invalid(announcement, BgpAnalysisState::AnnouncementInvalidAsn, disallowed_by)
810     }
811 
announcement_invalid_length(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self812     pub fn announcement_invalid_length(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self {
813         Self::announcement_invalid(announcement, BgpAnalysisState::AnnouncementInvalidLength, disallowed_by)
814     }
815 
announcement_disallowed(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self816     pub fn announcement_disallowed(announcement: Announcement, disallowed_by: Vec<RoaDefinition>) -> Self {
817         Self::announcement_invalid(announcement, BgpAnalysisState::AnnouncementDisallowed, disallowed_by)
818     }
819 
announcement_invalid( announcement: Announcement, state: BgpAnalysisState, mut disallowed_by: Vec<RoaDefinition>, ) -> Self820     fn announcement_invalid(
821         announcement: Announcement,
822         state: BgpAnalysisState,
823         mut disallowed_by: Vec<RoaDefinition>,
824     ) -> Self {
825         disallowed_by.sort();
826         BgpAnalysisEntry {
827             definition: RoaDefinition::from(announcement),
828             state,
829             allowed_by: None,
830             disallowed_by,
831             made_redundant_by: vec![],
832             authorizes: vec![],
833             disallows: vec![],
834         }
835     }
836 
announcement_not_found(announcement: Announcement) -> Self837     pub fn announcement_not_found(announcement: Announcement) -> Self {
838         BgpAnalysisEntry {
839             definition: RoaDefinition::from(announcement),
840             state: BgpAnalysisState::AnnouncementNotFound,
841             allowed_by: None,
842             disallowed_by: vec![],
843             made_redundant_by: vec![],
844             authorizes: vec![],
845             disallows: vec![],
846         }
847     }
848 }
849 
850 impl Ord for BgpAnalysisEntry {
cmp(&self, other: &Self) -> Ordering851     fn cmp(&self, other: &Self) -> Ordering {
852         let mut ordering = self.state.cmp(&other.state);
853         if ordering == Ordering::Equal {
854             ordering = self.definition.cmp(&other.definition);
855         }
856         ordering
857     }
858 }
859 
860 impl PartialOrd for BgpAnalysisEntry {
partial_cmp(&self, other: &Self) -> Option<Ordering>861     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
862         Some(self.cmp(other))
863     }
864 }
865 
866 //------------ BgpAnalysisState --------------------------------------------
867 
868 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize)]
869 #[serde(rename_all = "snake_case")]
870 pub enum BgpAnalysisState {
871     RoaSeen,
872     RoaRedundant,
873     RoaUnseen,
874     RoaDisallowing,
875     RoaTooPermissive,
876     RoaAs0,
877     RoaAs0Redundant,
878     RoaNotHeld,
879     AnnouncementValid,
880     AnnouncementInvalidLength,
881     AnnouncementInvalidAsn,
882     AnnouncementDisallowed,
883     AnnouncementNotFound,
884     RoaNoAnnouncementInfo,
885 }
886 
887 impl BgpAnalysisState {
is_invalid(self) -> bool888     pub fn is_invalid(self) -> bool {
889         matches!(
890             self,
891             BgpAnalysisState::AnnouncementInvalidAsn | BgpAnalysisState::AnnouncementInvalidLength
892         )
893     }
894 }
895 
896 //------------ Tests --------------------------------------------------------
897 
898 #[cfg(test)]
899 mod tests {
900     use super::*;
901 
902     #[test]
print_bgp_report_full()903     fn print_bgp_report_full() {
904         let json = include_str!("../../../test-resources/bgp/expected_full_report.json");
905         let report: BgpAnalysisReport = serde_json::from_str(json).unwrap();
906 
907         let expected = include_str!("../../../test-resources/bgp/expected_full_report.txt");
908 
909         assert_eq!(report.to_string(), expected);
910     }
911 }
912