diff --git a/src/app.rs b/src/app.rs index 13f4991..b3834f5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -126,6 +126,11 @@ pub enum InputMode { FobMetaRegion, /// Fob export metadata: editing notes field FobMetaNotes, + /// Capture metadata (Year/Make/Model/Region) for vuln lookup — press i on a capture + CaptureMetaYear, + CaptureMetaMake, + CaptureMetaModel, + CaptureMetaRegion, /// License overlay (centered box) License, /// Credits overlay (centered box) @@ -347,6 +352,14 @@ pub struct App { pub fob_meta_region: String, /// Notes input buffer pub fob_meta_notes: String, + + // -- Capture metadata (Year/Make/Model/Region for vuln lookup, set via 'i') -- + pub capture_meta_year: String, + pub capture_meta_make: String, + pub capture_meta_model: String, + pub capture_meta_region: String, + /// Which capture is being edited (when in CaptureMeta* modes) + pub capture_meta_capture_id: Option, } impl App { @@ -451,6 +464,11 @@ impl App { fob_meta_model: String::new(), fob_meta_region: String::new(), fob_meta_notes: String::new(), + capture_meta_year: String::new(), + capture_meta_make: String::new(), + capture_meta_model: String::new(), + capture_meta_region: String::new(), + capture_meta_capture_id: None, }) } @@ -907,7 +925,11 @@ impl App { } /// True if a capture with the same protocol, data, serial, and button already exists. + /// Unknown signals (no protocol) are never treated as duplicates so they can be kept for research. fn capture_duplicate_of_existing(&self, capture: &Capture) -> bool { + if capture.protocol.is_none() { + return false; + } self.captures.iter().any(|c| { c.protocol == capture.protocol && c.data == capture.data @@ -1072,18 +1094,32 @@ impl App { .map(|c| Self::default_export_filename(c)) .unwrap_or_else(|| format!("capture_{}", id)); - // Pre-fill make from protocol - let make = self.captures.iter().find(|c| c.id == id).map(|c| { - Self::get_make_for_protocol(c.protocol_name()).to_string() - }).unwrap_or_default(); - + // Pre-fill metadata from capture if set, otherwise make from protocol + let capture = self.captures.iter().find(|c| c.id == id); + let make = capture + .and_then(|c| c.make.as_ref().map(String::clone)) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| { + capture + .map(|c| Self::get_make_for_protocol(c.protocol_name()).to_string()) + .unwrap_or_default() + }); self.export_capture_id = Some(id); self.export_filename = default_name; self.export_format = Some(ExportFormat::Fob); - self.fob_meta_year = String::new(); + self.fob_meta_year = capture + .and_then(|c| c.year.as_ref()) + .map(String::clone) + .unwrap_or_default(); self.fob_meta_make = make; - self.fob_meta_model = String::new(); - self.fob_meta_region = String::new(); + self.fob_meta_model = capture + .and_then(|c| c.model.as_ref()) + .map(String::clone) + .unwrap_or_default(); + self.fob_meta_region = capture + .and_then(|c| c.region.as_ref()) + .map(String::clone) + .unwrap_or_default(); self.fob_meta_notes = String::new(); self.input_mode = InputMode::ExportFilename; Ok(()) @@ -1136,6 +1172,55 @@ impl App { Ok(()) } + /// Open the capture metadata form for the given capture (Year/Make/Model/Region). Called when user presses 'i'. + pub fn open_capture_meta_form(&mut self, capture_id: u32) { + let capture = self.captures.iter().find(|c| c.id == capture_id); + self.capture_meta_year = capture + .and_then(|c| c.year.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.capture_meta_make = capture + .and_then(|c| c.make.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.capture_meta_model = capture + .and_then(|c| c.model.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.capture_meta_region = capture + .and_then(|c| c.region.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.capture_meta_capture_id = Some(capture_id); + self.input_mode = InputMode::CaptureMetaYear; + } + + /// Save capture metadata from the form into the selected capture and return to Normal. + pub fn save_capture_meta(&mut self) { + let id = match self.capture_meta_capture_id { + Some(id) => id, + None => { + self.input_mode = InputMode::Normal; + self.capture_meta_capture_id = None; + return; + } + }; + if let Some(capture) = self.captures.iter_mut().find(|c| c.id == id) { + capture.year = Some(self.capture_meta_year.clone()).filter(|s| !s.is_empty()); + capture.make = Some(self.capture_meta_make.clone()).filter(|s| !s.is_empty()); + capture.model = Some(self.capture_meta_model.clone()).filter(|s| !s.is_empty()); + capture.region = Some(self.capture_meta_region.clone()).filter(|s| !s.is_empty()); + } + self.input_mode = InputMode::Normal; + self.capture_meta_capture_id = None; + } + + /// Cancel capture metadata form without saving. + pub fn cancel_capture_meta(&mut self) { + self.input_mode = InputMode::Normal; + self.capture_meta_capture_id = None; + } + /// Import pending .fob and .sub files into captures list. /// .sub files are decoded with registered protocols after load (no metadata in file). /// When research_mode is off, only decoded captures are added (same as live capture). @@ -1406,6 +1491,10 @@ impl App { raw_pairs: vec![], status: crate::capture::CaptureStatus::EncoderCapable, received_rf: None, + year: None, + make: None, + model: None, + region: None, }; self.next_capture_id += 1; self.captures.push(capture); diff --git a/src/capture.rs b/src/capture.rs index bcb641e..928fa45 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -64,6 +64,18 @@ pub struct Capture { /// Which demodulator path produced this capture (AM or FM). None if unknown/imported. #[serde(default)] pub received_rf: Option, + /// Vehicle/year for vulnerability lookup and .fob export (set via 'i' in UI). + #[serde(default)] + pub year: Option, + /// Make for vulnerability lookup and .fob export. + #[serde(default)] + pub make: Option, + /// Model for vulnerability lookup and .fob export. + #[serde(default)] + pub model: Option, + /// Region (e.g. NA, EU) for vulnerability lookup and .fob export. + #[serde(default)] + pub region: Option, } /// Modulation type used by protocol (encoding: PWM vs Manchester) @@ -138,6 +150,10 @@ impl Capture { raw_pairs: pairs, status: CaptureStatus::Unknown, received_rf, + year: None, + make: None, + model: None, + region: None, } } diff --git a/src/export/fob.rs b/src/export/fob.rs index 9cf7bf4..df7ee52 100644 --- a/src/export/fob.rs +++ b/src/export/fob.rs @@ -302,6 +302,10 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result { raw_pairs, status, received_rf: None, + year: None, + make: None, + model: None, + region: None, }) } @@ -368,6 +372,10 @@ fn import_fob_v1(fob: &FobFileV1, next_id: u32) -> Result { raw_pairs, status, received_rf: None, + year: None, + make: None, + model: None, + region: None, }) } diff --git a/src/main.rs b/src/main.rs index 94b12ec..612cd43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod protocols; mod radio; mod storage; mod ui; +mod vuln_db; use anyhow::Result; use crossterm::{ @@ -148,6 +149,14 @@ fn run_app(terminal: &mut Terminal, app: &mut A app.signal_menu_index = 0; } } + KeyCode::Char('i') => { + // Edit Year/Make/Model/Region for vuln lookup + if let Some(idx) = app.selected_capture { + if let Some(capture) = app.captures.get(idx) { + app.open_capture_meta_form(capture.id); + } + } + } KeyCode::Tab => { // Open settings selector app.input_mode = InputMode::SettingsSelect; @@ -408,6 +417,70 @@ fn run_app(terminal: &mut Terminal, app: &mut A _ => {} }, + // Capture metadata (Year/Make/Model/Region for vuln lookup) + InputMode::CaptureMetaYear => match key.code { + KeyCode::Enter => { + app.input_mode = InputMode::CaptureMetaMake; + } + KeyCode::Char(c) if c.is_ascii_digit() => { + if app.capture_meta_year.len() < 4 { + app.capture_meta_year.push(c); + } + } + KeyCode::Backspace => { + app.capture_meta_year.pop(); + } + KeyCode::Esc => { + app.cancel_capture_meta(); + } + _ => {} + }, + InputMode::CaptureMetaMake => match key.code { + KeyCode::Enter => { + app.input_mode = InputMode::CaptureMetaModel; + } + KeyCode::Char(c) => { + app.capture_meta_make.push(c); + } + KeyCode::Backspace => { + app.capture_meta_make.pop(); + } + KeyCode::Esc => { + app.cancel_capture_meta(); + } + _ => {} + }, + InputMode::CaptureMetaModel => match key.code { + KeyCode::Enter => { + app.input_mode = InputMode::CaptureMetaRegion; + } + KeyCode::Char(c) => { + app.capture_meta_model.push(c); + } + KeyCode::Backspace => { + app.capture_meta_model.pop(); + } + KeyCode::Esc => { + app.cancel_capture_meta(); + } + _ => {} + }, + InputMode::CaptureMetaRegion => match key.code { + KeyCode::Enter => { + app.save_capture_meta(); + } + KeyCode::Char(c) => { + app.capture_meta_region.push(c); + } + KeyCode::Backspace => { + app.capture_meta_region.pop(); + } + KeyCode::Esc => { + app.cancel_capture_meta(); + } + _ => {} + }, + InputMode::License | InputMode::Credits => match key.code { KeyCode::Esc | KeyCode::Enter => { app.input_mode = InputMode::Normal; diff --git a/src/ui/captures_list.rs b/src/ui/captures_list.rs index 45d3bf8..069ff9e 100644 --- a/src/ui/captures_list.rs +++ b/src/ui/captures_list.rs @@ -10,6 +10,7 @@ use ratatui::{ use crate::app::App; use crate::capture::CaptureStatus; +use crate::vuln_db; /// Render the captures area: table + detail panel pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) { @@ -45,6 +46,7 @@ pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) { fn render_table(frame: &mut Frame, area: Rect, app: &App) { let header_cells = [ "ID", "Time", "Protocol", "Freq", "Serial", "Btn", "Cnt", "Modulation", "CRC", "Status", + "Vuln Found", ] .iter() .map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD))); @@ -81,6 +83,21 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) { CaptureStatus::Unknown => "Unknown", }; + let vuln_found = capture.status == CaptureStatus::EncoderCapable + || !vuln_db::match_vulns( + capture.year.as_deref(), + capture.make.as_deref(), + capture.model.as_deref(), + capture.region.as_deref(), + ) + .is_empty(); + let vuln_text = if vuln_found { "Yes" } else { "No" }; + let vuln_style = if vuln_found { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + }; + Row::new(vec![ Cell::from(format!("{:02}", capture.id)), Cell::from(capture.timestamp_short()), @@ -92,6 +109,7 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) { Cell::from(capture.modulation().to_string()).style(mod_style), Cell::from(capture.crc_status()).style(crc_style), Cell::from(status_text).style(status_style), + Cell::from(vuln_text).style(vuln_style), ]) .height(1) }); @@ -107,6 +125,7 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) { Constraint::Length(12), // Modulation Constraint::Length(5), // CRC Constraint::Length(10), // Status + Constraint::Length(10), // Vuln Found ]; let table = Table::new(rows, widths) @@ -129,13 +148,24 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) { frame.render_stateful_widget(table, area, &mut state); } -/// Render the detail panel for the selected signal +/// Render the detail panel for the selected signal (left = signal info, right = vulnerability) fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { let capture = match app.selected_capture { Some(idx) if idx < app.captures.len() => &app.captures[idx], _ => return, }; + let halves = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + render_signal_detail(frame, halves[0], capture); + render_vulnerability_panel(frame, halves[1], capture); +} + +/// Left half: signal information +fn render_signal_detail(frame: &mut Frame, area: Rect, capture: &crate::capture::Capture) { let label_style = Style::default() .fg(Color::DarkGray) .add_modifier(Modifier::BOLD); @@ -144,10 +174,8 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { let good_style = Style::default().fg(Color::Green); let bad_style = Style::default().fg(Color::Red); - // Build detail content in two columns let make = App::get_make_for_protocol(capture.protocol_name()); - // --- Left column lines --- let mut left_lines = Vec::new(); // Row 1: Protocol + Make @@ -158,24 +186,28 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { Span::styled(make, value_style), ])); - // Row 2: Freq + Mod + RF (protocol) + Enc + Rx (demodulator path when known) - let mut row2 = vec![ + // Row 2: Freq + Mod + RF + left_lines.push(Line::from(vec![ Span::styled(" Freq: ", label_style), Span::styled(capture.frequency_mhz(), value_style), Span::styled(" Mod: ", label_style), Span::styled(capture.modulation().to_string(), value_style), Span::styled(" RF: ", label_style), Span::styled(capture.rf_modulation().to_string(), value_style), - Span::styled(" Enc: ", label_style), + ])); + + // Row 3: Enc + Rx (demodulator path when known) + let mut row3 = vec![ + Span::styled(" Enc: ", label_style), Span::styled(capture.encryption_type(), value_style), ]; if let Some(rf) = capture.received_rf { - row2.push(Span::styled(" Rx: ", label_style)); - row2.push(Span::styled(rf.to_string(), value_style)); + row3.push(Span::styled(" Rx: ", label_style)); + row3.push(Span::styled(rf.to_string(), value_style)); } - left_lines.push(Line::from(row2)); + left_lines.push(Line::from(row3)); - // Row 3: Full Serial + Button + // Row 4: Full Serial + Button left_lines.push(Line::from(vec![ Span::styled(" Serial: ", label_style), Span::styled(format!("0x{}", capture.serial_hex()), accent_style), @@ -186,7 +218,7 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { ), ])); - // Row 4: Counter + CRC + // Row 5: Counter + CRC let crc_span = if capture.protocol.is_none() { Span::styled("-", Style::default().fg(Color::DarkGray)) } else if capture.crc_valid { @@ -204,7 +236,7 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { Span::styled(capture.status.to_string(), value_style), ])); - // Row 5: Full data/key hex + // Row 6: Full data/key hex left_lines.push(Line::from(vec![ Span::styled(" Key/Data: ", label_style), Span::styled( @@ -217,7 +249,7 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { ), ])); - // Row 6: Timestamp + Raw data info + // Row 7: Timestamp + Raw data info let raw_info = if capture.has_raw_data() { format!("✓ {} transitions", capture.raw_pair_count()) } else { @@ -263,3 +295,69 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { frame.render_widget(detail, area); } + +/// Right half: vulnerability panel (matching CVEs or prompt to set Year/Make/Model) +fn render_vulnerability_panel( + frame: &mut Frame, + area: Rect, + capture: &crate::capture::Capture, +) { + let label_style = Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::BOLD); + let value_style = Style::default().fg(Color::White); + let accent_style = Style::default().fg(Color::Cyan); + + let vulns = vuln_db::match_vulns( + capture.year.as_deref(), + capture.make.as_deref(), + capture.model.as_deref(), + capture.region.as_deref(), + ); + + let mut lines = Vec::new(); + if vulns.is_empty() { + let has_meta = capture.year.is_some() + || capture.make.is_some() + || capture.model.is_some() + || capture.region.is_some(); + if has_meta { + lines.push(Line::from(Span::styled( + " No matching CVE in database.", + value_style, + ))); + } else { + lines.push(Line::from(Span::styled( + " Set Year, Make, Model, Region", + value_style, + ))); + lines.push(Line::from(Span::styled( + " (press i) to check vulnerabilities.", + value_style, + ))); + } + } else { + for v in vulns { + lines.push(Line::from(vec![ + Span::styled(" CVE: ", label_style), + Span::styled(v.cve, accent_style), + ])); + lines.push(Line::from(vec![ + Span::styled(" Description: ", label_style), + Span::styled(v.description, value_style), + ])); + lines.push(Line::from(Span::raw(""))); + } + } + + let block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + .title(" Vulnerability "); + let inner = block.inner(area); + frame.render_widget(block, area); + if inner.width > 0 && inner.height > 0 { + let para = Paragraph::new(lines).wrap(Wrap { trim: false }); + frame.render_widget(para, inner); + } +} diff --git a/src/ui/command.rs b/src/ui/command.rs index 50bae70..37752cf 100644 --- a/src/ui/command.rs +++ b/src/ui/command.rs @@ -58,6 +58,14 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) { "EXPORT", Style::default().fg(Color::Green), ), + InputMode::CaptureMetaYear + | InputMode::CaptureMetaMake + | InputMode::CaptureMetaModel + | InputMode::CaptureMetaRegion => ( + String::new(), + "META", + Style::default().fg(Color::Cyan), + ), InputMode::License => ( String::new(), "LICENSE", diff --git a/src/ui/layout.rs b/src/ui/layout.rs index a0c9cd9..5e3ae4f 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -110,6 +110,16 @@ pub fn draw_ui(frame: &mut Frame, app: &App) { render_export_form(frame, app); } + if matches!( + app.input_mode, + InputMode::CaptureMetaYear + | InputMode::CaptureMetaMake + | InputMode::CaptureMetaModel + | InputMode::CaptureMetaRegion + ) { + render_capture_meta_form(frame, app); + } + if app.input_mode == InputMode::License { render_text_overlay(frame, app, "License", LICENSE_TEXT, Alignment::Left); } @@ -229,6 +239,10 @@ fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) { | InputMode::FobMetaModel | InputMode::FobMetaRegion => "Enter: Next Field | Esc: Cancel Export", InputMode::FobMetaNotes => "Enter: Save & Export | Esc: Cancel Export", + InputMode::CaptureMetaYear + | InputMode::CaptureMetaMake + | InputMode::CaptureMetaModel => "Enter: Next Field | Esc: Cancel", + InputMode::CaptureMetaRegion => "Enter: Save | Esc: Cancel", InputMode::License | InputMode::Credits => "Esc/Enter: Close | Up/Down: Scroll", }; @@ -546,3 +560,151 @@ fn render_export_form(frame: &mut Frame, app: &App) { frame.render_widget(paragraph, popup); } + +/// Render the capture metadata form (Year/Make/Model/Region for vuln lookup). Shown when user presses 'i' on a capture. +fn render_capture_meta_form(frame: &mut Frame, app: &App) { + let area = frame.area(); + let popup = centered_rect(62, 18, area); + + frame.render_widget(Clear, popup); + + let active_style = Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD); + let inactive_style = Style::default().fg(Color::DarkGray); + let done_style = Style::default().fg(Color::Green); + let value_style = Style::default().fg(Color::White); + let dim_style = Style::default().fg(Color::DarkGray); + let accent_style = Style::default().fg(Color::Yellow); + let cursor = Span::styled( + "_", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::RAPID_BLINK), + ); + + let field_modes = [ + InputMode::CaptureMetaYear, + InputMode::CaptureMetaMake, + InputMode::CaptureMetaModel, + InputMode::CaptureMetaRegion, + ]; + let current_idx = field_modes + .iter() + .position(|m| *m == app.input_mode) + .unwrap_or(0); + + let style_for = |idx: usize| -> Style { + if idx == current_idx { + active_style + } else if idx < current_idx { + done_style + } else { + inactive_style + } + }; + + let mut lines = Vec::new(); + + if let Some(capture) = app + .capture_meta_capture_id + .and_then(|id| app.captures.iter().find(|c| c.id == id)) + { + lines.push(Line::from(vec![ + Span::styled(" Signal: ", dim_style), + Span::styled( + format!( + "#{:02} {} | {} | {} | 0x{}", + capture.id, + capture.protocol_name(), + capture.frequency_mhz(), + capture.modulation(), + capture.serial_hex(), + ), + accent_style, + ), + ])); + lines.push(Line::from(vec![ + Span::styled(" Key: ", dim_style), + Span::styled( + format!("0x{} ({})", capture.data_hex(), capture.encryption_type()), + accent_style, + ), + ])); + } + + lines.push(Line::from(Span::styled( + " ──────────────────────────────────────────────────────", + dim_style, + ))); + + struct FormField<'a> { + label: &'a str, + value: &'a str, + placeholder: &'a str, + idx: usize, + } + let fields: [FormField; 4] = [ + FormField { + label: " Year: ", + value: &app.capture_meta_year, + placeholder: "(e.g. 2021)", + idx: 0, + }, + FormField { + label: " Make: ", + value: &app.capture_meta_make, + placeholder: "(e.g. Renault, Honda)", + idx: 1, + }, + FormField { + label: " Model: ", + value: &app.capture_meta_model, + placeholder: "(e.g. ZOE, Civic — or ALL)", + idx: 2, + }, + FormField { + label: " Region: ", + value: &app.capture_meta_region, + placeholder: "(e.g. NA, EU, or ALL)", + idx: 3, + }, + ]; + + for field in &fields { + let label_s = style_for(field.idx); + let display_val = if field.value.is_empty() { + field.placeholder + } else { + field.value + }; + let val_s = if field.value.is_empty() && field.idx != current_idx { + dim_style + } else { + value_style + }; + let mut spans = vec![ + Span::styled(field.label, label_s), + Span::styled(display_val.to_string(), val_s), + ]; + if field.idx == current_idx { + spans.push(cursor.clone()); + } + if field.idx < current_idx && !field.value.is_empty() { + spans.push(Span::styled(" ✓", done_style)); + } + lines.push(Line::from(spans)); + } + + lines.push(Line::from("")); + let progress = format!(" Step {}/4 — Used for vuln lookup and .fob export", current_idx + 1); + lines.push(Line::from(Span::styled(progress, dim_style))); + + let block = Block::default() + .title(" Capture Metadata — Year / Make / Model / Region ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan)); + + let paragraph = Paragraph::new(lines).block(block); + frame.render_widget(paragraph, popup); +} diff --git a/src/vuln_db.rs b/src/vuln_db.rs new file mode 100644 index 0000000..cbf5fb8 --- /dev/null +++ b/src/vuln_db.rs @@ -0,0 +1,60 @@ +//! Vulnerability database: CVE entries matched by Year, Make, Model, Region. +//! Used for "Vuln Found" column and the Vulnerability detail panel. + +/// One CVE entry. Year/Make/Model/Region use "ALL" to match any value. +#[derive(Debug, Clone)] +pub struct VulnEntry { + pub cve: &'static str, + pub year: &'static str, + pub make: &'static str, + pub model: &'static str, + pub region: &'static str, + pub description: &'static str, +} + +pub const VULN_DB: [VulnEntry; 2] = [ + VulnEntry { + cve: "CVE-2022-38766", + year: "2021", + make: "Renault", + model: "ALL", + region: "ALL", + description: "The remote keyless system on Renault ZOE 2021 vehicles sends 433.92 MHz RF signals from the same Rolling Codes set for each door-open request, which allows for a replay attack.", + }, + VulnEntry { + cve: "CVE-2022-27254", + year: "2018", + make: "Honda", + model: "Civic", + region: "ALL", + description: "The remote keyless system on Honda Civic 2018 vehicles sends the same RF signal for each door-open request, which allows for a replay attack, a related issue to CVE-2019-20626.", + }, +]; + +/// Match a capture's year/make/model/region against the DB. "ALL" in an entry matches any value. +/// Empty/None from capture is treated as empty string (so "ALL" matches it). +pub fn match_vulns( + year: Option<&str>, + make: Option<&str>, + model: Option<&str>, + region: Option<&str>, +) -> Vec<&'static VulnEntry> { + let y = year.unwrap_or(""); + let m = make.unwrap_or(""); + let mod_ = model.unwrap_or(""); + let r = region.unwrap_or(""); + + VULN_DB + .iter() + .filter(|e| { + (e.year == "ALL" || eq_ignore_case(e.year, y)) + && (e.make == "ALL" || eq_ignore_case(e.make, m)) + && (e.model == "ALL" || eq_ignore_case(e.model, mod_)) + && (e.region == "ALL" || eq_ignore_case(e.region, r)) + }) + .collect() +} + +fn eq_ignore_case(a: &str, b: &str) -> bool { + a.eq_ignore_ascii_case(b) +}