Inital Vuln DB
This commit is contained in:
+97
-8
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user