Fix 29 audit findings across all severity tiers

Critical: fix unsquashfs arg order, quote Exec paths with spaces,
fix compare_versions antisymmetry, chunk-based signature detection,
bounded ELF header reads.

High: handle NULL CVE severity, prevent pipe deadlock in inspector,
fix glob_match edge case, fix backup archive path collisions, async
crash detection with stderr capture.

Medium: gate scan on auto-scan setting, fix window size persistence,
fix announce() for Stack containers, claim lightbox gesture, use
serde_json for CLI output, remove dead CSS @media blocks, add
detail-tab persistence, remove invalid metainfo categories, byte-level
fuse signature search.

Low: tighten Wayland env var detection, ELF magic validation,
timeout for update info extraction, quoted arg parsing, stop watcher
timer on window destroy, GSettings choices/range constraints, remove
unused CSS classes, define status-ok/status-attention CSS.
This commit is contained in:
lashman
2026-02-27 22:08:53 +02:00
parent f87403794e
commit e9343da249
27 changed files with 1737 additions and 250 deletions

View File

@@ -48,6 +48,20 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
view_stack.add_titled(&storage_page, Some("storage"), "Storage");
view_stack.page(&storage_page).set_icon_name(Some("drive-harddisk-symbolic"));
// Restore last-used tab from GSettings
let settings = gio::Settings::new(crate::config::APP_ID);
let saved_tab = settings.string("detail-tab");
if view_stack.child_by_name(&saved_tab).is_some() {
view_stack.set_visible_child_name(&saved_tab);
}
// Persist tab choice on switch
view_stack.connect_visible_child_name_notify(move |stack| {
if let Some(name) = stack.visible_child_name() {
settings.set_string("detail-tab", &name).ok();
}
});
// Banner scrolls with content (not sticky) so tall banners don't eat space
let scroll_content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
@@ -83,6 +97,7 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
let record_id = record.id;
let path = record.path.clone();
let app_name_launch = record.app_name.clone().unwrap_or_else(|| record.filename.clone());
let launch_args_raw = record.launch_args.clone();
let db_launch = db.clone();
let toast_launch = toast_overlay.clone();
launch_button.connect_clicked(move |btn| {
@@ -92,6 +107,7 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
let app_name = app_name_launch.clone();
let db_launch = db_launch.clone();
let toast_ref = toast_launch.clone();
let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
glib::spawn_future_local(async move {
let path_bg = path.clone();
let result = gio::spawn_blocking(move || {
@@ -101,7 +117,7 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
record_id,
appimage_path,
"gui_detail",
&[],
&launch_args,
&[],
)
}).await;
@@ -121,13 +137,16 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
}).await;
if let Ok(Ok(analysis)) = analysis_result {
let status_str = analysis.as_status_str();
log::info!("Runtime Wayland: {} -> {}", path_clone, analysis.status_label());
log::info!(
"Runtime Wayland: {} -> {} (pid={}, env: {:?})",
path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
);
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
}
});
}
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, .. }) => {
log::error!("App crashed on launch (exit {}): {}", exit_code.unwrap_or(-1), stderr);
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
}
Ok(launcher::LaunchResult::Failed(msg)) => {
@@ -247,9 +266,7 @@ fn build_banner(record: &AppImageRecord) -> gtk::Box {
.margin_top(4)
.build();
if record.integrated {
badge_box.append(&widgets::status_badge("Integrated", "success"));
}
badge_box.append(&widgets::integration_badge(record.integrated));
if let Some(ref ws) = record.wayland_status {
let status = WaylandStatus::from_str(ws);
@@ -1582,6 +1599,10 @@ fn build_backup_group(record_id: i64, toast_overlay: &adw::ToastOverlay) -> adw:
group.add(&empty_row);
} else {
for b in &backups {
log::debug!(
"Listing backup id={} for appimage_id={} at {}",
b.id, b.appimage_id, b.archive_path,
);
let expander = adw::ExpanderRow::builder()
.title(&b.created_at)
.subtitle(&format!(
@@ -1656,12 +1677,28 @@ fn build_backup_group(record_id: i64, toast_overlay: &adw::ToastOverlay) -> adw:
row_clone.set_sensitive(true);
match result {
Ok(Ok(res)) => {
let skip_note = if res.paths_skipped > 0 {
format!(" ({} skipped)", res.paths_skipped)
} else {
String::new()
};
row_clone.set_subtitle(&format!(
"Restored {} path{}",
"Restored {} path{}{}",
res.paths_restored,
if res.paths_restored == 1 { "" } else { "s" },
skip_note,
));
toast.add_toast(adw::Toast::new("Backup restored"));
let toast_msg = format!(
"Restored {} path{}{}",
res.paths_restored,
if res.paths_restored == 1 { "" } else { "s" },
skip_note,
);
toast.add_toast(adw::Toast::new(&toast_msg));
log::info!(
"Backup restored: app={}, paths_restored={}, paths_skipped={}",
res.manifest.app_name, res.paths_restored, res.paths_skipped,
);
}
_ => {
row_clone.set_subtitle("Restore failed");
@@ -1922,7 +1959,9 @@ fn show_screenshot_lightbox(
// --- Click outside image to close ---
// Picture's gesture claims clicks on the image, preventing close.
let pic_gesture = gtk::GestureClick::new();
pic_gesture.connect_released(|_, _, _, _| {});
pic_gesture.connect_released(|gesture, _, _, _| {
gesture.set_state(gtk::EventSequenceState::Claimed);
});
picture.add_controller(pic_gesture);
// Window gesture fires for clicks on the dark margin area.

View File

@@ -2,6 +2,8 @@ use adw::prelude::*;
use gtk::gio;
use crate::config::APP_ID;
use crate::core::appstream;
use crate::core::database::Database;
use crate::i18n::i18n;
pub fn show_preferences_dialog(parent: &impl IsA<gtk::Widget>) {
@@ -153,6 +155,47 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
scan_group.add(&add_button);
page.add(&scan_group);
// Desktop Integration group - AppStream catalog for GNOME Software/Discover
let integration_group = adw::PreferencesGroup::builder()
.title(&i18n("Desktop Integration"))
.description(&i18n(
"Make your AppImages visible in GNOME Software and KDE Discover",
))
.build();
let catalog_row = adw::SwitchRow::builder()
.title(&i18n("AppStream catalog"))
.subtitle(&i18n(
"Generate a local catalog so software centers can list your AppImages",
))
.active(appstream::is_catalog_installed())
.build();
catalog_row.connect_active_notify(|row| {
let enable = row.is_active();
glib::spawn_future_local(async move {
let result = gio::spawn_blocking(move || {
if enable {
let db = Database::open().expect("Failed to open database");
appstream::install_catalog(&db)
.map(|p| log::info!("AppStream catalog installed: {}", p.display()))
.map_err(|e| e.to_string())
} else {
appstream::uninstall_catalog()
.map(|()| log::info!("AppStream catalog removed"))
.map_err(|e| e.to_string())
}
})
.await;
if let Ok(Err(e)) = result {
log::warn!("AppStream catalog toggle failed: {}", e);
}
});
});
integration_group.add(&catalog_row);
page.add(&integration_group);
page
}

View File

@@ -61,7 +61,17 @@ pub fn build_security_report_page(db: &Rc<Database>) -> adw::NavigationPage {
if let Ok(results) = result {
let total_cves: usize = results.iter().map(|r| r.total_cves()).sum();
for r in &results {
log::info!(
"Security scan: appimage_id={} found {} CVEs",
r.appimage_id, r.total_cves(),
);
}
log::info!("Security scan complete: {} CVEs found across {} AppImages", total_cves, results.len());
widgets::announce(
&stack_refresh,
&format!("Security scan complete: {} vulnerabilities found", total_cves),
);
// Refresh the page content with updated data
let new_content = build_report_content(&db_refresh);
@@ -119,9 +129,14 @@ pub fn build_security_report_page(db: &Rc<Database>) -> adw::NavigationPage {
filters.append(&json_filter);
filters.append(&csv_filter);
let default_format = report::ReportFormat::Html;
let initial_name = format!(
"driftwood-security-report.{}",
default_format.extension(),
);
let dialog = gtk::FileDialog::builder()
.title("Export Security Report")
.initial_name("driftwood-security-report.html")
.initial_name(&initial_name)
.filters(&filters)
.default_filter(&html_filter)
.modal(true)
@@ -142,11 +157,8 @@ pub fn build_security_report_page(db: &Rc<Database>) -> adw::NavigationPage {
.unwrap_or("html")
.to_lowercase();
let format = match ext.as_str() {
"json" => report::ReportFormat::Json,
"csv" => report::ReportFormat::Csv,
_ => report::ReportFormat::Html,
};
let format = report::ReportFormat::from_str(&ext)
.unwrap_or(report::ReportFormat::Html);
btn_clone.set_sensitive(false);
btn_clone.set_label("Exporting...");

View File

@@ -88,7 +88,6 @@ pub fn status_badge_with_icon(icon_name: &str, text: &str, style_class: &str) ->
}
/// Create a badge showing integration status.
#[allow(dead_code)]
pub fn integration_badge(integrated: bool) -> gtk::Label {
if integrated {
status_badge("Integrated", "success")
@@ -345,7 +344,6 @@ fn crash_explanation(stderr: &str) -> String {
/// Inserts a hidden label with AccessibleRole::Alert into the given container,
/// which causes AT-SPI to announce the text to screen readers.
/// The label auto-removes after a short delay.
#[allow(dead_code)]
pub fn announce(container: &impl gtk::prelude::IsA<gtk::Widget>, text: &str) {
let label = gtk::Label::builder()
.label(text)
@@ -354,7 +352,16 @@ pub fn announce(container: &impl gtk::prelude::IsA<gtk::Widget>, text: &str) {
.build();
label.update_property(&[gtk::accessible::Property::Label(text)]);
if let Some(box_widget) = container.dynamic_cast_ref::<gtk::Box>() {
// Try to find a suitable Box container to attach the label to
let target_box = container.dynamic_cast_ref::<gtk::Box>().cloned()
.or_else(|| {
// For Stack widgets, use the visible child if it's a Box
container.dynamic_cast_ref::<gtk::Stack>()
.and_then(|s| s.visible_child())
.and_then(|c| c.downcast::<gtk::Box>().ok())
});
if let Some(box_widget) = target_box {
box_widget.append(&label);
label.set_visible(true);
let label_clone = label.clone();