monocle 1.2.0

A commandline application to search, parse, and process BGP information in public sources.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
//! Types for the inspect lens module
//!
//! This module defines all the types used by the inspect lens for unified
//! AS and prefix information queries.

use crate::database::{AsinfoCoreRecord, AsinfoFullRecord, RpkiRoaRecord};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

// Re-export connectivity types from the database module
pub use crate::database::{AsConnectivitySummary, ConnectivityEntry, ConnectivityGroup};

// =============================================================================
// Query Type Detection
// =============================================================================

/// Query type detected from input
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InspectQueryType {
    Asn,
    Prefix,
    Name,
}

impl std::fmt::Display for InspectQueryType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            InspectQueryType::Asn => write!(f, "asn"),
            InspectQueryType::Prefix => write!(f, "prefix"),
            InspectQueryType::Name => write!(f, "name"),
        }
    }
}

// =============================================================================
// Data Section Selection
// =============================================================================

/// Available data sections that can be selected via --show
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InspectDataSection {
    /// Basic AS information (name, country, org, peeringdb, hegemony, population)
    Basic,
    /// Announced prefixes (from pfx2as)
    Prefixes,
    /// AS connectivity (from as2rel)
    Connectivity,
    /// RPKI information (ROAs and ASPA)
    Rpki,
}

impl InspectDataSection {
    /// Get all available sections
    pub fn all() -> Vec<Self> {
        vec![Self::Basic, Self::Prefixes, Self::Connectivity, Self::Rpki]
    }

    /// Default sections for ASN queries (all sections)
    pub fn default_for_asn() -> Vec<Self> {
        Self::all()
    }

    /// Default sections for prefix queries (all sections)
    pub fn default_for_prefix() -> Vec<Self> {
        Self::all()
    }

    /// Default sections for name search (basic only for search results)
    pub fn default_for_name() -> Vec<Self> {
        vec![Self::Basic]
    }

    /// Parse from string
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "basic" => Some(Self::Basic),
            "prefixes" => Some(Self::Prefixes),
            "connectivity" => Some(Self::Connectivity),
            "rpki" => Some(Self::Rpki),
            _ => None,
        }
    }

    /// Get all section names as strings
    pub fn all_names() -> Vec<&'static str> {
        vec!["basic", "prefixes", "connectivity", "rpki"]
    }
}

impl std::fmt::Display for InspectDataSection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Basic => write!(f, "basic"),
            Self::Prefixes => write!(f, "prefixes"),
            Self::Connectivity => write!(f, "connectivity"),
            Self::Rpki => write!(f, "rpki"),
        }
    }
}

// =============================================================================
// Query Options
// =============================================================================

/// Options for controlling inspect query behavior
#[derive(Debug, Clone)]
pub struct InspectQueryOptions {
    /// Which data sections to include (None = defaults based on query type)
    pub select: Option<HashSet<InspectDataSection>>,

    /// Maximum ROAs to return (0 = unlimited)
    pub max_roas: usize,

    /// Maximum prefixes to return (0 = unlimited)
    pub max_prefixes: usize,

    /// Maximum neighbors per category (0 = unlimited)
    pub max_neighbors: usize,

    /// Maximum search results (0 = unlimited)
    pub max_search_results: usize,
}

impl Default for InspectQueryOptions {
    fn default() -> Self {
        Self {
            select: None, // Use defaults based on query type
            max_roas: 10,
            max_prefixes: 10,
            max_neighbors: 5,
            max_search_results: 20,
        }
    }
}

impl InspectQueryOptions {
    /// Create options for full output with no limits
    pub fn full() -> Self {
        Self {
            select: Some(InspectDataSection::all().into_iter().collect()),
            max_roas: 0,
            max_prefixes: 0,
            max_neighbors: 0,
            max_search_results: 0,
        }
    }

    /// Set specific sections to select
    pub fn with_select(mut self, sections: Vec<InspectDataSection>) -> Self {
        self.select = Some(sections.into_iter().collect());
        self
    }

