Inital Vuln DB

This commit is contained in:
KaraZajac
2026-02-17 21:04:09 -05:00
parent 9d55d1a1ad
commit aeccb649a7
8 changed files with 535 additions and 21 deletions
+97 -8
View File
@@ -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<u32>,
}
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);
+16
View File
@@ -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<RfModulation>,
/// Vehicle/year for vulnerability lookup and .fob export (set via 'i' in UI).
#[serde(default)]
pub year: Option<String>,
/// Make for vulnerability lookup and .fob export.
#[serde(default)]
pub make: Option<String>,
/// Model for vulnerability lookup and .fob export.
#[serde(default)]
pub model: Option<String>,
/// Region (e.g. NA, EU) for vulnerability lookup and .fob export.
#[serde(default)]
pub region: Option<String>,
}
/// 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,
}
}
+8
View File
@@ -302,6 +302,10 @@ fn import_fob_v2(fob: &FobFile, next_id: u32) -> Result<Capture> {
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<Capture> {
raw_pairs,
status,
received_rf: None,
year: None,
make: None,
model: None,
region: None,
})
}
+73
View File
@@ -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<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, 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<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, 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;
+110 -12
View File
@@ -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),
]));
// 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);
}
}
+8
View File
@@ -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",
+162
View File
@@ -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);
}
+60
View File
@@ -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)
}