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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user