    /// Check if a section should be included for the given query type
    pub fn should_include(
        &self,
        section: InspectDataSection,
        query_type: InspectQueryType,
    ) -> bool {
        match &self.select {
            Some(selected) => selected.contains(&section),
            None => {
                let defaults = match query_type {
                    InspectQueryType::Asn => InspectDataSection::default_for_asn(),
                    InspectQueryType::Prefix => InspectDataSection::default_for_prefix(),
                    InspectQueryType::Name => InspectDataSection::default_for_name(),
                };
                defaults.contains(&section)
            }
        }
    }
}

// =============================================================================
// RPKI Types
// =============================================================================

/// RPKI information for an ASN
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpkiAsnInfo {
    /// ROAs where this ASN is the origin
    pub roas: Option<RoaSummary>,

    /// ASPA record for this ASN (if exists)
    pub aspa: Option<AspaInfo>,
}

/// Summary of ROAs for an ASN
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoaSummary {
    /// Total ROA count for this ASN
    pub total_count: usize,

    /// IPv4 ROA count
    pub ipv4_count: usize,

    /// IPv6 ROA count
    pub ipv6_count: usize,

    /// ROA entries (limited by default, sorted by prefix)
    pub entries: Vec<RpkiRoaRecord>,

    /// Whether entries were truncated
    pub truncated: bool,
}

/// ASPA provider entry with ASN and name
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AspaProvider {
    pub asn: u32,
    pub name: Option<String>,
}

/// ASPA information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AspaInfo {
    pub customer_asn: u32,
    /// Customer AS name (enriched from asinfo)
    pub customer_name: Option<String>,
    /// Customer AS country (enriched from asinfo)
    pub customer_country: Option<String>,
    /// Provider ASNs with names (enriched from asinfo)
    pub providers: Vec<AspaProvider>,
}

/// RPKI information for a prefix
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpkiPrefixInfo {
    /// Covering ROAs (sorted by prefix, then max_length, then ASN)
    pub roas: Vec<RpkiRoaRecord>,

    /// ROA count
    pub roa_count: usize,

    /// Validation state (if single origin ASN known)
    pub validation_state: Option<String>,

    /// Whether ROAs were truncated
    pub truncated: bool,
}

// =============================================================================
// Connectivity Types
// =============================================================================

// Note: AsConnectivitySummary, ConnectivityGroup, and ConnectivityEntry
// are re-exported from crate::database::as2rel at the top of this file.

/// Connectivity section wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivitySection {
    pub summary: AsConnectivitySummary,

    /// Whether neighbor lists were truncated
    pub truncated: bool,
}

// =============================================================================
// Prefix Types
// =============================================================================

/// Prefix-to-AS mapping info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pfx2asInfo {
    pub prefix: String,
    pub origin_asns: Vec<u32>,
    /// "exact" or "longest"
    pub match_type: String,
    /// RPKI validation status for each origin ASN
    pub validations: Vec<String>,
}

/// Prefix information section (for prefix queries)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrefixSection {
    /// Prefix-to-AS mapping result
    pub pfx2as: Option<Pfx2asInfo>,

    /// RPKI information for this prefix
    pub rpki: Option<RpkiPrefixInfo>,
}

/// Announced prefixes section (for ASN queries with --select prefixes)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnnouncedPrefixesSection {
    /// Total prefix count
    pub total_count: usize,

    /// IPv4 count
    pub ipv4_count: usize,

    /// IPv6 count
    pub ipv6_count: usize,

    /// RPKI validation summary
    pub validation_summary: ValidationSummary,

    /// Prefix entries with validation status (sorted by validation then prefix)
    pub prefixes: Vec<PrefixEntry>,

    /// Whether prefixes were truncated
    pub truncated: bool,
}

/// A single prefix entry with origin ASN info and validation status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrefixEntry {
    pub prefix: String,
    pub origin_asn: u32,
    pub origin_name: Option<String>,
    pub origin_country: Option<String>,
    pub validation: String,
}

/// RPKI validation summary for prefixes
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationSummary {
    pub valid_count: usize,
    pub valid_percent: f64,
    pub invalid_count: usize,
    pub invalid_percent: f64,
    pub unknown_count: usize,
    pub unknown_percent: f64,
}

impl ValidationSummary {
    pub fn from_counts(valid: usize, invalid: usize, unknown: usize) -> Self {
        let total = valid + invalid + unknown;
        let total_f64 = total as f64;
        Self {
            valid_count: valid,
            valid_percent: if total > 0 {
                (valid as f64 / total_f64) * 100.0
            } else {
                0.0
            },
            invalid_count: invalid,
            invalid_percent: if total > 0 {
                (invalid as f64 / total_f64) * 100.0
            } else {
                0.0
            },
            unknown_count: unknown,
            unknown_percent: if total > 0 {
                (unknown as f64 / total_f64) * 100.0
            } else {
                0.0
            },
        }
    }
}

// =============================================================================
// ASInfo Section Types
// =============================================================================

/// Wrapper for ASInfo in results - distinguishes between direct query vs origin lookup
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsinfoSection {
    /// Full AS info for directly queried ASN (ASN queries)
    pub detail: Option<AsinfoFullRecord>,

    /// AS info for origin ASNs (prefix queries via pfx2as)
    pub origins: Option<Vec<AsinfoFullRecord>>,
}

// =============================================================================
// Search Results
// =============================================================================

/// Search results section (for name queries)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResultsSection {
    /// Total matches found
    pub total_matches: usize,

    /// Results (sorted by ASN, limited by default)
    pub results: Vec<AsinfoCoreRecord>,

    /// Whether results were truncated
    pub truncated: bool,
}

// =============================================================================
// Main Query Result Types
// =============================================================================

/// Result for a single query
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectQueryResult {
    /// Original query string
    pub query: String,

    /// Detected query type
    pub query_type: InspectQueryType,

    /// ASN information section
    /// - For ASN queries: contains `detail` (full record for queried ASN)
    /// - For prefix queries: contains `origins` (records for origin ASNs)
    pub asinfo: Option<AsinfoSection>,

    /// Prefix information (for prefix queries only)
    /// Contains pfx2as mapping and RPKI validation
    pub prefix: Option<PrefixSection>,

    /// Announced prefixes (for ASN queries with --select prefixes)
    pub prefixes: Option<AnnouncedPrefixesSection>,

    /// Connectivity information (for ASN queries)
    pub connectivity: Option<ConnectivitySection>,

    /// RPKI information (for ASN queries - ROAs originated, ASPA)
    pub rpki: Option<RpkiAsnInfo>,

    /// Search results (for name queries only)
    pub search_results: Option<SearchResultsSection>,
}

impl InspectQueryResult {
    /// Create a new empty result for an ASN query
    pub fn new_asn(query: String) -> Self {
        Self {
            query,
            query_type: InspectQueryType::Asn,
            asinfo: None,
            prefix: None,
            prefixes: None,
            connectivity: None,
            rpki: None,
            search_results: None,
        }
    }

    /// Create a new empty result for a prefix query
    pub fn new_prefix(query: String) -> Self {
        Self {
            query,
            query_type: InspectQueryType::Prefix,
            asinfo: None,
            prefix: None,
            prefixes: None,
            connectivity: None,
            rpki: None,
            search_results: None,
        }
    }

    /// Create a new empty result for a name query
    pub fn new_name(query: String) -> Self {
        Self {
            query,
            query_type: InspectQueryType::Name,
            asinfo: None,
            prefix: None,
            prefixes: None,
            connectivity: None,
            rpki: None,
            search_results: None,
        }
    }
}

/// Combined result for multiple queries
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectResult {
    /// Individual query results
    pub queries: Vec<InspectQueryResult>,

    /// Processing metadata
    pub meta: InspectResultMeta,
}

/// Metadata about the inspect operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectResultMeta {
    pub query_count: usize,
    pub asn_queries: usize,
    pub prefix_queries: usize,
    pub name_queries: usize,
    pub processing_time_ms: u64,
}

// =============================================================================
// Display Configuration
// =============================================================================

/// Display mode for multi-ASN queries
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MultiAsnDisplayMode {
    /// Standard mode - show each ASN result separately
    #[default]
    Standard,
    /// Table mode - show all ASNs in a single table, one per row
    Table,
}

/// Determines display configuration based on terminal width
#[derive(Debug, Clone)]
pub struct InspectDisplayConfig {
    pub terminal_width: usize,
    pub show_hegemony: bool,
    pub show_population: bool,
    pub show_peeringdb: bool,
    pub truncate_names: bool,
    pub name_max_width: usize,
    /// Use markdown table style instead of rounded
    pub use_markdown_style: bool,
    /// Force showing extended info (peeringdb, hegemony, population) regardless of width
    pub force_extended_info: bool,
    /// Display mode for multi-ASN queries
    pub multi_asn_mode: MultiAsnDisplayMode,
}

impl InspectDisplayConfig {
    /// Create display config based on terminal width
    pub fn from_terminal_width(width: usize) -> Self {
        match width {
            0..=80 => Self {
                terminal_width: width,
                show_hegemony: false,
                show_population: false,
                show_peeringdb: false,
                truncate_names: true,
                name_max_width: 25,
                use_markdown_style: false,
                force_extended_info: false,
                multi_asn_mode: MultiAsnDisplayMode::Standard,
            },
            81..=120 => Self {
                terminal_width: width,
                show_hegemony: false,
                show_population: false,
                show_peeringdb: false,
                truncate_names: true,
                name_max_width: 35,
                use_markdown_style: false,
                force_extended_info: false,
                multi_asn_mode: MultiAsnDisplayMode::Standard,
            },
            121..=160 => Self {
                terminal_width: width,
                show_hegemony: true,
                show_population: false,
                show_peeringdb: false,
                truncate_names: true,
                name_max_width: 45,
                use_markdown_style: false,
                force_extended_info: false,
                multi_asn_mode: MultiAsnDisplayMode::Standard,
            },
            _ => Self {
                terminal_width: width,
                show_hegemony: true,
                show_population: true,
                show_peeringdb: true,
                truncate_names: false,
                name_max_width: 60,
                use_markdown_style: false,
                force_extended_info: false,
                multi_asn_mode: MultiAsnDisplayMode::Standard,
            },
        }
    }

    /// Auto-detect terminal width
    ///
    /// Uses the COLUMNS environment variable if available, otherwise defaults to 80.
    pub fn auto() -> Self {
        let width = std::env::var("COLUMNS")
            .ok()
            .and_then(|s| s.parse::<usize>().ok())
            .unwrap_or(80);
        Self::from_terminal_width(width)
    }

    /// Set markdown style output
    pub fn with_markdown(mut self, use_markdown: bool) -> Self {
        self.use_markdown_style = use_markdown;
        self
    }

    /// Force extended info (peeringdb, hegemony, population) regardless of terminal width
    pub fn with_extended_info(mut self, force: bool) -> Self {
        self.force_extended_info = force;
        if force {
            self.show_hegemony = true;
            self.show_population = true;
            self.show_peeringdb = true;
        }
        self
    }

    /// Set multi-ASN display mode
    pub fn with_multi_asn_mode(mut self, mode: MultiAsnDisplayMode) -> Self {
        self.multi_asn_mode = mode;
        self
    }

    /// Check if hegemony should be shown (respects force_extended_info)
    pub fn should_show_hegemony(&self) -> bool {
        self.force_extended_info || self.show_hegemony
    }

    /// Check if population should be shown (respects force_extended_info)
    pub fn should_show_population(&self) -> bool {
        self.force_extended_info || self.show_population
    }

    /// Check if peeringdb should be shown (respects force_extended_info)
    pub fn should_show_peeringdb(&self) -> bool {
        self.force_extended_info || self.show_peeringdb
    }
}

impl Default for InspectDisplayConfig {
    fn default() -> Self {
        Self::auto()
    }
}